There’s a particular kind of software flaw that makes security researchers go quiet for a second before they explain it. Not because it’s incomprehensible, but because the gap between the cause and the consequence is so absurd you have to hear it twice.
CVE-2026-23111 is one of those. A single misplaced character. One inverted conditional check. And the result is a fully working, publicly available exploit that takes an unprivileged user on a standard Linux system and hands them root including a clean namespace breakout from inside a container.
The upstream patch, one line of code, shipped February 5, 2026. Working exploit code has been public since April. Exodus Intelligence published a full technical teardown on June 8. You have had four months to patch this. If you haven’t, you’re not racing the clock anymore.
Where the Bug Lives
To understand what broke, you need a rough picture of nftables. It’s Linux’s current-generation packet filtering framework, the successor to iptables, sitting on top of the kernel’s Netfilter hook infrastructure. It handles the rules that govern what network traffic gets passed, dropped, redirected, or inspected at various points in the networking stack. Most desktop and server Linux installs have it running whether they’ve explicitly configured it or not.
Within nftables, there’s a concept called a verdict map – a structure that matches incoming packets against a set and, depending on the match, executes a verdict (accept, drop, jump to another chain, and so on). The pipapo backend is a specific lookup algorithm used for these sets, designed to handle multi-field range matching efficiently using a bitset intersection approach. It’s the part of nftables that deals with complex, overlapping rule sets without collapsing into O(n) lookups.
There’s also something called a catchall element, which is essentially a default rule: if nothing else in the set matches, the catchall fires. It can reference a chain.
Now, nftables uses a two-generation system for its data structures – blob_gen_0 and blob_gen_1 to allow atomic-looking updates. When you commit a transaction, the kernel flips a cursor bit to swap which generation is active. This matters a lot later.
The bug is in a function called nft_map_catchall_activate(). Its job is to reactivate catchall elements during the abort phase of a failed transaction. In other words, to undo a deactivation when something goes wrong and a batch needs to be rolled back. During normal operation, when a transaction aborts, catchall elements that were deactivated as part of that transaction should be restored to active status.
The inverted check – the ! that wasn’t supposed to be there caused the function to do exactly the opposite. Instead of reactivating inactive elements during abort, it skipped them. The elements stayed deactivated. But the references they held onto did not disappear.
Specifically: when a pipapo set containing a catchall element that references a chain gets deleted, and a subsequent transaction in the same batch fails (triggering an abort), that catchall element is left incorrectly inactive. The chain it was pointing to has its reference counter decremented because the delete appeared to succeed but the pointer itself is still live. The reference count hits zero while something is still holding onto the chain.
What happens when a reference count hits zero in kernel memory management? The object gets freed. What happens if you then delete the chain through a normal path while a base chain rule still holds a pointer to that now-freed memory? Use-after-free. The kernel accesses memory it already handed back to the allocator, and an attacker who can control what gets allocated there next controls what the kernel reads and executes.
This is not a theoretical scenario. It is the exploit.
How Exodus Weaponized It
Oliver Sieber at Exodus Intelligence found this bug in early 2025 and spent time building a reliable, chained exploit. The result, published June 8, works at over 99% reliability on idle systems and around 80% under heavy heap pressure. Those numbers matter practically: most real-world exploitation doesn’t happen against idle lab machines. 80% reliability under load is more than enough to be operationally useful.
The exploit runs in four distinct transaction batches, each one manipulating nftables’ generational cursor to set up the next stage.
Batch 1 is where the bug gets triggered intentionally. The attacker deletes a pipapo set containing the catchall element that references a target chain, then deliberately forces an error at the end of the batch to cause an abort. The abort fires nft_map_catchall_activate() – the broken function which, because of the inverted check, skips reactivating the catchall element. The chain’s reference counter is decremented. The pointer survives. The reference count is now wrong.
Batch 2 sends a benign transaction. Its only purpose is to toggle the generational cursor, moving the active generation from one slot to the other. This is bookkeeping, but it’s necessary because the subsequent batches need to interact with the right generation of data structures.
Batch 3 deletes the pipapo set again, this time cleanly. This drives the chain’s reference counter to zero. The kernel, seeing a reference count of zero, marks the chain for deallocation. The slab allocator reclaims that memory.
Batch 4 deletes the chain through the normal path. But the base chain still has a rule with a live pointer into what is now freed memory. Use-after-free condition achieved.
At this point, the attacker controls the timing of what gets allocated into that freed slab region. The exploit proceeds through three phases: leak the kernel’s base address (defeat KASLR), leak heap addresses, then hijack control flow.
KASLR defeat happens by reclaiming the freed kmalloc-cg-32 slab region with a seq_operations structure. You do this by opening /proc/self/stat which is a completely unprivileged operation available to any user. The seq_operations struct contains function pointers into the kernel. An NFT_MSG_GETRULE request reads back the contents of the freed-then-reclaimed memory, and those function pointers are right there. Subtract the known offset to the function from the address you leaked, and you have the kernel’s load base. KASLR is gone.
Heap address disclosure uses the same principle. The exploit frees kmalloc-cg-192 objects and reclaims them with crafted nft_rule structures. These structures contain linked-list pointers – list_head entries that point to neighboring allocations on the heap. Exfiltrate them via another NFT_MSG_GETRULE request and you have heap addresses. The allocator’s layout is now mapped.
Control flow hijacking targets blob_gen_0, the data pointer in the deleted chain’s structure. The exploit overwrites it with a pointer to a fake nft_expr_ops structure, which the attacker has placed at a known heap address thanks to the previous leaks. That fake structure’s function pointer points to a ROP gadget: push rbx; pop rsp. This is a stack pivot. It redirects the kernel stack pointer to attacker-controlled memory.
From there, the ROP chain does two things. First, it calls commit_creds(&init_cred), which replaces the current process’s credentials with the kernel’s initial credential structure – root, every capability, no restrictions. Second, it calls switch_task_namespaces() to swap the process’s namespace pointer to the initial namespace, the one the host kernel itself lives in. Container escaped. Host root achieved.
On Ubuntu 24.04, there’s an extra wrinkle. AppArmor has policies in recent Ubuntu builds that restrict unprivileged user namespace creation, which is a prerequisite for reaching the nftables code in the first place. Sieber found that aa-exec -p trinity -- unshare -Urmin /bin/sh before running the exploit is sufficient to bypass the restriction and spawn a shell in an unconfined context. The AppArmor bypass is not exotic; it uses a profile that exists on the system by default.

The Entry Point: Unprivileged User Namespaces
There’s no remote vector here. An attacker needs an existing low-privileged foothold – a service account, a compromised container, a shell via a web application exploit to reach the nftables code at all. The kernel feature that provides that reach is unprivileged user namespaces.
User namespaces let an ordinary user create an isolated environment where they appear to have root capabilities, but only within that namespace. The original intent is good: it enables sandboxing, rootless containers, tools like Podman and Bubblewrap. But the side effect is that unprivileged users gain access to kernel interfaces including nftables that were previously reachable only by root.
This is not a new tension. It’s been a persistent theme in Linux local privilege escalation research for years. The kernel’s attack surface expands significantly when unprivileged namespace creation is enabled. The default on most desktop distributions and many server configurations is to allow it.
There are two ways to think about this. One view is that the feature is necessary for modern container workflows and the security community needs to keep pushing for better kernel hardening. The other is that this default made the kernel’s footprint reachable to the wrong people in the first place, and that organizations without explicit operational need for it should have turned it off years ago.
Both views are right. The feature is genuinely useful. The default is also genuinely risky. Most deployments never audited it.
FuzzingLabs and the Independent Route
Exodus wasn’t alone. FuzzingLabs, a French security research firm, independently reproduced the vulnerability on RHEL 10 in April 2026, ahead of Pwn2Own Berlin. Their route to root was different from Sieber’s – same bug, different exploitation path which tells you something about the flexibility of the primitive.
Use-after-free vulnerabilities in the kernel are not single-solution puzzles. Once you have a reliable use-after-free at the right allocation size, skilled researchers can generally find multiple routes to privilege escalation depending on the target’s allocator behavior, the kernel version, and what structures they can spray into the freed region. The fact that two independent teams found different working paths to root from the same bug means defenders can’t assume a specific mitigation that blocks one path also blocks the other.
The timeline here deserves a moment of attention. The patch shipped February 5. FuzzingLabs published a full working root exploit April 16. Exodus published its detailed write-up June 8. That’s four months of public exploit availability during which unpatched systems remained exposed. The gap between upstream patch and broad distribution-level adoption has always existed, but the pace at which exploit code follows patches has compressed sharply.
The Surge Context
CVE-2026-23111 isn’t a one-off. It arrived in the middle of what Synacktiv has called a surge in Linux local privilege escalation disclosures, and the pace is worth sitting with.
Recent months have brought Copy Fail, which exploited a flaw in the kernel’s copy-on-write handling. Dirty Frag, a heap fragmentation technique chained into an LPE. Fragnesia, a variant of the same approach targeting different allocation patterns. DirtyDecrypt, which leveraged memory decryption paths. And a nine-year-old ptrace vulnerability that let attackers read /etc/shadow and run commands as root on systems that had shipped a flawed kernel since 2017 and never been patched.
The techniques differ. The target subsystems differ. What doesn’t differ is the pattern: unprivileged foothold in, root on the host out. The threat model for any multi-user Linux system or any system where untrusted workloads run with limited privileges has to account for this class of attack as a nearly routine step in post-exploitation, not an exceptional one.
Synacktiv’s analysis of the LPE surge attributes part of the acceleration to AI-assisted research and faster patch-diffing tooling. Patch-diffing is the technique of comparing a security patch to the code it replaced and working backward to understand what the vulnerability was before the fix is publicly documented. As that analysis gets faster, the window between patch publication and working public exploit shrinks. The researchers publishing these exploits are not waiting months. They’re working in weeks.
What’s Exposed and What Mitigates It
The distribution picture is straightforward. Debian Bookworm and Trixie were both vulnerable in the configurations tested by Exodus. Ubuntu 22.04 LTS and 24.04 LTS were both confirmed vulnerable. RHEL 10 was confirmed vulnerable by FuzzingLabs. Because the bug is in the mainline kernel, any distribution that shipped a kernel with nftables enabled and unprivileged user namespace creation allowed is exposed, absent a distro-level patch.
Fixes are available now. Ubuntu has patched 22.04, 24.04, and 25.10. Debian has patched Bookworm and Trixie, with a 6.1 backport for Bullseye LTS. Red Hat, SUSE, and Amazon Linux are all tracking the flaw. The exact fixed kernel package version varies by distribution and release – check your vendor’s security advisory for the specific version that applies to your setup, then update and reboot. The reboot is not optional. Kernel patches don’t apply to a running kernel.
If you cannot reboot immediately, there are two partial mitigations worth knowing.
On Ubuntu systems, setting kernel.unprivileged_userns_clone=0 via sysctl blocks the creation of unprivileged user namespaces. This severs the path to the vulnerable nftables code for ordinary users. It also breaks things that depend on the feature – rootless container tools, certain sandboxes, some snap packages. Know what you’re breaking before you flip it in production.
The second option, applicable more broadly, is to block nf_tables module loading via a modprobe blacklist. This removes nftables from the kernel’s available subsystems entirely. It will break firewall tooling that depends on nftables rather than iptables. Check your firewall stack before doing this.
Neither of these is a substitute for patching. They buy time. The patch is the answer.
The Bigger Problem Underneath
Here’s what I keep coming back to when I look at this run of Linux LPEs.
Each individual bug has an individual fix. Patch the kernel, the specific flaw goes away. But the meta-problem doesn’t. The meta-problem is that the kernel’s attack surface from an unprivileged user context is enormous, the audit lag between code being written and code being read by hostile researchers is shrinking, and the standard deployment assumption that a low-privileged user or container is meaningfully isolated is regularly being proven wrong by public exploit code.
The Synacktiv observation about defense in depth is the right frame. Ordinary hardening still matters. Restricting unprivileged namespace creation reduces exposure. Running workloads as non-root with minimal capabilities reduces exposure. Enabling SELinux or AppArmor profiles reduces exposure. Maintaining a short patch cycle reduces exposure. None of these individually close every door. Together, they raise the cost of exploitation to the point where most opportunistic attackers move on.
The Synacktiv observation about defense in depth is the right frame. Ordinary hardening still matters. Restricting unprivileged namespace creation reduces exposure. Running workloads as non-root with minimal capabilities reduces exposure. Enabling SELinux or AppArmor profiles reduces exposure. Maintaining a short patch cycle reduces exposure. None of these individually close every door. Together, they raise the cost of exploitation to the point where most opportunistic attackers move on.
The issue is that most organizations are not doing all of these things, or they’re doing them inconsistently. Container defaults that looked fine two years ago turn out to assume a threat model the actual vulnerability landscape doesn’t support. The kernel keeps shipping features that expand the unprivileged attack surface in exchange for genuinely useful capabilities, and the security community keeps finding the gaps that expansion creates.
There are no reports of CVE-2026-23111 being exploited in the wild. There’s no known threat actor associated with it. That’s good news. It also reflects the fact that threat actors don’t always announce themselves, and “no reported exploitation” and “no exploitation” are not the same statement.
The patch has been available since February. Working exploit code has been available since April. If your systems still run a vulnerable kernel, the question isn’t whether the flaw is dangerous. It’s why you haven’t patched yet, and what the answer to that question reveals about how you’ll handle the next one.








