CHARON ferries file descriptors out of dying SUID/SGID processes through the __ptrace_may_access mm==NULL window in do_exit(), disclosed by Qualys 2026-05-15 (CVE-2026-46333). Default behavior: dump /etc/shadow to stdout, banner + progress on stderr. --quiet for pure-pipe output, --verbose for stats. Built-in lures cover Debian/Ubuntu (chage SGID-shadow), RHEL family (chage SUID-root), and ssh-keysign. Patched-kernel detection distinguishes "primitive fires but lure didn't open target" from "pidfd_getfd never succeeded → fix is in place". Pre-built 46KB musl-static binary included as charon-static.
5.9 KiB
____ _ _ _ ____ ___ _ _
/ ___|| | | | / \ | _ \ / _ \| \ | |
| | | |_| | / _ \ | |_) | | | | \| |
| |___ | _ |/ ___ \| _ <| |_| | |\ |
\____||_| |_/_/ \_\_| \_\\___/|_| \_|
ferries fds across the exit-mm() Styx
CVE-2026-46333 / Linux <= 6.12.89
"It is a fearful thing to fall into the hands of the living God." — Hebrews 10:31
What this is
A tight, dependency-free PoC for CVE-2026-46333: the
__ptrace_may_access mm==NULL bypass disclosed by Qualys on
2026-05-15. Charon 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(). Run it 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.
The bug, in 30 seconds
__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 skipped- file table still populated → fds still gettable
- creds reflect the post-
setreuid()drop → access check passes
pidfd_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 fix: 31e62c2ebbfd
(Linus 2026-05-14). As of 2026-05-15 the backport has not landed in
linux-6.12.y or linux-6.6.y stable.
Affected kernels
| Stable tree | Status |
|---|---|
| linux-6.12.y (≤ 6.12.89) | ❌ vulnerable |
| linux-6.6.y (pre-fix backport) | ❌ vulnerable |
| mainline ≥ 6.15-rc1 | ✅ patched (31e62c2ebbfd) |
| Distro | Kernel | Status (2026-05-15) |
|---|---|---|
| Debian trixie | 6.12.86+deb13 | ❌ |
| AlmaLinux 10.1 | 6.12.0-124.55.3 | ❌ |
| Ubuntu 26.04 | 7.0.0-15 | ⚠️ check |
| Fedora 44 | 7.0.4-200 | ⚠️ check |
The PR / rolling-status table will be updated as backports land.
Build
# 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.
Run
./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 lure)
./charon --help
Exit codes
| 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 |
Lures
Charon ships with four known SUID lures:
| 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 enabled |
Adding a lure is a 3-line edit to the lures[] array in charon.c.
Mitigations until your distro ships the backport
- Apply
31e62c2ebbfddirectly. - Disable
pidfd_getfd(2)via seccomp on production hosts. - Remove the setuid bit from
chageandpasswdif you do not need unprivileged users to query password aging. - For containerized workloads, enabling
no_new_privson the host blocks the primitive entirely — every "SUID" inside the container becomes inert, leaving Charon with no prey.
Not a kernelctf VRP candidate
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.
Provenance
- Bug discovered & disclosed by Qualys → oss-security 2026-05-15.
- Reference PoCs by @0xdeadbeefnetwork.
- Charon rewrites the lure-and-race loop into a single hardened binary, adds CLI ergonomics, patched-kernel auto-detection, and per-distro lure fallback.
License
Educational and authorized-defensive use only.
⛵ STYX ⛵
╔══════════════════════════════╗
║ do_exit(): ║
║ ├── exit_mm() ← task->mm ║
║ │ = NULL ║
║ ├── ... ← ferry ║
║ └── exit_files() ║
╚══════════════════════════════╝