Skip to content
Lpe Quartet

Lpe Quartet

heyitsas.im September 18, 2026

TLDR: Four more Linux LPEs; two of the corruption bugs are reachable remotely under very specific circumstances, with one theoretically remote-groomable to remote root.

This will be shorter than usual because a) I’m under a time crunch and b) we are covering 4 vulnerabilities at once.

These were discovered by combining the graph-based tracking of security-relevant objects/properties used in CIFSwitch with the tooling to enable agents to think ‘geometrically’ the memory state, as seen in OVSwrap . See those posts’ Background sections for more info.

The harness design is captured, in broad strokes, in Getting LLMs Drunk to Find Remote Linux Kernel OOB Writes (and More) , though it’s evolved considerably since.

The vulnerabilities #

The vulnerabilities are DirtyAH6 ( CVE-2026-80844 ), TUNderflow ( CVE-2026-81000 ), PPPoEject ( CVE-2026-68121 ), and DiagSpill ( CVE-2026-74469 ).

The underlying bugs have been around for 10-21 years. The first three LPEs require unprivileged user namespaces*; DiagSpill does not . In some very specific circumstances, DirtyAH6 ’s and DiagSpill ’s corruption bugs are remotely exploitable (see below for further details).

I reported these to [email protected] mid-July, with the fixes landing over the past weeks. Per the coordinated embargo with linux-distros@ , we agreed to publish the writeup/PoCs on September 18, 6am UTC, so the affected system owners can prioritize and apply the patches. Huge thanks to the many maintainers involved in the patching/coordination process: Stefan Klassert, Xin Long, Paolo Abeni, Willem de Bruijn, Greg KH, and many others.

Some background on the kernel subsystems involved in each of the vulnerabilities:

DirtyAH6 : IPsec ’s Authentication Header ( AH ) checks that packet data has not changed. Linux implements its IPv6 side in AH6 , using the kernel’s XFRM code; before calculating or checking authentication data, AH6 changes some IPv6 fields into the expected form, including addresses in a routing header.

TUNderflow : TUN and TAP are virtual network devices that move packets between the kernel and userspace through /dev/net/tun . Network devices built on top of other devices can pass down the receive headroom they need through ndo_set_rx_headroom() , and Open vSwitch can carry that value from another port to a TUN or TAP port.

PPPoEject : PPPoE carries PPP sessions in Ethernet frames. On send, pppoe_sendmsg() builds an skb , copies in the payload, and asks the lower network device to create its hardware header before filling in the PPPoE header.

DiagSpill : An SCTP association can have many peer transports, one for each peer address. sctp_diag reports SCTP socket and peer information through sock_diag , building a Netlink with one sockaddr_storage for each transport.

Vulnerability details #

DirtyAH6 : ipv6_rearrange_rthdr() would get the number of addresses from hdrlen , then use segments - segments_left to move an address pointer without first checking that segments_left align , while tun_get_user() also used it to choose how much packet data to keep in the head. A netkit device with 4,096 bytes of configured headroom, under a VXLAN device and an Open vSwitch datapath, could pass 4,160 bytes to a raw TUN port. Then, SKB_MAX_HEAD(4160) would underflow. The negative good_linear value became a huge positive size_t ; prepad + linear and len - linear then wrapped, and tun_alloc_skb() left skb->data 64 bytes beyond its 4,096-byte allocation. The packet processing afterward would then read/write outside the skb head.

PPPoEject : pppoe_sendmsg() kept a pointer into the skb head across dev_hard_header() , even though a device callback could call pskb_expand_head() and free that head. Blocking the payload copy on FUSE while adding the first GRE / IP6GRE port to an empty team or bonding device triggered the reallocation, effectively ejecting the old skb head while PPPoE still held a pointer into it. The subsequent header and length writes used that stale pointer.

DiagSpill : An SCTP association can have 65,536 peer transports, but transport_count is 16 bits – so the 65,536th transport wrapped it to 0. sctp_diag then reserved no peer payload but copied the full list, spilling 8 MiB past the end of the Netlink response.

DirtyAH6 : Check segments_left before changing the header or the address pointer, and pass -EINVAL through the existing AH6 error paths:

TUNderflow : Bound the headroom stored by TUN to the one-page skb -head budget and the largest allowed 16-bit header offset, while leaving enough data in the head for the raw- TUN protocol byte or a complete TAP Ethernet header. Then make sure those bytes are present before using them:

PPPoEject : Reload the PPPoE header through the skb ’s network-header offset after device header creation; have pskb_expand_head() update that offset when it moves the head:

DiagSpill : Reject a new unique peer once transport_count == U16_MAX . The check happens after looking for an existing peer, so an address already on the list still returns its existing transport at the limit:

The corresponding changelogs for the first upstream-fixed releases are:

These are all memory bugs that require per-target grooming for to-root escalation, so the linked PoCs specify the distro/kernel/cpu/memory they’re tuned for (see the READMEs) – I’ve managed to repro across a range of distros with some tuning, but I’m releasing only the minimal couple-target-specific PoCs for simplicity.

While these are generally reliable on their targets, they may still corrupt the wrong memory, so run these in dedicated VMs/throwaway hosts only:

DirtyAH6 : The PoC is a cousin of DirtyFrag ’s ESP variant: it corrupts skb_shared_info via a separate AH6 routing-header memmove() OOB (not addressed by the DirtyFrag fix), then makes a later ESP decrypt write into a file-backed fragment, replacing pam_rootok.so with pam_permit.so , getting a root shell with su .

TUNderflow : The PoC arranges file-backed pipe buffers to the bad TUN packet. The Open vSwitch out-of-bounds write then sets PIPE_BUF_FLAG_CAN_MERGE on one of them, after which a pipe write replaces pam_rootok.so with pam_permit.so in /etc/pam.d/su , with su used again for the root shell.

PPPoEject : The PoC races populated fd tables into the freed skb head and uses the PPPoE writes to redirect one live entry to an attacker-built fake struct file . Closing that fd calls a controlled kernel callback, installs root credentials, and opens a root shell.

DiagSpill : The PoC grooms the sock_diag overwrite into page tables, uses the corrupted page tables to map host memory, finds and rewrites a credential object, installs a sudoers rule, and opens a root shell.

Are you affected? + Mitigation #

The bug reachability pre-reqs #

The below are the pre-reqs for the actual bugs , not full LPE chains (which are captured in full in the above-linked PoCs’ READMEs). Nevertheless, it’s a good idea to treat your kernel as vulnerable if it’s in the list of vulnerable versions below and has the relevant subsystems available:

DirtyAH6 : Reachable on the affected kernels as long as AH6 / XFRM support is available on the affected kernels below; unprivileged user namespaces* are required for the specific LPE PoC above.

TUNderflow : Reachable on the affected kernels if unprivileged user namespaces are enabled* and TUN / TAP support is available, with some network-device path that can propagate oversized receive headroom to it.

PPPoEject : Reachable on the affected kernels as long as unprivileged user namespaces are enabled* and PPPoE support is available, with a lower-device header callback that can reallocate the skb head during dev_hard_header() .

DiagSpill : Reachable on the affected kernels as long as SCTP and sctp_diag support is available (no unprivileged user namespace/CAP requirements).

AppArmor (except for the Ubuntu cases of blocking unprivileged user namespaces themselves)/SELinux do not block the exploits in my testing.

* A mouthful of a note: the DirtyAH6 corruption trigger requires CAP_NET_ADMIN and CAP_NET_RAW in the user namespace that owns the attacker-controlled network ns. The TUNderflow and PPPoEject corruption triggers require only CAP_NET_ADMIN in the same user ns. In an appropriately-CAP'd container or a process in general, these paths can corrupt the host kernel (possibly leading to a container escape, etc.) with just the specified CAPs, without creating new user namespaces. Unprivileged user namespaces are just the most common path.

And so while a container escape is theoretically possible with these primitives, just as with CIFSwitch (see a community example ) and OVSwrap , I did not pursue that direction with the PoC.

Upgrade to a kernel containing all four fixes. The first stable releases with all four are 5.10.270 , 5.15.221 , 6.1.188 , 6.6.157 , 6.12.109 , 6.18.50 , and 7.2.4 .

If patching is not possible, disabling unprivileged user namespaces removes the ordinary-user path to the first three (but doesn’t protect against appropriately-CAP’d containers/other processes) – DiagSpill remains reachable. You can also disable the relevant functionality (if unused) to cut out the underlying bug: AH6 , TUN , PPPoE , and SCTP / sctp_diag . I wouldn’t recommend disabling PoC-specific kmods only as a proper mitigation, as there could be other paths to root – as always, patching is much preferred.

Given some personal/professional developments, this likely concludes the AI-assisted vulnerability hunting experiment – at least in its public form – for some while. Thanks for riding along!