CVE-2026-98432: A Two-Statement Kernel Fix Scored 9.8, 8.1 and High by Five Vendors

Two kfree() calls became kfree_rcu(). The scores never converged: CNA 9.8, Oracle and Amazon 8.1, SUSE 8.1/9.2, Ubuntu High. Trackers read 1 Nov.

On 15 October 2026 the Linux kernel’s own CVE Numbering Authority published CVE-2026-98432, “ipv4: free inet_opt and ireq_opt after an RCU grace period”, and scored it 9.8, Critical, vector AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. By the end of October the same bug was rated 8.1 by Oracle and Amazon, Important by SUSE, High by Ubuntu — whose own page prints “9.8 · Critical” two lines above its priority — and 9.8 Critical again by Red Hat, which had been sitting at 8.1 until it changed its mind on 30 October.

The patch itself is two statements. Eric Dumazet’s commit, authored 1 October and merged to stable on 15 October by Greg Kroah-Hartman, touches two files with +2 −2:

/* net/ipv4/af_inet.c — inet_sock_destruct() */
-	kfree(rcu_dereference_protected(inet->inet_opt, 1));
+	kfree_rcu(rcu_dereference_protected(inet->inet_opt, 1), rcu);

/* net/ipv4/tcp_ipv4.c — tcp_v4_reqsk_destructor() */
-	kfree(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1));
+	kfree_rcu(rcu_dereference_protected(inet_rsk(req)->ireq_opt, 1), rcu);

Two statements, four changed lines. That gap — between how small the fix is and how large the argument over its severity has become — is the useful thing to understand. If you operate Linux hosts, the score you were handed is not a measurement of your exposure. It is one metric, chosen differently, by five organisations with five different incentives.

What actually races

tcp_v4_syn_recv_sock() takes ownership of ireq->ireq_opt and hands it to the child socket as newinet->inet_opt without copying it. Meanwhile another CPU can process a retransmitted SYN for the same request socket and build a SYNACK from tcp_check_req(). The commit spells out the consequence:

tcp_v4_send_synack() and inet_csk_route_req() read ireq->ireq_opt under rcu_read_lock() only, and ip_build_and_send_pkt() and ip_options_build() then read opt->optlen twice.

The readers are already RCU-correct. The destructors are not: inet_sock_destruct() frees inet_opt with a plain kfree(), and TCP child sockets do not carry SOCK_RCU_FREE. So the object can be returned to the allocator while a second CPU is still inside ip_options_build() reading opt->optlen and copying opt->__data into the outgoing SYNACK.

The second path is request-socket migration — net.ipv4.tcp_migrate_req=1, or a BPF_SK_REUSEPORT_SELECT_OR_MIGRATE program — where inet_reqsk_clone() gives the old request socket and its clone the same ireq_opt, and tcp_v4_reqsk_destructor() also uses a plain kfree().

The trigger is an IP option, not a TCP option

This is the detail most summary posts blur. The stale object only exists if the connecting peer’s IPv4 header carried options — the CNA’s scoring notes name them exactly: “IPv4 Record Route or Timestamp bytes in a TCP SYN”, stored by ip_rcv_options() and tcp_v4_save_options().

TCP options are a different thing entirely, carried in the TCP header after the fixed 20 bytes. SYN cookies, timestamps, SACK and MSS all live there and are irrelevant here. What arms the bug is the IP header’s ihl field being greater than 5. Red Hat’s own mitigation states the same distinction, in the sentence that matters most for firewall writers: “standard TCP options are carried in the TCP header and continue to be permitted.”

Why 4.4 is the floor

The fix carries two Fixes: tags, and their dates explain the affected range:

  • 079096f103fa — “tcp/dccp: install syn_recv requests into ehash table”, 2 October 2015, by Eric Dumazet. Request sockets began being processed without the listener lock, which is precisely the lock that used to prevent this race.
  • c905dee62232 — “tcp: Migrate TCP_NEW_SYN_RECV requests at retransmitting SYN+ACKs”, 12 June 2021, by Kuniyuki Iwashima. This is the migration variant.

So the bug has been latent for eleven years on the first path and five on the second, and the CNA’s machine-readable range says affected from 4.4 upward — which is why Ubuntu 14.04, whose kernel is 3.13, reads “Not affected” on the tracker. That single fact is also the correction to the most repeated sentence in this disclosure; more on that below.

Five vendors, four scores

Every figure below was read from the vendor’s own record. Only one metric moves.

SourceRatingCVSS v3.1 baseAttack ComplexityOtherRecord notes
Linux kernel CNA (cve.org)Critical9.8Low—No CWE listed; published 15 Oct 12:08 Z
Red HatCritical9.8Low—Raised from 8.1 on 30 Oct; cvss3.status: draft; CWE-825
SUSEImportant8.1 (SUSE) / 9.8 (CNA columns)HighCVSS 4.0 9.2 AT:PState “Pending”; three v3 columns side by side
Oracle LinuxImportant8.1High—“preliminary and subject to review”
Amazon LinuxImportant8.1High—“primarily results in a system crash… or potential memory corruption”
UbuntuHigh9.8LowCVSS 4.0 8.3 VC:L/VI:L/VA:HPriority High, page prints “9.8 · Critical”
NVD————Status “Received”, no analysis of its own

Read the vectors side by side and the argument collapses to one field:

CNA / Red Hat / Ubuntu   CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H   9.8
SUSE / Oracle / Amazon   CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H   8.1
                                          ^^^^

Everything else is identical: network-reachable, no privileges, no interaction, same scope, and high impact on confidentiality, integrity and availability. The entire 1.7-point spread is AC:L versus AC:H.

That is not an accident of scoring. It is a genuine disagreement about what a race condition is, and the CNA wrote its reasoning into the record rather than leaving it implicit:

AC:L - On an accepting server, the peer can repeatedly create an option-bearing request and race a retransmitted SYN against completing and tearing down that same TCP connection. Both racing sides are attacker-induced; multicore scheduling and heap grooming do not constitute conditions outside the attacker’s influence.

Oracle and Amazon went the other way, and CVSS 4.0 has a metric that makes their position explicit. SUSE’s record keeps both: 9.8 under v3’s AC:H-or-AC:L framing, and 9.2 under v4 with AT:P — Attack Requirements Present. CVSS 4.0 split the old “attack complexity” idea into two things: AC (the attacker’s own work) and AT (conditions outside the attacker’s control). Vendors who score this AC:H are, in effect, saying the race requires luck. CVSS 4.0 lets them say that without also inflating every impact metric.

Ubuntu’s v4 entry goes further: VC:L/VI:L/VA:H — low confidentiality and integrity impact, high availability impact. That is a defensible reading of the armchair version of this bug (it crashes the box) and a hard one to square with Anthropic’s published description of a root shell. Which brings up the thing that decides the argument for everyone: the exploit exists, and nobody outside Anthropic has it.

Anthropic’s account of what its model produced is specific about time:

In our proof of concept, it takes 48 minutes to leak a kernel pointer and another five minutes to reach the shell command execution.

53 minutes, per attempt chain, against a target it says needs “a multipath TCP listener”. The enabling property Anthropic names is the one that pushes toward AC:L: “a lost race only fails the handshake without crashing the whole system, so a determined attacker can retry indefinitely until they succeed.” A defender reading 53 minutes sees a campaign. A CVSS calculator reading “retry forever, no crash” sees AC:L. Both are correct; they are measuring different things. If your threat model is opportunistic scanning, 53 minutes per host is expensive. If it is a targeted actor who picked you, the clock does not matter at all — and neither does the 9.8-versus-8.1 argument.

Patch status, checked on 1 November

Upstream is done: 7.2.10 and 6.18.56 on 11 October; 6.12.113, 6.6.159, 6.1.190, 5.15.223 and 5.10.272 on 15 October; 7.3 on 18 October — eight stable commits, eight release lines.

Distribution kernels carry their own numbers, so the upstream tag tells you nothing about your host. Below is what the trackers said at 2026-11-01 00:50 UTC, read directly from each vendor:

DistributionStatus at 00:50 UTC, 1 NovFixed build
Ubuntu 26.10 / 26.04 / 24.04 / 22.04Vulnerable — work in progress7.3.0-10.10 / 7.0.0-43.43 / 6.8.0-149.149 / 5.15.0-203.213
Ubuntu 20.04 / 18.04 / 16.04Vulnerablenone listed
Ubuntu 14.04 (linux package)Not affected— (3.13 predates 4.4)
RHEL 8 / 9 / 10Affected, no fix—
RHEL 6 / 7Not affected—
Debian 13 trixie6.12.107-1 vulnerable, 6.12.113-1 fixed in securityDSA-6568-1
Debian 12 bookworm6.1.176-1 vulnerable, 6.1.190-1 fixed in securityDLA-4841-1
Debian forky / sid7.2.12-1 / 7.2.14-1 fixed—
Amazon Linux 2 and 2023Fixed across all kernels (5.4/5.10/5.15 extras, 6.12, 6.18)advisories dated 24–30 Oct
Oracle Linux UEK 7/8/9/10FixedELSA-2026-500397/8/9, 16 Oct
SUSE SLES 16.0, 15 SP4–SP6 LTSSFixedkernel-default 6.12.0-160000.40.1
SUSE 15 SP7In progress—
AlmaLinux 8/9/10Fixed kernels in testing4.18.0-553.180.2.el8_10, 5.14.0-687.61.2.el9_8, 6.12.0-211.69.2.el10_2

Two things worth noticing. First, the two distributions with the largest installed base — Ubuntu and RHEL — are the two without a shipped fix, seventeen days after the stable backports went out. Second, “work in progress in 6.8.0-149.149” is a version number, not a fix: it becomes real when Canonical publishes it and you reboot.

Digging through the thread under the disclosure, a Proxmox employee reported their 7.0.14-25 kernel carrying the fix and being available on all repositories. That is a claim from a comment, not from a vendor tracker, and I have not verified it — but it is the kind of fact missing from every roundup published so far.

The Go default that widened it

The bug is in IPv4 TCP, not MPTCP. But Anthropic’s working exploit targeted “Linux servers with a multipath TCP listener”, and the Go security team’s 29 October notice added the part that turned a curiosity into an operations problem:

As of Go 1.24, Go programs enable MPTCP by default for listen sockets on Linux. This default may be changed by setting the “multipathtcp” GODEBUG: Setting “GODEBUG=multipathtcp=0” in a program’s environment will disable MPTCP by default at runtime. Placing “godebug multipathtcp=0” in the main module’s go.mod file will disable MPTCP by default at build time.

Note the granularity of that default: it follows the go line in go.mod. A service built with a 1.24+ toolchain opens MPTCP listeners without anyone deciding to. net.ListenConfig.SetMultipathTCP overrides the GODEBUG, in both directions.

The same notice is candid about why not to treat that as a fix:

Note that disabling MPTCP on a program-by-program basis is a fragile mitigation. Accidentally leaving a single MPTCP listener exposed will still leave a system vulnerable. Further, it assumes that all viable exploits for this bug depend on MPTCP. Again, we strongly recommend upgrading to a patched kernel version.

And on changing Go itself: “At this time, we believe it is premature to consider making any changes to Go (such as changing the default MPTCP behavior) in reaction to this issue.”

What we measured on the host that wrote this

Every number above came from a vendor API or tracker. The host this article was written on is a Debian 13 userland running an Ubuntu-built kernel, and it is a useful example of how thin a version string is:

$ uname -r
6.8.0-147-generic                  # below Ubuntu's WIP 6.8.0-149.149
$ cat /proc/sys/net/mptcp/enabled
1                                  # MPTCP compiled in and permitted
$ cat /proc/sys/net/ipv4/tcp_migrate_req
0                                  # the 2021 migration path is off
$ ss -Mlnp
ss: command not found              # iproute2 isn't installed

Three of four checks answered the question; the fourth failed because the container image has no iproute2. That failure is itself the practical lesson: the advertised one-liner (ss -Mlnp lists only MPTCP sockets, so empty output means no MPTCP listener) does not run on a minimal image, and a security checklist that assumes it does will silently report nothing. net.mptcp.enabled = 1 alone proves nothing about whether a listener exists — an application has to open one.

Nor does the running kernel match the distribution’s build number: the installed package and the running kernel are different objects, and a kernel update changes nothing until the machine boots into it.

Where the documentation stops

Four things are stated nowhere in the primary record, and all four change what you do next:

Why MPTCP. Anthropic says the exploit needs a multipath TCP listener and does not say why. The CVE record does not mention MPTCP at all. The Go notice says only that “the underlying bug is not specific to MPTCP. It is possible that systems with no MPTCP listeners are vulnerable to similar attacks.” Every explainer repeats the association without a mechanism, because there isn’t one published. Anthropic’s disclosure policy gives vendors 90 days plus a further 45 before it discusses details beyond the advisory, so the answer is embargoed, not absent.

Whether a plain listener is exploitable. Same sentence, same gap. “Not specific to MPTCP” is a statement about where the code lives, not a demonstration that a plain TCP listener can be driven to RCE.

The proof of concept. None is public. A GitHub search on 1 November returned 36 repositories referencing the CVE — earliest created 29 October 14:17 Z, latest 31 October 16:04 Z — of which eight use PoC, trigger or oracle wording in their names or descriptions. The number 36 matches what the most-read writeup reported, which is a rare case of a secondary source checking out. CISA’s KEV catalog, in a release dated 30 October, still does not list this CVE among its 1,766 entries. Four days after an AI-written root exploit is a normal time for a bug to be absent from KEV, and it is also not evidence that nothing is happening.

The weakness class. The CNA record lists no CWE. Red Hat assigns CWE-825, Expired Pointer Dereference. Vulnerability scanners that classify from CWE will find nothing to match; scanners that read NVD severity will find status “Received” and no NVD score of its own, leaving the CNA’s 9.8 as the only number available. The single number most tools will show you is the one score every dissenting vendor rejected.

Two community checks worth more than another roundup

The most useful public artifacts are not advisories.

A marker oracle. One toolkit makes each race attempt carry a unique marker inside its IP options, then watches the SYN-ACKs: a reply that echoes a different attempt’s marker proves the kernel read a freed object. That is a reachability test that does not require KASLR defeat, heap grooming or a payload — it stops at “the UAF happened, here is the packet that proves it”. Its own README is honest that it is not a one-click exploit and that “Stage 3 … is described in …, but not implemented: without a specific kernel build it would be fiction.”

A dissent about the race window. The same repository that ships the probe argues the other side of the score argument in its own words: the vulnerable interval is “roughly the distance between the ireq_opt load and the first dereference”, the free is “100 µs to milliseconds later”, and on a stock non-PREEMPT_RT kernel “softirqs are not scheduled at all and even a flood of hard IRQs would still finish in about 10ns”. Its conclusion — “You should patch your servers, but probably not panic about it” — is a technical claim that can be tested, not a vibe. It also names the conditions it thinks would change its mind: PREEMPT_RT, threadirqs, oversubscribed VMs, and per-packet steering. If your fleet is heavily oversubscribed, that is the paragraph to take seriously; if it is bare metal with default settings, it is the reason 53 minutes is not the number that matters.

We have not run either tool. We are not linking them, for the same reason the earlier writeup gave: an untested trigger on a production host is how a kernel bug becomes an outage.

Interim mitigations, and what each leaves open

MeasureCatchesLeaves openCost
nftables, hook input (Red Hat, priority −310)IPv4 TCP to the host carrying IP optionsTraffic the host routes or bridges to containers; the bug itselfBreaks CIPSO label networks; Red Hat: “reduces exposure … but cannot fully prevent it”
nftables, hook prerouting (Atlas/AlmaLinux)Also forwarded trafficTraffic bridged between containers on one host (AlmaLinux warns exactly this)Same CIPSO caveat
iptables -t raw -m u32 --u32 "0>>24&0xF=6:15"Same class of packet, pre-conntrackNeeds xt_u32; RHEL 9/10 kernels omit itLegacy path only
net.mptcp.enabled = 0New MPTCP sockets; Go falls back to plain TCPNon-MPTCP listeners (the actual bug); already-open listeners, which stay MPTCP until restarted; per-netns — each new container namespace starts with MPTCP onApps that need MPTCP; Go team calls per-program disabling “fragile”
GODEBUG=multipathtcp=0One processEvery other binary in the imageSilent behaviour change per program
init_on_free=1Not the bug; makes a stale read return zeroes, narrowing the leakOut-of-bounds writes and control-flow hijackNeeds a reboot and grep -o init_on_free=1 /proc/cmdline to prove; “commonly cited cost is roughly 1–5% on workloads heavy in memory frees”; off by default in the EL kernels the reporter checked

The filter rules are the only measures that touch the trigger rather than the exploit’s requirements, which is why they read as the sensible stopgap — and also why their gaps matter: a rule hooked on input never sees a bridged container-to-container packet, and that traffic runs the same vulnerable handshake.

Two corrections to the sentences you will see quoted

“Affects all Ubuntu versions.” Ubuntu’s tracker shows 14.04 LTS trusty as Not affected for the linux package, because 3.13 is below the 4.4 floor set by the 2015 commit. Derivative kernels differ: linux-azure on 14.04 is listed Vulnerable, since that is a 4.15 kernel. The correct statement is “every Ubuntu release from 16.04 up”, not “all”. The 14.04 line is exactly the kind of detail that decides whether a fleet audit produces a false positive on an ESM box.

“Two-line” versus “four-line”. The commit is +2 −2: two statements replaced, four lines changed in the diff. Both descriptions are accurate and they are not interchangeable — a grep-based patch checker counting changed lines will look for the wrong number. Count statements, or check the commit hash.

FAQ

Is my host patched if uname -r looks new? Only if that exact build is on your distribution’s fixed list. Ubuntu’s fixed 6.8.0 build is 6.8.0-149.149 and it is not published yet; a 6.8.0-147 box is not patched regardless of how recently it was updated.

Does IPv6 matter here? The first issue does not affect IPv6 — tcp_v6_syn_recv_sock() duplicates the options. The migration variant does affect ipv6_opt, but only when CALIPSO is set, and the commit says that “will be addressed in a separate patch”. No CVE for that follow-up appears in the references we checked.

Do I need MPTCP to be at risk? Not according to the Go team: the bug is not specific to MPTCP. Do you need it to be at risk from the published exploit? Yes — that is the one exploit that exists, and it requires an MPTCP listener.

Is this in CISA KEV? No, as of the catalog release dated 30 October 2026 (1,766 entries). Absence from KEV means no confirmed in-the-wild exploitation, not that exploitation is impractical.

Should I apply the nftables rule? If you cannot patch this week and you serve IPv4 TCP from the internet, the rule costs little and closes the only documented trigger. Put it at the edge if you can — then every host behind it inherits the protection, including the ones you cannot reach. Do not mistake it for a fix, and do not apply it blind if you run CIPSO-labelled networks.

What about containers? A container runs the host kernel. A new image changes nothing. Check on the host, not inside the container — and remember that a prerouting rule protects forwarded traffic while a bridged packet never leaves the host’s bridge.

The short version

The kernel had a two-statement bug for eleven years: an object handed to a child socket without a copy, then freed with kfree() while another CPU was still reading it under RCU. The fix is kfree_rcu() in two destructors, and it is already in eight stable lines.

Everything after that is a disagreement about AC. The CNA says the attacker controls both sides of the race and can retry forever, so AC:L. Three vendors say a race is a race, so AC:H, and CVSS 4.0 gives them AT:P to say it precisely. Ubuntu publishes 9.8 and prioritises it High. All of them are describing the same two-statement commit, and none of them can tell you whether your kernel has it.

That last part is the only part you can settle yourself, and it takes four commands — three if your image has iproute2.