On 10 November 2026 the kernel’s CVE team published CVE-2026-99288, a race in the code that matches every incoming TCP packet to its connection. The fix commit comes with a kernel panic triggered by a proof of concept, and the code dates back to Linux 4.4, released in 2016. The fix reached 6.12, 6.18 and 7.3 the same day. The older long-term kernels, 5.10, 5.15, 6.1 and 6.6, do not have it yet.
“Every box we ship speaks TCP” is the right first reaction, and for once it is close to the right answer: there is no exotic feature to switch on here. But the details still matter: what has actually been demonstrated (a crash, not code execution), which part of the attack is under an attacker’s control, and why on arm64 the release that “has the fix” carries only half of it. This post goes through the bug, the kernels that carry it, who can reach it, and how to decide quickly.
The facts
| CVE-2026-99288 | |
|---|---|
| Component | __inet_lookup_established() and __inet6_lookup_established(), the lockless lookup of established TCP connections for IPv4 and IPv6 |
| Bug class | Race condition: memory of a closed socket is reused for a new one while a lookup still holds the old address, and the lookup returns the new, not yet registered socket |
| Demonstrated impact | Kernel panic (NULL pointer dereference in tcp_v4_rcv()) in the packet receive path. No code execution shown |
| Kconfig gate | None for the bug itself. The published crash uses SYN cookies (CONFIG_SYN_COOKIES, also selected by the netfilter SYNPROXY targets) |
| Introduced by | 079096f103fa “tcp/dccp: install syn_recv requests into ehash table”, first released in 4.4 |
| Affected kernels | Every release from 4.4 up to 6.12.116, 6.18.62 and 7.3.3; all 5.10, 5.15, 6.1 and 6.6 releases to date; every end-of-life series since 4.4, such as 5.4 and 7.2, which will not be fixed |
| Fix | 4e2b3ef19e0e “inet: reject unhashed sockets during established lookups”, in mainline since 7.4-rc2, with Cc: stable. A second patch, 5b5d8f351dd9, fixes memory ordering on arm64 and other weakly ordered CPUs |
| Stable and long-term status | Fixed in 6.12.117, 6.18.63 and 7.3.4 (first patch only). Queued for 5.10, 5.15, 6.1 and 6.6 |
| Score | No CVSS from the kernel CNA; NVD status “Received”, no NVD analysis yet |
| Exploit | The reporter’s proof of concept is not public; not in CISA KEV |
How the bug works
For every incoming TCP segment, the kernel looks up the connection it belongs to in a hash table. To keep that fast, the lookup takes no locks. The memory for TCP sockets comes from caches marked SLAB_TYPESAFE_BY_RCU: when a socket is closed, its memory may be handed out again right away, but only to another socket of the same kind. A lookup that found the old socket can therefore end up holding a different, newer socket at the same address.
The lookup knows this and protects itself: after taking a reference, it checks the connection’s addresses and ports again. What it did not check is whether the socket it now holds is actually registered in the hash table. If the new socket happens to have the same addresses and ports, the second check passes. The fix adds the missing test:
if (unlikely(!refcount_inc_not_zero(&sk->sk_refcnt)))
goto out;
- if (unlikely(!inet_match(net, sk, acookie,
- ports, dif, sdif))) {
+ if (unlikely(hlist_nulls_unhashed_lockless(&sk->sk_nulls_node) ||
+ !inet_match(net, sk, acookie, ports, dif,
+ sdif))) {
sock_gen_put(sk);
goto begin;
}
The reporter’s proof of concept makes the memory come back as a request socket created from a SYN cookie. That kind of socket is never registered and has no listener attached. The lookup returns it anyway, and tcp_v4_rcv() then reads the listener’s state through a NULL pointer. The result is a panic in interrupt context, which takes the whole machine down. In the published trace, the crashing packet arrived through the virtual machine’s network driver, not over loopback.
There is no real gate
Unlike most kernel CVEs we write about, this one is not behind a driver, a filesystem or an opt-in feature. The lookup runs for every TCP packet on every kernel with networking. The only switch in the published crash is SYN cookies:
# Built in? (also selected by the netfilter SYNPROXY targets) grep SYN_COOKIES .config # CONFIG_SYN_COOKIES=y # On by default: cookies are sent when a listener's SYN queue overflows sysctl net.ipv4.tcp_syncookies # net.ipv4.tcp_syncookies = 1
With the default setting of 1, a server only falls back to SYN cookies when the SYN queue of a listening socket overflows. A remote client can force that with a SYN flood, which is the textbook attack SYN cookies exist to absorb. Turning cookies off is not a good trade: it removes the demonstrated crash route but reopens the door to SYN floods, and the lookup bug itself stays in place.
CONFIG_SYN_COOKIES lacks the code path the published crash uses, but the lookup flaw is in the core of TCP. In his review, the TCP maintainer names TCP Fast Open, migrated request sockets and new connection sockets before they are registered as further cases of sockets that hold a reference without being in the table. We would not mark a product not_affected on the build configuration alone.Two attackers, two answers
| Remote client | Local, unprivileged user | |
|---|---|---|
| What they control | The packets of their own connections: they can reuse the same addresses and ports, and they can push a listener into SYN-cookie mode with a SYN flood | Both ends of the connection over loopback, the timing of their own sockets, and unlimited retries |
| What they need from you | A TCP port on the device that they can reach | Nothing beyond a shell; TCP over loopback needs no privileges |
| The race | A short window on the target’s CPUs between a lookup and the reuse of the same memory, which the attacker cannot observe | The same window, with far better control over both sides |
| Realistic outcome | A kernel crash if the race is won. Whether a purely remote attacker can win it has not been shown: the trace shows the crashing packet arrived over a network interface, but not where the other side of the race ran, or how many attempts it took | A crash. Privilege escalation has not been shown (see below) |
For an appliance, the remote case is the one that matters: if a remote attacker can win this race, a device that answers TCP on a reachable port goes down, and it reboots or hangs until someone power-cycles it. For devices that run untrusted local code, the local case is the easier one.
Is it a local root risk? Compared with exploited bugs of the same class
A memory reuse race in the kernel is the raw material of many privilege escalation exploits, so the question is fair. We compared this bug with race-condition use-after-frees in our exploit database that have a working public exploit, and with its sibling from last week in the same TCP code. The score is our estimate of how hard each bug is to turn into root, from 1 (easy) to 5 (hard).
| CVE | Freed memory | Use triggered by | Exploit | Score |
|---|---|---|---|---|
| CVE-2026-23272 nf_tables race | Generic kmalloc-cg-64; refilled from the same cache | Attacker’s syscalls and packets | kernelCTF, probabilistic | 2 / 5 |
| CVE-2026-46242 Bad Epoll | Dangling struct file in a type-stable cache; cross-cache | Attacker’s close() race | kernelCTF + PoCs, ~99 % | 3 / 5 |
| CVE-2024-26583 kernel TLS race | Generic kmalloc-512; cross-cache | Attacker’s recv()/close() race | kernelCTF, “very unreliable” | 4 / 5 |
| CVE-2026-99270 TCP request timer | request_sock, type-stable cache; cross-cache | The retransmit timer, on the kernel’s schedule | None | 4–5 / 5 |
| CVE-2026-99288 this bug | Socket in a type-stable cache; in the crash, a different live socket | The attacker’s incoming packet | Crash PoC, private | 4–5 / 5 |
One property favours the attacker: here the kernel touches the reused memory when a packet arrives, so the attacker decides when to try, as in the exploited bugs at the top of the table. Another property works against them. In the published crash, the stale pointer does not land on freed memory but on a different, live socket: the kernel treats one socket as another and then fails on a missing field. The patch also closes error paths where a request socket could be freed directly while a stale lookup still held a reference to it, so freed memory is not ruled out. In our reading, turning this into control over memory would need more than the published crash shows.
The reporter’s first patch version described a second route that did end in a use-after-free of a full socket. That result came from a test kernel with artificial delays added to widen the race. The TCP maintainer questioned whether the sequence is possible on a real kernel, and the paragraph was removed from the final commit. We do not count it as demonstrated.
The timeline NVD won’t show yet
| Date | Event |
|---|---|
| Oct 2015 | 079096f103fa puts request sockets into the connection hash table; first released in 4.4 |
| 2017–2020 | arm64 (4.15) and 32-bit ARM (4.16), then every architecture with 5.5, switch to a reference-count implementation that is not a full memory barrier on weakly ordered CPUs (the problem the second patch fixes) |
| 21 Oct 2026 | Hyunwoo Kim reports the bug with a first patch to the networking list |
| 29 Oct 2026 | Fifth version, a two-patch series; Eric Dumazet reviews the first patch, Kuniyuki Iwashima both |
| 3 Nov 2026 | Both patches applied to the networking tree |
| 5 Nov 2026 | Merged into mainline; released in 7.4-rc2 (8 November) |
| 6 Nov 2026 | The first patch is queued for 5.10, 5.15, 6.1, 6.6, 6.12, 6.18 and 7.3. The same day, 5.10.274, 5.15.225, 6.1.192 and 6.6.162 are released without it |
| 10 Nov 2026 | 6.12.117, 6.18.63 and 7.3.4 released with the first patch. The second patch is queued for those three branches and fails to apply to 5.10, 5.15, 6.1 and 6.6 |
| 10 Nov 2026 | CVE-2026-99288 published by the kernel CNA, without a CVSS score; NVD status “Received” |
Which kernels really have it
This time the CVE record and the code largely agree. The record marks everything from 4.4 onward as affected, with 6.12.117, 6.18.63 and 7.3.4 as the first fixed releases. It has no entry for mainline, so tools that follow it may still flag 7.4-rc2 and later, and it tracks only the first patch. We checked the source of the latest release of each branch: the new check is in 6.12.117, 6.18.63 and 7.3.4, and missing from 5.10.274, 5.15.225, 6.1.192 and 6.6.162. 5.4 reached end of life with 5.4.302 in December 2025 and will not get the fix; neither will 7.2, which ended with 7.2.15 on 1 November and is still what Debian testing ships.
The second patch and arm64
The fix came as two patches. The first adds the missing check; the second makes sure that, on CPUs that may reorder memory accesses, the check actually reads the new socket’s state and not the old one’s. During review, the networking list’s automated reviewer pointed out that the first patch alone does not close the race on such CPUs, and asked whether both should be backported together; the question went unanswered. x86 does not reorder in this way; arm64, 32-bit ARM, RISC-V, PowerPC and MIPS, among others, can.
The stable releases of 10 November carry only the first patch. The second is queued for the next 6.12, 6.18 and 7.3 releases. For 5.10, 5.15, 6.1 and 6.6 it failed to apply: those branches lack the reference-count helpers it uses and need an adapted backport. If you ship arm64, plan for two updates, or carry both patches yourself.
Was the risk visible at merge?
Yes. Both patches carry Cc: stable, and the commit message includes the panic. The first patch reached three stable branches within a week of its mainline merge. The remaining gap is simply the release schedule: 5.10, 5.15, 6.1 and 6.6 were released the same day the patch was queued, so it is expected in their next releases.
What the distributions say
Status of their trackers on 11 November 2026:
| Distribution | Status | Their rating |
|---|---|---|
| Debian | 12 “bookworm” (6.1.190), 13 “trixie” (6.12.116), testing (7.2.15) and unstable (7.3.3): vulnerable. Fixed version recorded for unstable: 7.3.4 | — |
| Ubuntu | Needs evaluation, every release from 14.04 to 26.10 | Priority medium |
| Red Hat | Bugzilla entry open, no product status yet | Severity high, titled “Denial of Service via race condition in TCP socket lookup” |
| SUSE | No CVE page yet | — |
Red Hat’s title matches what has been demonstrated: a denial of service.
What your tools will show
Neither the kernel CNA nor NVD has scored the CVE yet, and NVD has no CPE version range. Dependency-Track and other NVD-fed scanners will show it unscored, or not match it to your kernel, until NVD’s analysis lands. Scanners that read the CVE record directly will flag every 5.10, 5.15, 6.1 and 6.6 release, which is correct today. They will not tell you that an arm64 6.12.117 still waits for the second patch.
30-second checklist
- Is your kernel version in the affected range? Before 4.4:
not_affected, justificationcode_not_present. 6.12.117, 6.18.63, 7.3.4 and later in their branches: stateresolvedon x86. On arm64 and other weakly ordered CPUs, keep it open until you ship the release with the second patch, which is queued for the next 6.12, 6.18 and 7.3 releases. Otherwise continue. - Who can reach TCP on the device? If no listening port is reachable from untrusted networks (for example, the device sits on an isolated management network behind a firewall you control) and no untrusted local code runs on it (third-party apps, containers, other users), you can record
not_affectedwith justificationprotected_at_perimeterfor now, and patch with your next regular update. A firewall does not stop the local route over loopback. Otherwise continue. - Patch. Take the next 5.10, 5.15, 6.1 or 6.6 release once it is out, or carry the first patch now: backports for 5.10 and 5.15 have been posted to the stable list, and the patch is in the stable queue for 6.1 and 6.6. Do not switch off SYN cookies as a workaround.
Sometimes the answer is “patch”
Most kernel CVEs fail one of the checks above: the code is not built, not loaded, or not reachable by the attacker who matters. This one passes most of them, and that is useful to know too: it tells you which CVE of this week deserves the update slot. Upload your .config to KernelScan and every kernel CVE is checked against your exact kernel version, the long-term backports and the options your product builds. On paid plans, the security factors you declare for a product, such as whether it exposes TCP services to untrusted networks, feed into the assessment of each CVE.
References: CVE record · NVD · fixes 4e2b3ef19e0e and 5b5d8f351dd9 · introducing commit 079096f103fa · patch discussion (v5) and first version with review · ip-sysctl documentation (tcp_syncookies) · distribution trackers as linked in the table above. The per-branch fix status (checked in the source of each release tag and in the stable queue), the threat-model split and the exploitability assessment are our own analysis as of 11 November 2026.