Tech12 min read

CVE-2026-98432 Linux TCP Use-After-Free Can Give Unauthenticated Root on MPTCP Listeners

IkesanContents

TL;DR

What happened October 15, 2026: CVE-2026-98432, a use-after-free in IPv4 TCP handling in Linux 4.4 and later, was published. The kernel CNA scores it CVSS 9.8 (Critical); Red Hat scores it 8.1 (Important). On October 27, Anthropic showed unauthenticated remote root on servers with a Multipath TCP (MPTCP) listener

Who’s affected Any kernel from 4.4 up to the fixed releases: 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10 and 7.3. Go 1.24+ programs listen with MPTCP by default, so they match the demonstrated attack’s precondition

What to do

  1. Install a fixed kernel or your distro’s patched kernel package
  2. Reboot, then confirm the running kernel with uname -r
  3. Until then, GODEBUG=multipathtcp=0 turns off MPTCP in Go programs. This only removes the demonstrated precondition: the bug itself is not MPTCP-specific

On October 15, 2026, CVE-2026-98432, a use-after-free (UAF) in the Linux kernel’s IPv4 TCP handling, was published.
A use-after-free happens when a program keeps reading or writing memory it has already freed. Other data can later be placed in that freed region, so if an attacker controls what lands there, it can lead to information leaks or memory corruption.

At first, the only public detail was the fix commit message.
On October 27, Anthropic’s Frontier Red Team published a research post showing that the bug can be used to run commands as root, without authentication, on Linux servers listening with MPTCP, a TCP extension that spreads one connection across multiple paths.
On October 29, the Go security team followed with an advisory, because Go programs tend to meet that precondition.

A child socket and a SYN retransmit race over the same IPv4 options

TCP sets up a connection with a three-way handshake: the client sends SYN, the server replies with SYN-ACK, and the client sends ACK.
When the Linux kernel receives the first SYN, it creates a “request socket”, a lightweight temporary object for the half-open connection.
If the SYN’s IP header carries IPv4 options such as Record Route or Timestamp, the kernel stores them in the request socket’s ireq_opt and uses them when building the SYN-ACK.

When the ACK arrives and the connection is established, tcp_v4_syn_recv_sock() creates the regular socket (the child socket) that the application actually talks through.
The memory behind ireq_opt is not copied at this point. The pointer itself is handed over to the child socket’s inet_opt.
From here on, the same options memory is referenced from both the request socket and the child socket.

If another CPU processes a retransmitted SYN for the same request socket, tcp_check_req() starts resending the SYN-ACK.
tcp_v4_send_synack() and related code read the options inside an RCU (Read-Copy Update) read-side section. RCU lets readers access data without taking a lock; in exchange, whoever frees the data must wait until every current reader is done.
ip_build_and_send_pkt() and ip_options_build(), which then build the SYN-ACK, read the option length optlen twice between them.
ip_build_and_send_pkt() uses it to reserve header room, and ip_options_build() uses it to copy the options into the packet.

While RCU-protected readers exist, writers are supposed to defer the free until those readers leave, a waiting period called the grace period.
But inet_sock_destruct(), the destructor that tears down the child socket, freed inet_opt immediately with a plain kfree().
If the connection is closed right away and the child socket is destroyed, the options memory is freed while another CPU is still building the SYN-ACK, and ip_options_build() reads freed memory.
If the freed region is reused for something else between the two reads of optlen, the two reads disagree on the length. That mismatch can leak memory to the peer or cause out-of-bounds writes.

flowchart TD
    A["Attacker sends SYN<br/>with IPv4 options"] --> B["Kernel stores them in<br/>the request socket's ireq_opt"]
    B --> C["ACK completes the handshake<br/>pointer handed to child's inet_opt"]
    B --> D["Another CPU handles retransmitted SYN<br/>building a SYN-ACK"]
    C --> E["Connection closed, child destroyed<br/>freed immediately with kfree"]
    E --> F["The SYN-ACK being built<br/>reads freed options"]
    D --> F

According to the fix commit, the race was introduced in October 2015 by commit 079096f103fa, “tcp/dccp: install syn_recv requests into ehash table”.
Since that commit, request sockets are processed without holding the listener lock, so nothing prevents the child socket from being freed while a SYN-ACK is still being built.
The CVE record lists Linux 4.4, the first release with that commit, as the start of the affected range.

The commit message also points out a similar use-after-free with request socket migration, which hands pending connection requests from one listener to another (net.ipv4.tcp_migrate_req=1, or a BPF_SK_REUSEPORT_SELECT_OR_MIGRATE eBPF program that selects or migrates sockets).
During migration, the old request socket and its clone share the same ireq_opt, and tcp_v4_reqsk_destructor(), which destroys the clone, also used a plain kfree().

IPv6 is not affected by the SYN retransmit race, because tcp_v6_syn_recv_sock() copies the options before handing them to the child socket.
The migration issue does exist for IPv6’s ipv6_opt, but that option is only set by CALIPSO, the IPv6 security labeling mechanism, and the commit says it will be fixed in a separate patch.

The fix switches to an RCU-aware free

The fix is Eric Dumazet’s commit e3f33b0a1d89, which changes two lines in net/ipv4/af_inet.c and net/ipv4/tcp_ipv4.c.
It switches the frees in inet_sock_destruct() and tcp_v4_reqsk_destructor() from kfree() to kfree_rcu(), so the memory is freed only after RCU readers have left.

-	kfree(rcu_dereference_protected(inet->inet_opt, 1));
+	kfree_rcu(rcu_dereference_protected(inet->inet_opt, 1), rcu);

Other paths that set or replace socket options (do_ip_setsockopt(), cipso_v4_sock_setattr() and others) were already using kfree_rcu().
Only these two socket-teardown spots freed the memory immediately without waiting for RCU readers.

How the CVSS 9.8 score breaks down, and Red Hat’s lower score

The CVE record includes a CVSS 3.1 score of 9.8 (Critical) from the kernel CNA (kernel.org’s CVE Numbering Authority), along with a rationale for each metric.
The vector is AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. The main rationales are below.

MetricRationale in the CVE record
Attack Vector (AV:N)An unauthenticated peer only has to send a SYN with a Record Route or Timestamp option
Attack Complexity (AC:L)The attacker can repeat the option-bearing request, the retransmitted SYN, and the connection setup and teardown as often as needed
Privileges Required (PR:N)The bug is reached during the TCP handshake, before any application authentication
Confidentiality (C:H)After the freed object is reused, data is copied into the SYN-ACK for the stale optlen, sending adjacent kernel heap contents back to the peer
Integrity (I:H)ip_options_build() writes according to the length and offsets in the reused object, causing out-of-bounds writes
Availability (A:H)Depending on the length, the SYN-ACK can overrun its header room and stop the kernel through skb_under_panic()

Red Hat, on the other hand, scores it CVSS 8.1 (Important) and sets Attack Complexity to High (AC:H).
As of October 30, the Red Hat score is still a draft and gives no rationale for AC:H.
NVD lists the CVE as Received as of October 30, with no analysis published yet.

Anthropic’s exploit against an MPTCP listener

Anthropic’s October 27 research post, “Discovering and exploiting vulnerabilities in critical, real-world software with Claude”, covers this CVE as the last of five attack chains.
The post says the team used an unreleased internal research model, and that after initial prompting, Claude wrote the exploit.

The target is a Linux server listening with MPTCP.
According to the post, the attacker only sends packets and runs arbitrary commands as root, with no credentials and no user interaction.

KASLR (kernel address space layout randomization) changes the kernel’s load address on every boot, so an attacker doesn’t know where to jump.
The exploit first defeats KASLR, then hijacks control flow.

StageWhat it does
1. Defeat KASLRUse the race to make the kernel read an attacker-controlled length and send extra bytes back in the reply. If even one of those overreads contains a kernel pointer, the attacker can work out the kernel’s address layout
2. Hijack control flowUse the same bug to overflow a buffer and overwrite a callback pointer. Execution jumps to a payload already staged in kernel memory through an ordinary protocol feature, which chains calls to existing kernel functions and ends by running an attacker-chosen shell command as root

According to the post, losing the race only fails the handshake without crashing the system, so the attacker can keep retrying until it works. In the proof of concept, leaking a kernel pointer took 48 minutes, and reaching shell command execution took another five.
As the CVSS availability rationale above notes, though, some length values can stop the kernel, so a lost race is not guaranteed to fail safely. The Anthropic post also doesn’t explain why an MPTCP listener is required for the attack.

Go 1.24 enables MPTCP listeners by default

Since Go 1.24, Go uses MPTCP by default for listening sockets on Linux.
The Go documentation says the default for the multipathtcp setting became 2 (MPTCP enabled on listeners only) in 1.24.
A server built with Go 1.24 or later that calls net.Listen ends up listening with MPTCP unless told otherwise.

On October 29, issue #82717 (proposal: net: Disable MPTCP by default) was opened in the Go repository on GitHub, proposing to turn MPTCP off by default.
The same day, the Go security team sent an advisory to the golang-announce mailing list, which was also forwarded to oss-security.

The advisory calls upgrading to a patched kernel the best fix.
It then describes two ways to turn off MPTCP: set the environment variable GODEBUG=multipathtcp=0 at run time, or add godebug multipathtcp=0 to the main module’s go.mod at build time.
It also notes that while the demonstrated exploit needs an MPTCP listener, the underlying bug is not MPTCP-specific, so systems with no MPTCP listener may be open to similar attacks.

# Disable at run time
GODEBUG=multipathtcp=0 ./server
// Disable at build time (add to the main module's go.mod)
godebug multipathtcp=0

Per socket, net.ListenConfig.SetMultipathTCP and net.Dialer.SetMultipathTCP control MPTCP and override the GODEBUG setting.
The security team also calls disabling MPTCP program by program a fragile mitigation.
A single MPTCP listener left exposed keeps the precondition in place, and there may be attack paths that don’t go through MPTCP at all.
It considers changing Go’s own default premature for now and says discussion will continue on the issue.

On the issue, a member of the Go team also writes that the root cause is in IPv4 TCP handling.
Whether there is a viable path to root code execution on a system without an MPTCP listener is still unknown.

There is also a kernel setting that stops MPTCP sockets from being created at all.
The kernel documentation says MPTCP sockets can be created when net.mptcp.enabled is 1, and the default is 1.
Like the Go setting, though, this only removes the demonstrated attack’s precondition. The IPv4 TCP bug remains.

Fixed kernels and distro status

The CVE record lists the following fixed versions.
Mainline was fixed in 7.3, and the Debian tracker records the fix commit as landing in 7.3-rc7.

BranchFixed in
5.105.10.272
5.155.15.223
6.16.1.190
6.66.6.159
6.126.12.113
6.186.18.56
7.27.2.10
Mainline7.3

Distro status as of October 30, 2026:

DistributionStatus
Debian 12 (bookworm)Fixed in linux 6.1.190-1 (DLA-4841-1) and linux-6.12 6.12.113-1~deb12u1 (DLA-4850-1)
Debian 13 (trixie)Fixed in 6.12.113-1 (DSA-6568-1)
Debian forky (testing) and sid (unstable)Fixed
UbuntuNot confirmed on the security tracker (unconfirmed, not unaffected)
RHEL 8, 9, 10 and OpenShift 4Affected. No fixed packages listed yet
RHEL 6 and 7Not affected according to Red Hat’s CVE data

Installing a patched kernel package does nothing on its own. You have to reboot into the new kernel (live patching, which updates a kernel without a reboot, is a separate mechanism).
Check the running kernel with uname -r, and whether MPTCP sockets can be created with sysctl net.mptcp.enabled.

uname -r
sysctl net.mptcp.enabled

Distro kernels often backport fixes into older versions, so don’t compare the uname -r version directly with the table above. Check it against the package version listed in your distro’s security advisory.