Linux Kernel CVE-2026-98432: Two Sockets Share IP Options, and One Frees Them Too Early
When a Linux server accepts a TCP connection, the kernel first creates a small request socket and stores the IP options from the client's SYN there. Once the handshake completes, the new connection socket takes over these options, and since Linux 4.4 it does so as a bare pointer to the same memory. With CVE-2026-98432 that becomes the problem, because a retransmitted SYN on another CPU core can still build a reply from those options while the new socket has already freed them. The bug sits in the ordinary IPv4 path and is reachable over the network without authentication. Anthropic says it built a chain from it that runs commands as root on servers with an MPTCP listener, and the fix therefore moves the free behind an RCU grace period.
TL;DR — 90 seconds
- What: A use-after-free in the IPv4 TCP handshake. The request socket and the connection socket share the stored IP options, and a SYNACK built in parallel can read them after they have been freed.
- Severity: The kernel CNA assigns CVSS 9.8 (
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H), Amazon arrives at 8.1 because of high attack complexity, and Red Hat lists a provisional 5.9. Anthropic reports a working remote root exploit against MPTCP listeners but has not published it. - Who is affected: Every kernel from 4.4 onward without the fix that accepts IPv4 TCP connections from untrusted networks itself. Hosts with MPTCP listeners are particularly exposed, and Go services from Go 1.24 onward open those on their own.
- What to do: Install a patched kernel and reboot. For Amazon Linux 2 and Amazon Linux 2023, every kernel package has been fixed since October 30, and Debian ships 6.1.190 and 6.12.113.
- What would have protected you: Dropping TCP packets with IP options before they reach the stack, disabling MPTCP where nobody needs it, and accepting connections from the internet on a few proxy nodes only. seccomp, capabilities and Pod Security do not help against this mechanism.
Shared IP options in the TCP handshake
An IPv4 header is normally 20 bytes long. The IHL field gives its length in 32-bit words, the minimum value of 5 corresponds to exactly those 20 bytes, and anything beyond that is IP options as defined in RFC 791. Two of them matter here: Record Route, where routers write their address into the packet, and Timestamp, which also collects timestamps. Such options are rare on today's internet. The Linux kernel still supports them, however, and when a SYN carries them, it stores them so it can return them appropriately in its reply.
The kernel handles the handshake in two stages. When a SYN hits a listening socket, it creates a lightweight request socket, stores the options in its ireq_opt field and sends the SYNACK. Since commit 079096f103fa in Linux 4.4, these request sockets live directly in the central connection hash table, so the kernel can match incoming packets without going through the listener and process them on several CPUs in parallel. When the final ACK arrives, tcp_v4_syn_recv_sock() creates the actual connection socket, called the child in kernel terms.
This is exactly where the bug was. The child takes over the request socket's options into its inet_opt field without copying them, and afterwards both point to the same memory. If a retransmission of the original SYN arrives at the same time, another CPU core can find the request socket that still exists and build another SYNACK, during which ip_options_build() reads the stored options. If the child is torn down at that moment, inet_sock_destruct() frees the options with kfree(), and the SYNACK path reads memory that the allocator may long since have handed to another object. A use-after-free that an attacker can trigger remotely as often as they like is a classic starting point for memory corruption.
A second, narrower path runs through request socket migration. If net.ipv4.tcp_migrate_req is set to 1 or a BPF program of type BPF_SK_REUSEPORT_SELECT_OR_MIGRATE is attached to the listener, the kernel hands half-open connections to another listener in the same reuseport group, and the clone shares the options pointer with the old request socket. Both are off by default. IPv6 is not affected by the main path because tcp_v6_syn_recv_sock() copies the options. According to the commit message, the kernel team will fix the IPv6 migration case separately.
Why the design favours this bug becomes clear with a look at RCU. The kernel reads many structures in the network path without a lock, protected only by Read-Copy-Update. A reader marks a short critical section, and whoever replaces an object frees the old one only after all readers that could still see it have left that section. According to the commit message, the other places that replace inet_opt already follow this rule. Only the final free during socket teardown did not, and a lock-free reading path next to an immediately freeing path is exactly the combination RCU is meant to prevent.
What the fix commit changes
The fix commit e3f33b0a1d89 by Eric Dumazet, reported by Xinyang Ge and included upstream in Linux 7.3-rc7, is remarkably small. In inet_sock_destruct() in net/ipv4/af_inet.c and in tcp_v4_reqsk_destructor() in net/ipv4/tcp_ipv4.c, it replaces the direct kfree() with kfree_rcu(). The memory holding the options is therefore only returned after an RCU grace period, when no reader that saw the pointer can still be inside its critical section. The handshake logic does not change, and the pointer is still shared.
That says a fair amount about the root cause. It was never about wrongly parsed options or a missing length check, because the code read the right data, just too late. The two Fixes tags point to optimisations that moved request sockets into the hash table and made their migration possible. Both made the handshake parallel, and the lifetime of the options was not thought through along with them. That the bug went unnoticed for about ten years fits this picture, because a lost race leaves no traces in normal operation.
From SYN packet to root shell
For the main path, an attacker needs no special configuration on the target. A reachable IPv4 TCP listener and the ability to send SYN packets with IP options are enough, which is where the kernel CNA's score of 9.8 comes from. Amazon rates the attack complexity as high and arrives at 8.1, which I consider the more realistic assessment, because the attacker has to hit a narrow window between two CPU cores. Red Hat provisionally rates only availability, meaning a crash, and lands at 5.9.
How the race turns into more is described only in broad strokes in Anthropic's report of October 27. The chain has two stages. First, the attacker wins the race in a way that makes the kernel read too much and reveal a kernel pointer in a reply, which defeats KASLR, the randomised placement of the kernel in memory. Then they overwrite a callback pointer and redirect execution to a chain of existing kernel functions that starts a command as root. In the test, the leak took about 48 minutes and execution another five. According to Anthropic, a lost race only makes the handshake fail without crashing the system, so the attacker can repeat it as often as they like.
The chain shown targets servers with a Multipath TCP listener. Anthropic does not explain why MPTCP is needed for it, and it has not published code, offsets or the kernel version tested. The bug itself sits in the ordinary IPv4 path. TuxCare quotes the Go Security Team as saying that systems without an MPTCP listener may also be vulnerable to similar attacks. Disabling MPTCP therefore lowers the risk of the known chain but does not close the hole.
Whether a server has an MPTCP listener out of the box is decided in two places. The kernel allows MPTCP sockets when net.mptcp.enabled is 1, which according to the kernel documentation is the default, while the RHEL family sets the value to 0. On top of that, an application has to open an MPTCP socket explicitly, and that is exactly what Go programs have done on their own since Go 1.24. ListenConfig uses MPTCP on Linux by default, controlled by the GODEBUG setting multipathtcp, whose default value of 2 enables MPTCP for listeners. Traefik, Caddy and therefore FrankenPHP, CrowdSec, k3s and many Kubernetes components are written in Go. Whether a specific service adopts the default depends on its Go version and its code, so I would check on your own host rather than assume it.
Kubernetes adds a detail that is easy to miss. net.mptcp.enabled applies per network namespace, and the kernel sets the value to 1 for every new namespace in net/mptcp/ctrl.c, regardless of the value on the host. A pod with its own network namespace can therefore open MPTCP listeners even if the host has the switch set to 0. Nodes with many container workloads are thus, if anything, more exposed than a single server.
The machines really affected are those whose kernel accepts TCP connections from untrusted networks itself: ingress and edge nodes, hosts with a public IP, bastion hosts with an open SSH port and databases reachable from outside. A backend behind a reverse proxy that terminates TCP never sees the attacker's SYN, because the proxy builds its own connection and does not carry over foreign IP options. That is my own conclusion from the mechanism. If you run a kernel from the affected list but only accept internal connections, your risk is much smaller as long as nobody is already inside the internal network. On nodes with several tenants this reassurance does not apply, though, because foreign pods share the kernel there.
| CVE | Affected | Fix | Prerequisite | CVSS |
|---|---|---|---|---|
| CVE-2026-98432 | Linux kernel 4.4 and later without the fix | 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10, 7.3 (commit e3f33b0a1d89) | Reachable IPv4 TCP listener, an MPTCP listener for the chain shown | 9.8 kernel CNA (critical), 8.1 Amazon, 5.9 Red Hat (provisional) |
As of October 31, none of the sources checked lists a public exploit or an independent reproduction. Attacks in the wild are not known, and the vulnerability is not in the CISA KEV catalog. EPSS estimated the probability of exploitation in the next 30 days at 0.45 percent on October 30. In my view this figure understates the situation, because a vendor has already described the path to a root shell and kernel patches are easy to diff.
Patched kernels and how to get there
The only complete fix is a kernel with commit e3f33b0a1d89 or its backport. Upstream it is included in the stable lines 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56 and 7.2.10, as well as in Linux 7.3. It brings no change in behaviour, because the fix only delays freeing a few bytes by one grace period. It only takes effect after a reboot, though, and in practice that is where it most often fails.
For Amazon Linux, the Security Center lists every kernel package as fixed as of October 30:
- Amazon Linux 2023,
kernel6.18: ALAS2023-2026-3251 of October 24 - Amazon Linux 2023,
kernel6.12: ALAS2023-2026-3335 of October 29 - Amazon Linux 2023,
kernel: ALAS2023-2026-3365 of October 30 - Amazon Linux 2,
kernel: ALAS2-2026-4065 of October 30 - Amazon Linux 2, Kernel-5.4, Kernel-5.10 and Kernel-5.15 from Extras: ALAS2KERNEL-5.4-2026-135, ALAS2KERNEL-5.10-2026-137 and ALAS2KERNEL-5.15-2026-120 of October 30
Amazon Linux 2023 has a pitfall. According to the AWS documentation, its repositories are locked to the version the AMI was built with, so a plain update does not necessarily fetch the new kernel. sudo dnf check-release-update shows which release versions are available, and sudo dnf upgrade --releasever=<version> moves you to one of them. If you rebuild your instances from images anyway, swap the AMI instead.
Debian shipped the fixes via DLA-4841-1 as 6.1.190-1 for Bookworm, via DSA-6568-1 as 6.12.113-1 for Trixie and via DLA-4850-1 as 6.12.113-1~deb12u1 in the linux-6.12 package for Bookworm. Container images based on Wolfi, Alpine or Debian contain no kernel, because every container uses its host's kernel. A rebuilt image therefore changes nothing about this vulnerability. Kata Containers are different, as they run with their own guest kernel, which then has to be updated as well.
In Kubernetes clusters this means draining, updating and rebooting nodes one after another, or replacing them right away with nodes from a fresh image. Afterwards, check with uname -r that the new kernel is really running, because an installed package without a reboot does not protect you.
Hardening that works beyond the patch
First, some context. The attack needs neither code in the container nor an unusual system call. It arrives from outside as a network packet and hits the kernel before any process is involved. What works, therefore, is anything that keeps a SYN with IP options from reaching the TCP stack, reduces the number of kernels that accept foreign connections, or switches off the code path of the chain shown. Measures that only restrict the container miss the target here.
Dropping TCP packets with IP options before the stack
The bug requires a SYN to carry IP options, because only then does the request socket have anything in ireq_opt at all. A packet filter that drops IPv4 TCP packets with a header longer than 20 bytes therefore removes the basis of the attack. AlmaLinux publishes an nftables rule for this that, in its own table in the prerouting chain at priority -300, counts and drops every packet matching meta l4proto tcp ip hdrlength > 5. The priority makes sure the rule applies before connection tracking and the remaining filters see the packet.
You should know the limits. According to AlmaLinux the rule does not survive a reboot, so if you want to keep it, you have to move it into your own firewall configuration. It breaks networks that depend on CIPSO or NetLabel labelling, because those labels travel as an IP option. And traffic between containers on the same host can bypass it. Before dropping, I would first load the rule with only counter and without drop and watch for a while whether legitimate traffic uses IP options at all. That is my own recommendation and not something AlmaLinux prescribes. You do not need to filter IPv6 for the main path.
Disabling MPTCP where nobody needs it
The chain shown needs an MPTCP listener. With net.mptcp.enabled = 0 in a file under /etc/sysctl.d/ and sysctl -w net.mptcp.enabled=0, the host no longer allows new MPTCP sockets. Existing listeners keep running until you restart the affected services, though, and ss -lnMp shows which ones those are. Because the switch applies per network namespace and every new namespace starts at 1, the host value does not reach your pods. For Go services, GODEBUG=multipathtcp=0 in the container's environment is therefore the more direct lever, because it makes the service behave as it did before Go 1.24.
Setting the value inside the pod itself is more awkward in Kubernetes. net.mptcp.enabled is not one of the safe sysctls that a pod may set via securityContext.sysctls without further ado. The kubelet has to allow it on every node via --allowed-unsafe-sysctls, and pods with hostNetwork: true cannot set net.* sysctls at all. I therefore consider the environment variable the more practical route. Both measures only shrink the attack surface of the known chain, they do not close the hole in the IPv4 path.
Accepting foreign connections in only a few places
The fewer kernels accept TCP connections from the internet themselves, the smaller the set of machines that have to be patched immediately for a vulnerability like this. A reverse proxy or load balancer that terminates TCP accepts the attacker's SYN and builds its own connection to the backend, so the backends never see foreign IP options. To manage EC2 instances, AWS Systems Manager Session Manager needs no open inbound port, which removes a publicly reachable SSH port. In Kubernetes, NodePort, hostPort and hostNetwork deserve scrutiny, and NetworkPolicies with ingress rules limit who can reach a pod at all.
This measure shifts the risk rather than removing it. The proxy nodes still accept foreign SYNs and have to be patched first, and the packet filter from the first section is worthwhile on them too. A load balancer that merely forwards packets at layer 4 terminates no connection, and then the backend does see the SYN after all. AWS does not document whether a Network Load Balancer passes IP options through to its targets. I would therefore treat targets behind such a load balancer as if they accepted the connections themselves. NetworkPolicies filter by addresses and ports, not by IP options, so they only help if the attacker cannot get through in the first place.
Separating tenant workloads from the host kernel
When several tenants share a node, the same kernel handles the handshakes of all pods. gVisor brings its own network stack, netstack, which according to its documentation is handled entirely inside the Sentry and is thus isolated from the host's network stack. A listener in a gVisor pod therefore does not go through the host kernel's TCP handshake, which I derive from this architecture. In passthrough mode with --network=host this protection disappears. Kata Containers isolate via a separate virtual machine with a guest kernel, which has to be patched for this vulnerability just the same. In Kubernetes you choose either via a RuntimeClass. The price is performance and compatibility, so this route pays off mainly for foreign or less trusted code.
Kernel updates as routine rather than a project
This vulnerability shows that a kernel patch only takes effect with the reboot. Tools like kured, the Kubernetes Reboot Daemon, drain nodes in an orderly way after an update and reboot them one by one. If you build your nodes from images, you roll out new AMIs or node images instead and let old nodes expire. What matters is that this process runs regularly, so that it is no longer a special operation when a critical vulnerability hits.
What does not help here
Many proven hardening measures remain correct but are ineffective against this mechanism. seccomp with RuntimeDefault or a custom profile filters system calls made by processes, and here nobody calls anything, the packet lands directly in the network stack. Dropped capabilities, runAsNonRoot, allowPrivilegeEscalation: false, readOnlyRootFilesystem and the Pod Security Standard “restricted” limit what a process inside the container may do. The attacker does not need a process at all, though, and once the chain has run, the kernel executes their command outside any container.
User namespaces do not change this picture, because they map user IDs but do not swap the kernel. A minimal base image without a shell does not protect you, because the attacker's command runs on the host and not in the image. A CVE gate for container images will not fire either, because the vulnerable code lives in the node's kernel and appears in no image. A scanner that only checks images therefore shows green while the node is open.
You will find more on network rules, node hardening and update processes in the series TYPO3 on Kubernetes under IT-Grundschutz.
Detection and verification
The first indication of vulnerability comes from uname -r on every host and every node, compared against your distribution's fixed versions. On Amazon Linux, rpm -q kernel or rpm -q kernel6.12 or kernel6.18 gives the installed version, but what counts is the running kernel. After that, look at exposure. sysctl net.mptcp.enabled shows the value of the host namespace, ss -lnMp the MPTCP listeners. Because pods have their own network namespaces, you have to run ss inside each namespace, for example via nsenter with the PID of a container process. Also check that net.ipv4.tcp_migrate_req is 0 and whether any reuseport-type BPF programs are loaded that would open the second path.
The harder question is whether someone has already tried. Neither the vendors nor Anthropic name any indicators. From the mechanism I derive two signals. An attacker has to send many SYNs with IP options, and going by Anthropic's timings, that takes closer to an hour than a few seconds. The counter of the nftables rule from the hardening section makes exactly this visible, even in pure counting mode. In addition, a failed attempt can crash the kernel, which Amazon names as the most likely consequence. Kernel oopses or crashes with ip_options_build or inet_sock_destruct in the stack trace therefore deserve a close look.
If you suspect a compromise, the usual rule for kernel compromises applies. Anyone who could run code as root in the kernel can cover their tracks and persist, so cleaning up the running system is not trustworthy. Take the host off the network or drain and cordon the node, secure a memory image and logs if you need them for forensics, and rebuild the machine from a patched image. Afterwards, rotate everything that was on the host or reachable from it: kubelet credentials, the service account tokens of the pods on that node and the instance role sessions.
Recommendation for operators
Act now on every machine whose kernel itself accepts TCP connections from the internet or from foreign tenants: ingress nodes, proxies, hosts with a public IP and Kubernetes nodes with several tenants. Install the patched kernel there and reboot. Until that is done, load the packet filter for IP options and set GODEBUG=multipathtcp=0 for Go services, unless you use MPTCP deliberately.
Monitoring is enough if a host only accepts connections from a trusted internal network, such as a backend behind a terminating proxy or a database without access from outside. The update still belongs in the next regular maintenance window, because an attacker who is already inside the internal network finds a convenient path to root here.
In the end, what I say about every vulnerability applies: patches and updates belong in automation, and for kernel vulnerabilities that also means automating the reboot. When Renovate or Dependabot propose new AMI versions, node images and Helm charts as merge requests and the pipeline tests them against staging, a kernel fix like this one is a matter of hours rather than weeks. A cooldown period for routine updates protects against compromised releases, and you exempt security updates from it. If a new kernel breaks drivers or workloads, that shows up in staging and not on the production ingress node. If you update kernels by hand, on the other hand, you can easily end up with four weeks of open nodes during the holiday season, followed by a bulk update in which the kernel, container runtime and charts all change at once.
Frequently asked questions about CVE-2026-98432
Is disabling MPTCP enough?+
No. It slows down the known chain, but the vulnerability sits in the ordinary IPv4 path. Only a patched kernel after a reboot closes it.
Do I have to rebuild my container images?+
No. Containers use the host's kernel, so your nodes' kernel is what counts. The exception is Kata Containers, whose guest kernel you have to update as well.
Am I safe behind a load balancer?+
If it terminates TCP, your backend never sees the attacker's SYN. The load balancer itself then has to be patched, though. A device that only forwards packets does not protect you.
Why do the vendors rate it so differently?+
The kernel CNA assumes low attack complexity and arrives at 9.8. Amazon treats the narrow time window as high complexity and lands at 8.1. Red Hat provisionally rates only a crash.
Conclusion
CVE-2026-98432 is neither a parser bug nor a forgotten length check. The kernel read the right data, but at a point when it no longer owned it, because two objects shared one memory region and only one of them took RCU seriously. Bugs like this appear when code is parallelised and the lifetime of shared data does not move along with it. For operators, the transferable lesson lies elsewhere. A server's attack surface depends on defaults that change without your involvement, as here with Go 1.24, which quietly switched on MPTCP listeners. If you only count open ports and do not check which protocols the kernel accepts behind them, you underestimate your exposure. An inventory of listeners including their protocol and a packet filter that does not let the unusual through in the first place are therefore worth more than any single patch round.
Sources
- Debian Security Tracker: CVE-2026-98432
- kernel.org: ChangeLog 6.18.56 with the backport of e3f33b0a1d89
- OpenCVE: CVE-2026-98432 (CVSS from the kernel CNA and Red Hat)
- Amazon Linux Security Center: CVE-2026-98432
- Anthropic: Discovering vulnerabilities with Claude
- AlmaLinux: CVE-2026-98432, Patched kernels available in testing
- TuxCare: CVE-2026-98432 Linux Kernel TCP Flaw
- Red Hat Bugzilla 2553390
- Kernel documentation: MPTCP Sysfs variables
- Linux source: net/mptcp/ctrl.c
- Kernel documentation: What is RCU?
- RFC 791: Internet Protocol
- Go: GODEBUG settings (multipathtcp)
- Go 1.24 Release Notes
- Kubernetes Docs: Using sysctls in a Kubernetes Cluster
- AWS: Deterministic upgrades in Amazon Linux 2023
- gVisor: Networking
- FIRST EPSS: CVE-2026-98432
I check your Linux hosts and Kubernetes nodes for unpatched kernels and open listeners, put packet filters and MPTCP settings in place and automate the kernel rollout.
Kernel and node audits, an inventory of listeners including their protocol, nftables rules for edge and ingress nodes, automated reboots and image rotation with tested pipelines.
Platform operations instead of consulting on paper: I continuously check, patch and harden your infrastructure.
