Skip to content
Huseyin Babal
Go back

CVE-2026-98432: the Linux TCP use-after-free, and how to check your server

CVE-2026-98432 is a use-after-free in the kernel code that sets up incoming IPv4 TCP connections. Every kernel from 4.4 on carries it. This post covers what to run on a server, what each vendor has shipped, and how the bug works. It contains no exploit and no proof of concept.

Vendor status below was read on November 1 and 2, 2026. It changes daily, so treat the trackers as the current answer.

Check a server in three steps

Look at the kernel on the host. Containers share it, so rebuilding an image changes nothing.

uname -r

Compare that with your distribution’s fixed package, because upstream version numbers only apply if you build your own kernel. On Debian:

apt-cache policy linux-image-amd64

After installing a fixed kernel, reboot and run uname -r again. A kernel that is installed but not booted protects nothing.

Fix status

Upstream stable releases with the fix: 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10 and 7.3.

DistributionStatus when checked
Debian 12Fixed: linux 6.1.190-1 (DLA-4841-1), linux-6.12 6.12.113-1~deb12u1 (DLA-4850-1)
Debian 13Fixed: 6.12.113-1 (DSA-6568-1)
Amazon Linux 2, 2023Fixed on all kernels. AL2023 instances locked to a release need 2023.12.20261102 or later
Oracle Linux 7 to 10UEK fixed on October 16. No fix listed for the Red Hat compatible kernel
SUSESLES 16, SLES 15 SP4 to SP6 LTSS, SLE Micro 5.3 to 5.5, SL Micro 6.2 and openSUSE Leap 16 fixed. SLES 15 SP7 in progress. Leap 15 and SLES 12 SP5 affected, no fix listed
Ubuntu22.04, 24.04, 26.04, 26.10: vulnerable, work in progress. 16.04, 18.04, 20.04: vulnerable. 14.04: not affected
RHEL 8, 9, 10, OpenShiftRated critical, fixes being expedited, no fixed kernels listed in the bulletin
AlmaLinuxPatched kernels in the testing repository
Rocky LinuxCIQ lists fixed builds for some of its own variants. I found no published fix for the community releases
ArchCurrent package 7.2.14, past the fixed 7.2.10

For managed services:

ServiceStatus when checked
AWS LambdaNot affected
Amazon EKSAMI release v20261031 or later. Existing nodes need replacing
AWS FargateTasks launched after November 1, 17:00 UTC are patched. Older tasks need replacing or retire on their schedule
BottlerocketKernel released, AMIs expected from November 2
GKE, AKSNo mention of this CVE in their bulletins

Debian: which metapackage you have matters

On Bookworm, linux-image-amd64 follows the 6.1 kernel and is at the fixed 6.1.190-1. A machine that was moved to the 6.12 backport won’t be taken along by it. That line has its own metapackage:

sudo apt install linux-image-6.12-amd64

It depends on linux-image-6.12.113+deb12-amd64, the fixed build.

If there is no fix yet

Red Hat publishes an nftables table that drops incoming IPv4 TCP packets carrying IP options. By Red Hat’s own description this lowers the risk without closing it. Try it outside production first, since some workloads may break.

table inet cve_2026_98432 {
    chain input {
        type filter hook input priority -310; policy accept;
        ip protocol tcp ip hdrlength > 5 counter drop
    }
}cve-2026-98432.nft
sudo nft --check --file cve-2026-98432.nft
sudo nft --file cve-2026-98432.nft
sudo nft list table inet cve_2026_98432

The counter shows whether the rule is dropping anything, which is useful for finding out if your traffic ever used IP options. AlmaLinux notes that a rule like this does not survive a reboot.

Be careful with GitHub repositories that claim to hold a proof of concept. Atlas Security’s advice is to assume an unvetted one is hostile.

Finding multipath TCP listeners

The exploit Anthropic describes depends on a multipath TCP listener, and you may have one without asking for it: according to the Go team’s oss-security post, listening sockets in Go programs have used multipath TCP by default since Go 1.24.

The obvious check can give a wrong answer. On an Ubuntu cloud image, this came back empty:

ss -Mlnp
State  Recv-Q  Send-Q   Local Address:Port   Peer Address:Port  Processoutput

The kernel disagreed:

grep -E '^(protocol|MPTCP)' /proc/net/protocols | cut -c1-24
protocol  size sockets
MPTCPv6   2096      0
MPTCP     1936      1output

ss needs the mptcp_diag module, and on this image it lives in an extra package. With that installed, and with root so ss can name the process, the listener appears:

sudo apt install linux-modules-extra-$(uname -r)
sudo ss -Mlnp | awk '{print $1, $4, $6}'
State Local Peer
LISTEN 127.0.0.1:45795 users:(("containerd",pid=3596596,fd=35))output

That one is on localhost, so nothing outside the machine can reach it. Its origin is easy to confirm:

go version /usr/bin/containerd
/usr/bin/containerd: go1.24.13output

A small Go server of my own listed the same way on *:8080, which is every interface.

To turn it off for one service, use a systemd drop-in:

[Service]
Environment=GODEBUG=multipathtcp=0/etc/systemd/system/containerd.service.d/mptcp.conf
sudo systemctl daemon-reload
sudo systemctl restart containerd
sudo ss -Mlnp

The list is then empty for a correct reason, and containerd shows up under ss -tlnp as plain TCP.

This doesn’t fix the bug. Per-program switches are fragile in the Go team’s view, as a single missed listener leaves the machine exposed. They and AlmaLinux both say the underlying bug isn’t specific to multipath TCP, and CIQ warns that net.mptcp.enabled=0 alone leaves the plain TCP path reachable.

How the bug works

A use-after-free means code keeps a pointer to memory that was already released. The allocator can hand that memory to something else, and a read through the old pointer then returns whatever lives there now. In the kernel that can be any kernel data, so reads through the stale pointer may leak secrets and writes may corrupt state.

Here the memory holds IPv4 options, which the kernel saves when a SYN carries them because its SYN-ACK has to repeat them. The steps come from the kernel commit message:

  1. A SYN arrives. Linux keeps a small request socket instead of a full socket, with the options attached as ireq_opt.
  2. The final ACK arrives. tcp_v4_syn_recv_sock() creates the child socket and transfers ireq_opt to it as inet_opt without copying.
  3. Another CPU processes a retransmitted SYN for the same request socket and builds a SYN-ACK from tcp_check_req(), reading those options under rcu_read_lock().
  4. The child socket is torn down. inet_sock_destruct() frees inet_opt with a plain kfree(), right away.
  5. The reader on the second CPU hasn’t finished. ip_options_build() is where the use-after-free lands, and opt->optlen gets read twice along the way.

Three outcomes are on the kernel team’s list: an out-of-bounds write, a crash, or nearby kernel memory going out in the SYN-ACK.

A second path exists through request socket migration (net.ipv4.tcp_migrate_req=1 or a BPF program). The clone and the old request socket share ireq_opt, and tcp_v4_reqsk_destructor() also used plain kfree().

IPv6 avoids the first issue because tcp_v6_syn_recv_sock() duplicates the options. It has the same migration issue with ipv6_opt, which only CALIPSO sets, and that gets a separate patch.

The window dates to commit 079096f103fa in October 2015, when processing request sockets stopped requiring the listener lock.

The fix

It changes two lines:

--- a/net/ipv4/af_inet.c
+++ b/net/ipv4/af_inet.c
@@ void inet_sock_destruct(struct sock *sk)
-	kfree(rcu_dereference_protected(inet->inet_opt, 1));
+	kfree_rcu(rcu_dereference_protected(inet->inet_opt, 1), rcu);

--- a/net/ipv4/tcp_ipv4.c
+++ b/net/ipv4/tcp_ipv4.c
@@ static void tcp_v4_reqsk_destructor(struct request_sock *req)
-	kfree(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1));
+	kfree_rcu(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1), rcu);

The readers were using RCU already, and other paths that replace inet_opt already used kfree_rcu(). With RCU, a reader marks where its read starts and ends. kfree_rcu() holds the memory until a grace period passes, meaning every reader inside at that moment has left. Only these two frees skipped that wait.

8.1 or 9.8

SourceScore
Kernel CVE team, Ubuntu9.8
Red Hat9.8, raised from 8.1 on October 30
Oracle, SUSE, Amazon8.1

The gap is one CVSS field, attack complexity. A 9.8 sets it to low. An 8.1 sets it to high, reasoning that winning a race is hard, and Red Hat’s bulletin also mentions the precise timing involved.

Anthropic says an unreleased model built a remote root exploit against a server with a multipath TCP listener, and that a lost race only fails that one handshake, so the attacker can try again. Nobody outside Anthropic has reproduced this as far as I can tell: the exploit isn’t public, no vendor reported attacks in the wild, and it wasn’t on CISA’s known exploited list when I checked.

Timeline

Date (2026)Event
October 1Fix written by Eric Dumazet, with the report credited to Xinyang Ge of Anthropic
October 15In stable kernels. CVE published
October 24Kernel CVE team scores it 9.8
October 27Anthropic describes its model’s result
October 30Red Hat bulletin moves to 9.8

Sources


Share this post:

Previous Post
Postgres JSONB vs MongoDB under load: the numbers, the plans and the one GIN setting
Next Post
Postgres vs SQLite under load: where each one stops on one machine