``` ____ _ _ _ ____ ___ _ _ / ___|| | | | / \ | _ \ / _ \| \ | | | | | |_| | / _ \ | |_) | | | | \| | | |___ | _ |/ ___ \| _ <| |_| | |\ | \____||_| |_/_/ \_\_| \_\\___/|_| \_| 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`](https://github.com/torvalds/linux/commit/31e62c2ebbfdc3fe3dbdf5e02c92a9dc67087a3a) (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 ```sh # 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 ```sh ./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-discovery `--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 seconds even when nothing matches. `--list-baits` is the read-only version — it enumerates the same candidates without firing the exploit. Useful for surveying which distros ship which baits. ### 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 `) | `/etc/shadow` | Most Debian, Ubuntu, Fedora | | `/usr/sbin/chage` | `/etc/shadow` | RHEL / Rocky / Alma family | | `/usr/bin/passwd` (`passwd -S `) | `/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 `31e62c2ebbfd` directly. - Disable `pidfd_getfd(2)` via seccomp on production hosts. - Remove the setuid bit from `chage` and `passwd` if you do not need unprivileged users to query password aging. - For containerized workloads, enabling `no_new_privs` on 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](https://github.com/0xdeadbeefnetwork/ssh-keysign-pwn). - 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() ║ ╚══════════════════════════════╝ ```