Guide · Backend
Linux's October 2026 TCP Use-After-Free Explained: How to Patch CVE-2026-98432
20 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- CVE-2026-98432 is a use-after-free bug in the Linux kernel's IPv4 TCP code. Memory holding a connection's IP options, rare extra fields in a packet header, could be freed while another CPU core still read it.
- Use-after-free means code keeps using memory after it was handed back. Here the reader was building a SYNACK, the server's reply in the TCP handshake, so a remote client can trigger it over the network.
- Upstream fixes shipped in October 2026: 7.3, 7.2.10, 6.18.56, 6.6.159, 6.1.190, 5.15.223 and 5.10.272. Distro kernels carry their own numbers, so check your distro's CVE page.
- The fix is two lines. A plain free became kfree_rcu(), which waits for an RCU grace period: the moment every reader already holding the pointer has finished.
- Beginners can stop after the patch steps. Engineers then get the race traced through kernel source, Red Hat's interim firewall rule tested, why scores range from 8.1 to 9.8, and a 6.12 gap.
What goes wrong when a kernel frees memory another core is still reading?
The other core reads whatever now sits in that memory, so the kernel can crash or corrupt its own data. Red Hat says code execution may be possible but needs precise timing. That is the core of CVE-2026-98432, a bug in the Linux TCP handshake fixed in October 2026.
Picture a common setup. A payments start-up in Bengaluru runs 40 virtual machines behind a load balancer, half on Debian 12 and half on Ubuntu 24.04. The setup is made up, but the bug is real.
On a Saturday morning the team sees a hosting status page warning about a remote code execution CVE in the kernel. Every one of their machines accepts TCP connections from the internet. They need three answers fast: are we exposed, is there a fix, and what do we do on the machines with no fix yet.
This guide answers those three questions for that team, and explains the bug well enough that you could describe it in an interview.
Which words do you need before reading about CVE-2026-98432?
You need six terms. Each one is used later exactly as defined here.
- Kernel: the core of the operating system. It runs the network stack, so a kernel bug in TCP sits under every server program on the machine.
- TCP handshake: how a connection starts. The client sends SYN, the server replies SYNACK, and the client answers ACK.
- Request socket: a small placeholder the kernel creates when a SYN arrives, before the handshake finishes. When the ACK arrives, the kernel builds a full child socket from it.
- IP options: rarely used extra fields in the IPv4 header, such as Record Route. They sit between the fixed 20-byte header and the data.
- Use-after-free: code frees a block of memory, then some code still holding the old address reads or writes it.
- RCU (read-copy-update): a kernel scheme that lets readers use shared data without locks. Writers delay freeing old data until a grace period has passed.
What is CVE-2026-98432 and why is it rated up to 9.8?
CVE-2026-98432 is a race in the IPv4 TCP handshake. One CPU core frees the memory holding a connection's IP options while another core is still reading it to build a SYNACK. The fix commit says this leads to a use-after-free in ip_options_build(), the function that copies options into an outgoing packet.
It is rated high because the trigger is network traffic, with no login. Ubuntu lists CVSS 3.1 base score 9.8 and priority High. Red Hat rates it Critical. CVSS is a 0 to 10 severity score, and 9.8 is near the top.
The bug was reported by an outside security researcher. Eric Dumazet wrote the fix, and it was posted on 1 October 2026.
How does the TCP handshake create this race?
Two CPU cores touch the same block of memory, and only one of them thinks it owns it. The diagram shows the order, then the subsections walk through each step.
How two cores end up sharing one options block
Drag to exploreNotice that the child socket and the SYNACK path point at the same options memory. Nothing stops the free in step 4 while step 3 is still reading.
Diagram connections:
- SYN with IP options to Request socket (1. options saved)
- Request socket to Child socket (2. ACK arrives, pointer moves)
- Retransmitted SYN to SYNACK being built (3. same request socket)
- Request socket to SYNACK being built (pointer read by core 2)
- Child socket to Child socket freed (4. socket released)
- Child socket freed to Use-after-free (memory gone)
- SYNACK being built to Use-after-free (still reading)
- SYN with IP options: Client → server: options saved in a new block
- Request socket: Holds ireq_opt, a pointer to that block
- Child socket: Gets the same pointer, no copy is made
- Retransmitted SYN: Client → server: handled on another core
- SYNACK being built: Reads options under rcu_read_lock() only
- Child socket freed: inet_sock_destruct() calls kfree() at once
- Use-after-free: ip_options_build() reads freed memory
Step 1: the request socket keeps a copy of the client's IP options
When a SYN carries IPv4 options, the kernel allocates a small block and stores a pointer to it in the request socket. The field is called ireq_opt. The server needs those options to build its SYNACK reply.
Not spelled out in the advisories as of 1 November 2026, but visible in the source: the block is only created when the SYN actually carries IPv4 options. In the 6.8 source, tcp_v4_save_options() returns nothing when the option length is zero.
Step 2: the child socket takes the pointer without copying
When the client's ACK completes the handshake, tcp_v4_syn_recv_sock() builds the child socket. The fix commit says it transfers ownership of ireq_opt to the child without copying it. The child now believes it owns that block and will free it when the child is destroyed.
Step 3: a retransmitted SYN is handled on another core
Clients resend a SYN when they think the first one was lost. The commit says another core can process that retransmitted SYN for the same request socket at the same moment. That core builds a SYNACK and reads ireq_opt holding only rcu_read_lock().
Holding rcu_read_lock() is a promise, not a lock. It tells the kernel: I am reading, so do not free what I can see until I finish. The promise only works if whoever frees the memory waits for readers.
Step 4: the free does not wait
Before the fix, inet_sock_destruct() freed the child's options with a plain kfree(). Plain kfree() hands memory back at once. If core 2 is still inside its read section, it reads memory that may already hold something else.
The commit names a second route to the same problem: request socket migration. It is opt-in: it moves pending connections to another listener on the same port when one closes. It is switched on by the setting net.ipv4.tcp_migrate_req=1, or by a BPF program, a small program loaded into the kernel, of the migration type.
How does waiting for an RCU grace period fix it?
The fix delays the free until every reader that might hold the old pointer has finished. It changes kfree() to kfree_rcu() in two functions.
| Function | File | Before | After |
|---|---|---|---|
| inet_sock_destruct() | net/ipv4/af_inet.c | kfree() of inet_opt, freed at once | kfree_rcu() of inet_opt, freed after a grace period |
| tcp_v4_reqsk_destructor() | net/ipv4/tcp_ipv4.c | kfree() of ireq_opt, freed at once | kfree_rcu() of ireq_opt, freed after a grace period |
Think of a busy office canteen. A cleaner wants to take away a steel plate that diners may still be eating from.
- The plate is the options memory block.
- Sitting down and picking up the plate is a reader entering rcu_read_lock() and reading the pointer.
- Getting up from the table is rcu_read_unlock().
- Washing the plate is the real free. Washing it while someone eats is the use-after-free.
- The RCU rule: wait until everyone who was already seated when the plate was cleared has left, then wash it. That wait is the grace period.
Where the picture breaks: the kernel does not watch individual diners or plates. It waits until every CPU has passed through a quiet point, so it may wait for readers who never touched this block.
What kfree_rcu() changes on the timeline
Drag to exploreNotice that the free now happens only after the last reader that started earlier has left its read section.
Diagram connections:
- Core 2 starts reading to Core 2 finishes (reads options safely)
- Core 1 frees the socket to kfree_rcu() queues it (free requested)
- kfree_rcu() queues it to Grace period (starts the wait)
- Core 2 finishes to Grace period (last earlier reader exits)
- Grace period to Memory freed (then free)
- Core 2 starts reading: rcu_read_lock(), picks up the options pointer
- Core 1 frees the socket: inet_sock_destruct() runs
- kfree_rcu() queues it: Memory is not handed back yet
- Grace period: Waits for every earlier reader on all CPUs
- Core 2 finishes: SYNACK sent, rcu_read_unlock()
- Memory freed: No earlier reader can still hold it
Why not copy the options instead, so each socket owns its own block? That is what the IPv6 code already does. The commit says IPv6 escapes the first issue because tcp_v6_syn_recv_sock() duplicates the options. For IPv4 the fix instead makes these two frees match other IPv4 paths, such as do_ip_setsockopt(), which the commit says already use kfree_rcu().
Which Linux kernels are fixed for CVE-2026-98432?
Upstream, seven branches carry the fix. We confirmed each in the kernel.org changelog for that release, where the entry reads "commit e3f33b0a1d89 upstream".
| Branch | First fixed release | Released | Notes |
|---|---|---|---|
| 7.3 (mainline) | 7.3 | 18 Oct 2026 | Fix landed in 7.3-rc7 |
| 7.2 | 7.2.10 | 11 Oct 2026 | Latest is 7.2.14 |
| 6.18 | 6.18.56 | 11 Oct 2026 | Latest is 6.18.60 |
| 6.12 | Not confirmed | Not confirmed | Backport commit exists; not found in the 6.12.112 to 6.12.115 changelogs |
| 6.6 | 6.6.159 | 15 Oct 2026 | Latest is 6.6.161 |
| 6.1 | 6.1.190 | 15 Oct 2026 | Latest is 6.1.191 |
| 5.15 | 5.15.223 | 15 Oct 2026 | Latest is 5.15.224 |
| 5.10 | 5.10.272 | 15 Oct 2026 | Latest is 5.10.273 |
One secondary write-up lists 6.12.113 as fixed. The upstream 6.12.113 changelog does not include the commit, so treat upstream 6.12 as unconfirmed as of 1 November 2026. Debian's 6.12.113-1 package is a different thing: Debian marks it fixed.
Most servers run a distro kernel, and distros backport fixes into their own version numbers. This is the status each vendor published.
| Distro | Status | Fixed package or advisory | As of |
|---|---|---|---|
| Debian 12 (bookworm) | Fixed | linux 6.1.190-1 (DLA-4841-1); linux-6.12 6.12.113-1~deb12u1 (DLA-4850-1) | Read 1 Nov 2026 |
| Debian 13 (trixie) | Fixed | linux 6.12.113-1 (DSA-6568-1) | Read 1 Nov 2026 |
| Ubuntu 22.04, 24.04, 26.04, 26.10 | Vulnerable, fix in progress | None yet | Page updated 30 Oct 2026 |
| Ubuntu 16.04, 18.04, 20.04 | Vulnerable | None yet | Page updated 30 Oct 2026 |
| Ubuntu 14.04 | Not affected | Not needed | Page updated 30 Oct 2026 |
| Amazon Linux 2 and 2023 | Fixed | ALAS2-2026-4065, ALAS2023-2026-3365 and others | Page updated 30 Oct 2026 |
| RHEL 8, 9 and 10 | Affected, no fix yet | RHSB-2026-016 interim firewall rule | Bulletin of 30 Oct 2026 |
For the Bengaluru team, this splits the fleet in two. The 20 Debian machines can be patched today. The 20 Ubuntu machines have no fixed kernel yet and need an interim plan.
Is my server affected by CVE-2026-98432?
Assume yes if it runs an unpatched Linux kernel and accepts IPv4 TCP connections from networks you do not control. Web servers, API servers, SSH and database ports all accept TCP.
The buggy code runs on the listening side of the handshake, where request sockets live. A machine with no listening TCP port reachable from outside does not run it for outside traffic.
- IPv4 matters. The commit says IPv6 is not affected by the main race, because IPv6 copies the options. Most servers accept IPv4 too.
- Containers do not change the answer. Standard containers share the host's kernel, so you patch the host or Kubernetes node, not the image.
- Managed services are your provider's job. If you only use a hosted database or serverless platform, watch the provider's status page.
- Request socket migration widens it. If net.ipv4.tcp_migrate_req is 1, or a migration-type BPF program is attached, the second path applies too. The sysctl defaults to 0.
How do you check and patch a Linux server for CVE-2026-98432?
Find your kernel version, compare it with your vendor's page, install the fixed kernel, then reboot. A kernel update does nothing until the machine boots into it.
uname -r
cat /proc/sys/net/ipv4/tcp_migrate_reqThe first line prints the running kernel, for example 6.8.0-147-generic on an Ubuntu 24.04 server. The second prints 0 unless someone switched request socket migration on. Both work without sudo. On older kernels that predate request socket migration, the file does not exist and cat says so; that path is then absent.
This small script compares an upstream version number with the fixed releases. It refuses to judge distro builds, because their numbers do not tell you what was backported. Save it as check_kernel.py.
import re
import sys
# First upstream release in each branch that carries the fix (kernel.org changelogs, read 1 Nov 2026)
FIXED = {(7, 2): 10, (6, 18): 56, (6, 6): 159, (6, 1): 190, (5, 15): 223, (5, 10): 272}
def check(release):
m = re.match(r"(\d+)\.(\d+)(?:\.(\d+))?(-rc\d+)?(.*)", release)
if not m:
return "cannot read this version string"
major, minor, patch = int(m[1]), int(m[2]), int(m[3] or 0)
if m[5]:
return "distro or vendor build: the number alone cannot tell you, check your vendor CVE page"
if (major, minor) >= (7, 3):
if m[4] and (major, minor) == (7, 3) and int(m[4][3:]) < 7:
return "upstream release candidate before 7.3-rc7: not fixed"
return "upstream 7.3 or newer: fixed"
if (major, minor) == (6, 12):
return "upstream 6.12: fixed release not confirmed, check your distro's CVE page"
if (major, minor) not in FIXED:
return "branch not in the fixed list: check your distro, or move to a fixed branch"
first = FIXED[(major, minor)]
if patch >= first:
return f"fixed (needs {major}.{minor}.{first} or later)"
return f"NOT fixed: upgrade to {major}.{minor}.{first} or later"
for release in sys.argv[1:]:
print(f"{release:24} {check(release)}")Run it as python3 check_kernel.py "$(uname -r)" or pass any version strings. These are real results from Python 3.13 on 1 November 2026:
| Input | Result |
|---|---|
| 7.3-rc6 | upstream release candidate before 7.3-rc7: not fixed |
| 7.2.9 | NOT fixed: upgrade to 7.2.10 or later |
| 6.6.161 | fixed (needs 6.6.159 or later) |
| 6.12.115 | upstream 6.12: fixed release not confirmed, check your distro's CVE page |
| 5.4.290 | branch not in the fixed list: check your distro, or move to a fixed branch |
| 6.1.0-28-amd64 | distro or vendor build: the number alone cannot tell you, check your vendor CVE page |
The last row is the trap. On Debian 12, every kernel from the main linux package reports 6.1.0 plus a build number, whether it is built from 6.1.190 or a much older release. The 6.1.0 part is a fixed name, not the upstream version.
Find your kernel package
On Debian or Ubuntu run apt list --installed 'linux-image*'. On RHEL or Amazon Linux run rpm -q kernel. Note the package version, not just uname.
Compare with your vendor's page
Debian 12 needs linux 6.1.190-1 or later, or linux-6.12 6.12.113-1~deb12u1 if you run that newer kernel. Debian 13 needs 6.12.113-1 or later. Otherwise use the vendor's CVE page.
Install the update
Use your normal package upgrade. Drain the machine from the load balancer first, so live connections move to its neighbours.
Reboot into the new kernel
Reboot, then run uname -r again. Confirm the machine booted the kernel you just installed, not an older one still on disk.
Put it back and move on
Return the machine to the load balancer, confirm health checks pass, then do the next one.
For the team in our example, that is 20 rolling reboots on Debian today. Beginners can stop here. The rest is for engineers: the interim rule, the scores and the race in code.
What can you do if your distro has no fixed kernel yet?
Red Hat's bulletin RHSB-2026-016 gives an interim nftables rule that drops incoming IPv4 TCP packets carrying IP options. Red Hat says this can reduce exposure but cannot fully prevent the issue. Patching remains the fix.
Our reading of the source explains why it helps: as Step 1 showed, the options block exists only when a SYN carries IPv4 options. A plain IPv4 header is 5 words of 4 bytes, so 20 bytes. hdrlength above 5 means options are present.
table inet cve_2026_98432 {
chain input {
type filter hook input priority -310; policy accept;
ip protocol tcp ip hdrlength > 5 counter drop
}
}The rule is plain nftables, so it is not tied to RHEL, though Red Hat is the only vendor we found publishing it as of 1 November 2026. Save it exactly as shown in a file named cve-2026-98432.nft, then check it, load it and inspect it:
sudo nft --check --file cve-2026-98432.nft
sudo nft --file cve-2026-98432.nft
sudo nft list table inet cve_2026_98432We tested it on 1 November 2026 in an isolated network namespace, with nftables 1.0.9 on an Ubuntu 24.04 userspace. A normal local connection still worked. A connection whose SYN carried a Record Route option timed out, and the counter showed packets 2 bytes 200: the SYN and its retry.
- It is not saved. A rule loaded this way is gone after a reboot, so reload it until the fixed kernel is in.
- Check after firewall reloads. Tools that manage nftables can replace the ruleset, so run the list command again afterwards.
- It can break rare traffic. Red Hat warns it may disrupt workloads that need IP options. Normal TCP options, such as window scaling, sit in the TCP header and pass.
- Check forwarded traffic. The rule uses the input hook. Packets the host forwards to containers or VMs are not addressed to the host, and the nftables docs say those pass the forward hook instead. The rule also covers only the network namespace it is loaded in, so containers or pods with their own namespace are not filtered by it. Patch the host first.
- Remove it after patching with sudo nft delete table inet cve_2026_98432.
Red Hat also tells you to test the rule against your own traffic in a non-production environment first. The Bengaluru team loads it on one Ubuntu machine, watches error rates for an hour, then rolls it out.
Why do vendors score CVE-2026-98432 between 8.1 and 9.8?
Because they disagree on how hard the race is to win. Ubuntu and Red Hat publish 9.8 with attack complexity Low, and Red Hat calls its score preliminary. Amazon Linux publishes 8.1 with attack complexity High, and calls the main impact a crash with possible memory corruption.
Red Hat's own statement explains the tension. It says an unauthenticated remote attacker can corrupt kernel memory or crash the system. It also says reliable code execution through this timing-sensitive bug needs precise control.
Some write-ups relay claims about working exploits for specific setups. As of 1 November 2026 we found no vendor advisory or kernel source that confirms them, so this guide does not repeat them. Patch on the 9.8 assumption anyway, because a crash on every server is bad enough.
What does a use-after-free look like when a tool catches it?
The kernel bug cannot be safely reproduced on a laptop, but the bug class can. This harmless C program frees a struct, then reads it through a second pointer, much like the SYNACK path read the freed options.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct options {
int length;
char data[40];
};
int main(void) {
struct options *opt = malloc(sizeof *opt);
opt->length = 4;
strcpy(opt->data, "RR");
struct options *reader = opt; /* a second pointer to the same memory */
free(opt); /* the owner frees it */
printf("length = %d\n", reader->length); /* use after free */
return 0;
}Compile it with AddressSanitizer, a compiler feature that catches memory errors, using gcc -g -fsanitize=address uaf_demo.c -o uaf_demo, then run ./uaf_demo. With gcc 13.3 on 1 November 2026 it stopped with this report (addresses and paths trimmed):
==660==ERROR: AddressSanitizer: heap-use-after-free on address 0x504000000010
READ of size 4 at 0x504000000010 thread T0
#0 in main uaf_demo.c:18
freed by thread T0 here:
#1 in main uaf_demo.c:16
previously allocated by thread T0 here:
#1 in main uaf_demo.c:11What just happened? Read the report bottom up: allocated on line 11, freed on line 16, read on line 18. The read of size 4 is the int length field. The kernel has its own version of this tool, KASAN, which kernel developers use to catch the same class of bug.
Built without the sanitizer, the same program did not crash. It printed a random-looking value, such as length = 1643263949, because the allocator had reused those bytes. Nothing warned us, which is how use-after-free bugs survive testing.
How do you explain CVE-2026-98432 in a system design interview?
Interviewers rarely ask about one CVE. They ask how you would roll out an urgent kernel fix across a fleet, or how you reason about shared memory between threads. Use this frame.
Name the shared object and its owners
Options memory, pointed to by the request socket and the child socket. Two owners, one free.
Find the reader that holds no reference
The SYNACK path on another core reads under rcu_read_lock() and relies on the freer to wait.
Pick a lifetime rule and its cost
Copy per owner, as IPv6 does, or defer the free with RCU, as the fix does. Deferring holds memory a little longer.
Plan the rollout
Inventory kernels, patch internet-facing machines first, and do rolling reboots behind the load balancer so capacity never drops.
Cover the gap
Where no fix exists, apply the vendor's interim control, test it on one machine, and set a reminder to remove it.
Verify and name the limits
Check uname -r after reboot. Say what is still unknown: exploitability and an unconfirmed upstream 6.12 release.
Questions people ask
- Is CVE-2026-98432 being exploited in the wild?
- No vendor we read confirms exploitation as of 1 November 2026. By 31 October, Akamai's Linode status page said it had no evidence of exploitation on its platform. Red Hat's bulletin says its investigation is ongoing and will be updated. Treat that as no public confirmation yet, not as proof of safety, and patch anyway.
- Does using IPv6 protect my server?
- Only partly. The fix commit says IPv6 is not affected by the main race, because the IPv6 code copies the options instead of sharing them. It notes a similar IPv6 migration issue that only applies when CALIPSO labelling is in use, to be fixed separately. Almost every server also accepts IPv4, so you still need the patch.
- Do I need to reboot after installing the fixed kernel?
- Yes. The running kernel stays the old one until the machine boots into the new one. Check with uname -r after the reboot. Some vendors offer live patching that changes a running kernel, but we did not find a live patch announced for this CVE as of 1 November 2026.
- Are Docker containers or Kubernetes pods affected?
- Yes, through the host. Standard containers share the host machine's kernel, so a vulnerable host kernel is vulnerable for every container on it. Rebuilding images does not help. Patch and reboot the host or Kubernetes node, usually by draining it, updating it and returning it to the cluster.
- Should I set net.ipv4.tcp_migrate_req to 0?
- It is already 0 unless someone changed it. Teams switch it on to move pending connections between listeners during zero-downtime restarts. Setting it to 0 does not stop a migration-type BPF program, and it never touches the main race. If you rely on migration, patch rather than turning it off in a hurry.
- Why does my Debian kernel say 6.1.0 when the fix is in 6.1.190?
- Because Debian 12 keeps a fixed name for the kernel's binary interface, 6.1.0 plus a build number, while the package underneath moves forward. The real base shows in the package version, for example linux 6.1.190-1. Check the package version with apt list --installed 'linux-image*', and compare it with the Debian security tracker.
Sources and further reading
- Linux fix commit e3f33b0a1d89: ipv4: free inet_opt and ireq_opt after an RCU grace period
- Debian security tracker: CVE-2026-98432
- Ubuntu security: CVE-2026-98432
- Red Hat CVE page: CVE-2026-98432
- Red Hat bulletin RHSB-2026-016 (interim nftables rule)
- Amazon Linux Security Center: CVE-2026-98432
- Linode status: Customer Security Advisory CVE-2026-98432
- kernel.org ChangeLog-7.2.10
- kernel.org ChangeLog-6.18.56
- kernel.org ChangeLog-6.6.159
- kernel.org ChangeLog-6.1.190
- kernel.org ChangeLog-5.15.223
- kernel.org ChangeLog-5.10.272
- kernel.org ChangeLog-6.12.113 (fix not listed)
- Linux kernel docs: What is RCU?
- Linux kernel docs: IP sysctl (tcp_migrate_req)
- Linux kernel docs: Kernel Address Sanitizer (KASAN)
- nftables wiki: Netfilter hooks (input and forward hooks)