CVE-2026-98432: a Linux TCP use-after-free in IP options
A server’s whole public surface is one listening TCP port. No login page, no upload form, no application code that has seen a single byte. Just the kernel finishing a handshake.
Someone sends it a SYN with a few extra bytes in the IP header. Then the same SYN again, a fraction too late. CVE-2026-98432 lives in that gap: a use-after-free in the Linux kernel’s IPv4 TCP connection setup, scored CVSS 9.8 by the kernel’s own CNA [1]. The short version is that the fix is a one-word change from kfree() to kfree_rcu(), and you need a patched kernel and a reboot [1][3][4].
Why now
The kernel CNA published the record on 15 October 2026 and updated it on 31 October [1]. The fix is older: Eric Dumazet wrote it on 1 October and it was committed on 5 October [8]. What changed last week is the context around it.
On 29 October the Go security team wrote to oss-security, pointing at an exploit Anthropic describes and warning that Go programs listen with MPTCP by default [4]. AlmaLinux put patched kernels in its testing repository on 30 October [3], and on 31 October someone proposed one more interim hardening option [5]. Red Hat rates it Critical at 9.8, and calls its own scores preliminary. Reliable code execution needs precise heap control, it says, but the chance of knocking systems over is what pushes it above Important [7].


Handing over a pointer
During a TCP handshake the kernel keeps a small request socket for each half-open connection. If the SYN carried IPv4 options, such as Record Route or Timestamp, they’re stored in that request socket’s ireq_opt [1]. When the handshake completes, tcp_v4_syn_recv_sock() creates the real child socket and hands it that pointer. Not a copy, the pointer. Ownership just moves [1].
Fine, as long as nobody is still looking at the old request socket. But a retransmitted SYN can land on another CPU. That CPU runs tcp_check_req() and builds a SYNACK, reading ireq_opt under rcu_read_lock() and nothing stronger. Then ip_build_and_send_pkt() and ip_options_build() read opt->optlen. Twice [1].
Since commit 079096f103fa, request sockets are processed without the listener lock, so nothing stops the child from being freed halfway through [1]. Child sockets don’t have SOCK_RCU_FREE, and inet_sock_destruct() frees inet_opt with a plain kfree() [1]. Result: a use-after-free in ip_options_build(). The record lists 4.4 as the first affected version [1].
It’s easy to miss in review, because each piece looks reasonable on its own. A toy model, not kernel source:
struct opts { int optlen; char data[40]; };
/* reader: holds rcu_read_lock() but no lock the freer takes */
void build_synack(struct opts *o) {
if (o->optlen > 40) return;
memcpy(pkt, o->data, o->optlen); /* optlen fetched again */
}
void destroy_buggy(struct opts *o) { kfree(o); } /* gone now */
void destroy_fixed(struct opts *o) { kfree_rcu(o, rcu); } /* gone after readers leave */RCU is a promise to readers: whatever you were looking at when you entered the read section won’t disappear until you leave it. The readers here were already using RCU. The free path just didn’t keep the promise. The fix makes it keep it, with kfree_rcu() in inet_sock_destruct() and in tcp_v4_reqsk_destructor(). Other places that replace inet_opt, like do_ip_setsockopt(), already did it this way [1][8].


Two more routes in
The record describes a second way to reach the same object. With request-socket migration (net.ipv4.tcp_migrate_req=1, or a BPF reuseport program that migrates), reqsk_timer_handler() clones the request socket, so the old one and the clone share ireq_opt. The old one is cleared, and a CPU still holding it can use the options when the clone is freed with a plain kfree() [1]. AlmaLinux notes that this setting is off by default [3].
IPv6 isn’t affected by the first issue, because tcp_v6_syn_recv_sock() duplicates the options. The IPv6 migration path has the same shape for ipv6_opt, which only CALIPSO sets, and the record says a separate patch will deal with it [1].
Not “only MPTCP”
The CNA’s scoring notes make the case for the full 9.8. An unauthenticated peer on the network supplies the option bytes. The handshake happens before any application authentication. Both sides of the race are attacker-induced. And reclaiming the freed object could leak neighbouring kernel heap into the SYNACK, turn a copy into an out-of-bounds write, or push past the packet headroom and panic the kernel [1]. That’s the CNA’s reasoning for the score, not a measured exploit.
The exploit that does exist is Anthropic’s. Its Frontier Red Team used an unreleased research model that wrote an exploit giving a remote attacker root on a Linux server with an MPTCP listener [2]. It works in two stages. First it uses the race to get the kernel to send back extra bytes containing a kernel pointer, which defeats KASLR. Then it uses the same bug to overwrite a callback pointer so that existing kernel functions run a chosen command. In the proof of concept the first stage took 48 minutes and the second five more. A lost race only fails the handshake; the machine doesn’t crash, so the attacker just tries again [2].
So don’t read “MPTCP listener” as “only MPTCP”. The Go security team says the demonstrated exploit needs one, but the bug itself isn’t specific to MPTCP, and systems without MPTCP listeners may fall to similar attacks [4]. AlmaLinux ships with MPTCP disabled and still tells its users to patch, because the flaw is in ordinary IPv4 TCP code [3].
And if you write Go: since Go 1.24, Go programs enable MPTCP by default for listen sockets on Linux [4]. You may be running an MPTCP listener without ever having chosen to.
Patching
Run uname -r on every host. Containers share the host kernel, so a clean image proves nothing; the node is what you patch.
Install a fixed kernel and reboot. Compare against your vendor’s fixed version rather than the upstream number, because distributions backport. Upstream stable fixes:
| Kernel line | Fixed in | Source |
|---|---|---|
| 5.10 | 5.10.272 | [1] |
| 5.15 | 5.15.223 | [1] |
| 6.1 | 6.1.190 | [1] |
| 6.6 | 6.6.159 | [1] |
| 6.12 | 6.12.113 | [1] |
| 6.18 | 6.18.56 | [1] |
| 7.2 | 7.2.10 | [1] |
Older stable lines such as 5.4, 4.19, 4.14 and 4.9 are inside the affected range, but the record names no fixed version for them [1]. If you’re on one of those, your vendor’s backport is the only way out.
Debian lists 6.1.190-1 as fixed in bookworm (DLA-4841-1), 6.12.113-1~deb12u1 for bookworm’s linux-6.12 package (DLA-4850-1) and 6.12.113-1 in trixie (DSA-6568-1), and notes that the mainline fix landed in 7.3-rc7 [6]. AlmaLinux’s patched builds were in the testing repository on 30 October, with production to follow after community testing. Installing from there means sudo dnf install -y almalinux-release-testing, then sudo dnf update 'kernel*' --enablerepo=almalinux-testing and a reboot. Once uname -r shows the right build, switch the testing repo off again with sudo dnf config-manager --disable almalinux-testing; a production box shouldn’t keep it [3]:
| Release | Patched kernel |
|---|---|
| AlmaLinux 8 | kernel-4.18.0-553.180.2.el8_10 |
| AlmaLinux 9 | kernel-5.14.0-687.61.2.el9_8 |
| AlmaLinux 10 | kernel-6.12.0-211.69.2.el10_2 |
Red Hat’s CVE page points to security bulletin RHSB-2026-016 [7]. When I read the bulletin, it listed no fixed kernel yet and called the investigation ongoing, so watch your subscription’s errata. It also reminds you that OpenShift, OpenStack, Virtualization and RHEL CoreOS run on the RHEL kernel, so their nodes need the update too [10].
Then find your MPTCP listeners and decide whether you need them. If you don’t, turn MPTCP off system-wide. Any host that can’t run a fixed kernel yet gets the stopgaps further down, with a name next to it for whoever removes them later.
Checking MPTCP
sysctl net.mptcp.enabled # 0 means off
sudo ss -lnMp # open MPTCP listeners
echo 'net.mptcp.enabled = 0' | sudo tee /etc/sysctl.d/90-disable-mptcp.conf
sudo sysctl -w net.mptcp.enabled=0The sysctl only stops new MPTCP sockets, so restart the services that own existing listeners [3]. One oss-security reply adds that newly started Go programs cope with this, while running ones may see errors on new connections [9].
For Go you can also set GODEBUG=multipathtcp=0 in the environment, or godebug multipathtcp=0 in go.mod. Programs can override both per socket through net.ListenConfig.SetMultipathTCP [4]. The Go team calls the per-program route fragile, since one forgotten listener leaves the host exposed, and it assumes every workable exploit needs MPTCP [4].
Until a fixed kernel is running
Three stopgaps turn up in the sources. None of them fixes the bug, and all three sources say so.
Drop TCP packets that carry IPv4 options. AlmaLinux publishes an nftables rule that drops inbound IPv4 TCP packets with options, meaning an IP header longer than five 32-bit words [3]:
sudo nft -f - <<'EOF'
add table ip cve_2026_98432
delete table ip cve_2026_98432
table ip cve_2026_98432 {
chain prerouting {
type filter hook prerouting priority -300; policy accept;
meta l4proto tcp ip hdrlength > 5 counter drop
}
}
EOF
sudo nft list table ip cve_2026_98432 # the counter shows what it droppedRed Hat expects ordinary traffic to pass, since standard TCP options live in the TCP header, not the IP header [10]. Read AlmaLinux’s caveats before you rely on it, though [3]. The rule doesn’t survive a reboot, so apply it again if you reboot before the patched kernel is in. Traffic between containers on the same host may never pass through it. On a host that routes, it also drops forwarded TCP packets with IPv4 options. Don’t use it on networks with CIPSO labels (NetLabel), because there the kernel adds those options itself. IPv6 doesn’t need it. And you need the nftables package installed.
Red Hat’s bulletin has its own version: an inet table on the input hook at priority -310, with ip protocol tcp ip hdrlength > 5 counter drop. It suggests saving it to a file and checking it with sudo nft --check --file cve-2026-98432.nft before loading it. Red Hat says it “may disrupt necessary workloads”, so try it outside production first [10]. The two rules differ in one way that matters. AlmaLinux’s prerouting hook also sees forwarded packets. Red Hat’s input hook only sees traffic addressed to the host itself.
Turn off MPTCP, with the sysctl shown above. It removes the setup Anthropic’s exploit used, but the ordinary IPv4 TCP path is still vulnerable [3][4].
Zero freed memory with init_on_free=1. On 31 October Rui Sousa proposed this kernel boot parameter on oss-security as an extra layer [5]. It makes the kernel zero slab objects and pages when they’re freed. The stale pointer still gets dereferenced, but a use-after-free read then sees zeroes instead of the old object’s contents, which “narrows what an attacker can learn through it” [5]:
sudo grubby --update-kernel=ALL --args="init_on_free=1"
sudo reboot
grep -o init_on_free=1 /proc/cmdlineYes, that needs a reboot. It’s for a host that can reboot but has no fixed kernel to boot into yet, because its vendor hasn’t shipped one. According to the post, the AlmaLinux 9 and 10 kernels it checked have this off by default. The post puts the cost at roughly 1 to 5% on workloads that free a lot of memory, though it cites no source for that figure, so measure your own workload. It also points out that this helps where the packet filter doesn’t, such as traffic between containers on the same host. Keep the weight of the evidence in mind: it’s one person’s post, not vendor guidance, and the only track record it reports is “a handful” of hosts running it next to the nftables rule “with no problems so far” [5].
There’s also something none of the sources suggest, but that follows from the record. The second route to the bug only exists when net.ipv4.tcp_migrate_req is 1, or a BPF reuseport program migrates requests [1]. It’s off by default [3], so check that sysctl net.ipv4.tcp_migrate_req prints 0 and leave it that way until you’ve patched.
Taking the stopgaps out
Write down where you applied each one. After the host is running a fixed kernel:
sudo nft delete table ip cve_2026_98432 # AlmaLinux rule
sudo nft delete table inet cve_2026_98432 # Red Hat rule
sudo grubby --update-kernel=ALL --remove-args="init_on_free=1"If you want MPTCP back, delete /etc/sysctl.d/90-disable-mptcp.conf and set net.mptcp.enabled to 1 again. And if you didn’t need MPTCP in the first place, you might as well leave it off.
Share this article
Sources
- CVE.org: CVE-2026-98432 record (Linux kernel CNA)12345678910111213141516171819202122
- Anthropic: discovering vulnerabilities with Claude12
- AlmaLinux: CVE-2026-98432 fixes and interim mitigations12345678910
- oss-security: Protecting against CVE-2026-98432 when using Go programs1234567
- oss-security: init_on_free=1 as an additional interim mitigation1234
- Debian security tracker: CVE-2026-984321
- Red Hat: CVE-2026-9843212
- Linux kernel: ipv4: free inet_opt and ireq_opt after an RCU grace period12
- oss-security: Re: Protecting against CVE-2026-98432 when using Go programs1
- Red Hat: RHSB-2026-016, kernel IPv4 options use-after-free123