Hi folks,

Emailing here now that the embargo agreed upon with linux-distros@ has
expired.

Flagging four local root vulnerabilities in the Linux kernel, originally
reported to [email protected] and the relevant maintainers in
mid-July:

  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 either unprivileged user namespaces or specific CAPs
(see below); DiagSpill does not.

The corruption bugs in DirtyAH6 and DiagSpill are remotely reachable
under _very specific circumstances_, with DirtyAH6 theoretically
(unlikely/with difficulty) remote-groomable to remote root.

The fixes are now public and have been landing in the relevant stable
trees over the past several weeks:

DirtyAH6:
  7bad4bda74dc4713f398d3b7624ff05478e3a568
  ("xfrm: ah6: validate routing header segments_left")

TUNderflow:
  447c9303942c439a117d9b76ce6d6e2116b38ee7
  ("net: tun: bound receive headroom")

PPPoEject:
  e9c238f6fe42fb1b4dba3a578277de32cb487937
  ("pppoe: reload header pointer after dev_hard_header()")

DiagSpill:
  bd0e9289e2642f6a5c54faad304ce0f41e926d22
  ("sctp: prevent peer transport count overflow")

The first upstream stable releases containing all four fixes are
5.10.270, 5.15.221, 6.1.188, 6.6.157, 6.12.109, 6.18.50, and 7.2.4.

LPE impact:

  All four PoCs provide unprivileged local user -> root code execution
  on their targets.

  The pre-reqs listed below are for the vulns' _corruption bugs
  specifically_; the full LPE-specific requirements, including
  additional kmods and various userspace tooling, are captured in the
  PoCs' READMEs.

  DirtyAH6 pre-reqs:
    - AH6/XFRM support
    - unprivileged user/network namespaces, or CAP_NET_ADMIN and
      CAP_NET_RAW over an attacker-controlled network namespace

  TUNderflow pre-reqs:
    - TUN support and some network-device path that can propagate
      oversized receive headroom to it
    - unprivileged user/network namespaces, or CAP_NET_ADMIN over an
      attacker-controlled network namespace

  PPPoEject pre-reqs:
    - PPPoE support and a lower-device header callback that can
      reallocate the skb head during dev_hard_header()
    - unprivileged user/network namespaces, or CAP_NET_ADMIN over an
      attacker-controlled network namespace

  DiagSpill pre-reqs:
    - SCTP and sctp_diag support (no unprivileged user namespace or CAP
      requirements)

These pre-reqs are not exotic -- they cover standard modules/paths
commonly available on regular distro kernels.

AppArmor (except for the Ubuntu cases of blocking unprivileged user
namespaces themselves)/SELinux do not block the exploits in my testing.
All four bugs can also corrupt the host kernel from a container; the
first three would require the appropriate CAPs (without creating new
user namespaces), while DiagSpill would work without any special CAPs
so long as SCTP and sctp_diag are available. This could theoretically
enable a container escape, though I didn't pursue this angle with the
PoCs.

Remote impact:
  DirtyAH6:
    If the target is acting as an IPv6 router/gateway and adds AH in
    transport mode, the bug can be turned into a remote crash/DoS.
    With on-target memory grooming, I was able to turn it into remote
    root in a lab environment.

    Remote-only grooming to root is theoretically possible, but looks
    extremely difficult.

  DiagSpill:
    If ASCONF/ADD-IP are enabled with either SCTP-AUTH or
    net.sctp.addip_noauth_enable=1 (all disabled by default), a
    malicious peer can add enough transports to wrap transport_count.
    Something on the target (e.g., ss) must still issue the sock_diag
    request that triggers the overwrite. This can be turned into a
    remote crash/DoS; I do not see a path to full remote root, even
    assuming perfect remote memory grooming.

Bug:
  DirtyAH6:
    ipv6_rearrange_rthdr() would get the number of addresses from
    hdrlen, then use segments - segments_left to move an address
    pointer without checking that segments_left <= segments. A raw
    IPv6 HDRINCL packet with hdrlen=2 and segments_left=255 moved the
    pointer back 4,064 bytes and passed a 4,064-byte length to
    memmove(), causing an out-of-bounds access.

  TUNderflow:
    tun_set_headroom() stored receive headroom directly in tun->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 VXLAN and Open vSwitch, could pass 4,160 bytes to
    a raw TUN port. SKB_MAX_HEAD(4160) would underflow; prepad + linear
    and len - linear then wrapped, leaving skb->data 64 bytes beyond
    its 4,096-byte allocation.

  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 and the 65,536th transport wrapped it
    to zero. sctp_diag then reserved no peer payload but copied the full
    list, spilling about 8 MiB past the end of the Netlink response.

Affected upstream kernels:

  The versions below are, again, for the underlying bugs, not the
  full-root PoCs, which have some additional dependencies:

  DirtyAH6:
    2.6.12–5.9: all releases
    5.10.0–5.10.268; 5.11–5.14: all releases
    5.15.0–5.15.219; 5.16–6.0: all releases
    6.1.0–6.1.186; 6.2–6.5: all releases
    6.6.0–6.6.155; 6.7–6.11: all releases
    6.12.0–6.12.107; 6.13–6.17: all releases
    6.18.0–6.18.48; 6.19–7.0: all releases
    7.1.0–7.1.12; 7.2.0–7.2.2

  TUNderflow:
    4.6–5.9: all releases
    5.10.0–5.10.269; 5.11–5.14: all releases
    5.15.0–5.15.220; 5.16–6.0: all releases
    6.1.0–6.1.187; 6.2–6.5: all releases
    6.6.0–6.6.156; 6.7–6.11: all releases
    6.12.0–6.12.108; 6.13–6.17: all releases
    6.18.0–6.18.49; 6.19–7.0: all releases
    7.1.0–7.1.13; 7.2.0–7.2.3

  PPPoEject:
    2.6.12–5.9: all releases
    5.10.0–5.10.264; 5.11–5.14: all releases
    5.15.0–5.15.215; 5.16–6.0: all releases
    6.1.0–6.1.182; 6.2–6.5: all releases
    6.6.0–6.6.147; 6.7–6.11: all releases
    6.12.0–6.12.100; 6.13–6.17: all releases
    6.18.0–6.18.41; 6.19–7.0: all releases
    7.1.0–7.1.5

  DiagSpill:
    4.7–5.9: all releases
    5.10.0–5.10.264; 5.11–5.14: all releases
    5.15.0–5.15.215; 5.16–6.0: all releases
    6.1.0–6.1.182; 6.2–6.5: all releases
    6.6.0–6.6.150; 6.7–6.11: all releases
    6.12.0–6.12.102; 6.13–6.17: all releases
    6.18.0–6.18.43; 6.19–7.0: all releases
    7.1.0–7.1.7

Immediate-term mitigations (aside from using updated kernels):
  - Disabling unprivileged user namespaces removes the standard
    ordinary-user path to the first three vulns, though it doesn't
    protect against appropriately-CAP'd containers/other processes;
    DiagSpill would still remain reachable too.
  - Disable AH6, TUN, PPPoE, and/or SCTP/sctp_diag if unused.

Full writeup:
  https://heyitsas.im/posts/lpe-quartet/

PoCs for validation:

  https://github.com/manizada/DirtyAH6
  https://github.com/manizada/TUNderflow
  https://github.com/manizada/PPPoEject
  https://github.com/manizada/DiagSpill

  Note that while the LPEs reproduce across a range of distros/kernels,
  the PoCs are targeted at just a handful, as each target often requires
  custom derivation/grooming.

Thanks,
-Asim Manizada

Reply via email to