Preview-only mode for --auto / --exploit / --mitigate / --cleanup.
Walks the full scan (with active probes, fork isolation, verdict
table — everything the real --auto does) and prints what would be
launched, without ever calling the exploit/mitigate/cleanup callback.
Wiring:
- struct skeletonkey_ctx gains a 'dry_run' field (core/module.h).
- Long option --dry-run, getopt case 10.
- cmd_auto: after picking the safest, if dry_run, print
[*] auto: --dry-run: would launch `--exploit <NAME> --i-know`; not firing.
plus the remaining ranked candidates, then return 0.
- cmd_one (used for --exploit/--mitigate/--cleanup) shorts on dry_run
with [*] <module>: --dry-run: would run --<op>; not firing.
UX: --auto --dry-run does NOT require --i-know (nothing fires). The
refusal message for bare --auto now points to --dry-run for the
preview path:
[-] --auto requires --i-know (or --dry-run for a preview that never fires).
ROADMAP --auto accuracy section updated with the dry-run + the
version-pinned detect work from the previous commit.
Smoke-tested locally on macOS: scanning runs, verdicts print, the
'would launch' line fires, exit 0.
Adds the third ported module — Pack2TheRoot, a userspace PackageKit
D-Bus TOCTOU LPE — and spends real effort hardening --auto so its
detect step gives an accurate, robust verdict before deploying.
pack2theroot (CVE-2026-41651):
- Ported from the public Vozec PoC
(github.com/Vozec/CVE-2026-41651). Original disclosure by the
Deutsche Telekom security team.
- Two back-to-back InstallFiles D-Bus calls (SIMULATE then NONE)
overwrite the cached transaction flags between polkit auth and
dispatch. GLib priority ordering makes the overwrite deterministic,
not a timing race; postinst of the malicious .deb drops a SUID bash
in /tmp.
- detect() reads PackageKit's VersionMajor/Minor/Micro directly over
D-Bus and compares against the pinned fix release 1.3.5 (commit
76cfb675). This is a high-confidence verdict, not precondition-only.
- Debian-family only (PoC builds its own .deb in pure C; ar/ustar/
gzip-stored inline). Cleanup removes /tmp .debs + best-effort
unlinks /tmp/.suid_bash + sudo -n dpkg -r the staging packages.
- Adds an optional GLib/GIO build dependency. The top-level Makefile
autodetects via `pkg-config gio-2.0`; when absent the module
compiles as a stub returning PRECOND_FAIL.
- Embedded auditd + sigma rules cover the file-side footprint
(/tmp/.suid_bash, /tmp/.pk-*.deb, non-root dpkg/apt execve).
--auto accuracy improvements:
- Auto-enables --active before the scan. Per-module sentinel probes
(page-cache /tmp files, fork-isolated namespace mounts) turn
version-only checks into definitive verdicts, so silent distro
backports don't fool the scan and --auto won't pick blind on
TEST_ERROR.
- Per-module verdict printing — every module's result is shown
(VULNERABLE / patched / precondition / indeterminate), not just
VULNERABLE rows. Operator sees the full picture.
- Scan-end summary line: "N vulnerable, M patched/n.a., K
precondition-fail, L indeterminate" with a separate callout when
modules crashed.
- Distro fingerprint added to the auto banner (ID + VERSION_ID from
/etc/os-release alongside kernel/arch).
- Fork-isolated detect() — each detector runs in a child process so
a SIGILL/SIGSEGV in one module's probe is contained and the scan
continues. Surfaced live while testing: entrybleed's prefetchnta
KASLR sweep SIGILLs on emulated CPUs (linuxkit on darwin); without
isolation the whole --auto died at module 7 of 31. With isolation
the scan reports "detect() crashed (signal 4) — continuing" and
finishes cleanly.
module_safety_rank additions:
- pack2theroot: 95 (userspace D-Bus TOCTOU; dpkg + /tmp SUID footprint
— clean but heavier than pwnkit's gconv-modules-only path).
- dirtydecrypt / fragnesia: 86 (page-cache writes; one step below the
verified copy_fail/dirty_frag family at 88 to prefer verified
modules when both apply).
Docs:
- README badge / tagline / tier table / ⚪ block / example output /
v0.5.0 status — all updated to "28 verified + 3 ported".
- CVES.md counts line, the ported-modules note (now calling out
pack2theroot's high-confidence detect vs. precondition-only for
the page-cache pair), inventory row, operations table row.
- ROADMAP Phase 7+: pack2theroot moved out of carry-overs into the
"landed (ported, pending VM verification)" group; added a new
"--auto accuracy work" subsection documenting the dispatcher
hardening landed in this commit.
- docs/index.html: scanning-count example bumped to 31, status line
updated to mention 3 ported modules.
Build verification: full `make clean && make` in `docker gcc:latest`
with libglib2.0-dev installed: links into a 31-module skeletonkey
ELF (413KB), `--list` shows all modules including pack2theroot,
`--detect-rules --format=auditd` emits the new pack2theroot section,
`--auto --i-know --no-shell` exercises the new banner + active
probes + verdict table + fork isolation + scan summary end-to-end.
Only build warning is the pre-existing
`-Wunterminated-string-initialization` in dirty_pipe (not introduced
here).
Three-agent rigorous review of the dirtydecrypt + fragnesia ports plus
repo-wide doc consistency, followed by a full Linux build verification.
dirtydecrypt (NOTICE + detection rules):
- NOTICE.md: removed an unsupported "Zellic co-founder" detail and a
fabricated disclosure-date narrative; tightened phrasing of the
Zellic + V12 credit; noted that upstream poc.c carries no
author/license header of its own.
- Embedded auditd + sigma rules and detect/sigma.yml broadened to
cover every binary in dd_targets[] (added /usr/bin/mount,
/usr/bin/passwd, /usr/bin/chsh) and added the b32 splice rule, so
the embedded ruleset matches the on-disk reference and the carrier
list the exploit actually targets.
- Exploit primitive verified byte-for-byte against the V12 PoC
(tiny_elf[] identical, all rxgk/XDR/fire/pagecache_write logic
token-identical). docker gcc:latest compile of the Linux path:
COMPILE_OK, zero warnings.
fragnesia: review found no defects. Exploit primitive byte-identical
to the V12 PoC (shell_elf[] 192 bytes identical, AF_ALG GCM keystream
table + userns/netns/XFRM + receiver/sender/run_trigger_pair all
faithful). The deliberate omissions (ANSI TUI, CLI arg parsing) drop
nothing exploit-critical. docker gcc:latest compile: COMPILE_OK; full
project build links into a working skeletonkey ELF and --list shows
the module registered correctly.
Repo docs (README.md / CVES.md / ROADMAP.md):
- Chose to keep "28 verified" as the headline; the two ported
modules are represented as a separate clearly-labelled tier
("ported-but-unverified") that is explicitly excluded from the
28-module verified counts. README + CVES.md + ROADMAP.md now tell
one consistent story.
- Filled a pre-existing documentation gap: sudo_samedit, sequoia,
sudoedit_editor, vmwgfx were registered + built but absent from
CVES.md's inventory + operations tables. Added rows synthesized
from each module's .cve / .summary / .kernel_range fields.
- ROADMAP Phase 8 "7 🟡 PRIMITIVE modules" → "14"; added a "Landed
since v0.1.0" group; moved vmwgfx out of the stale carry-overs.
docs site (docs/index.html):
- Stat box "28 / total modules" → "28 / verified modules" (the 14+14
breakdown now sums to the headline consistently).
- Terminal example "scanning 28 modules" → "scanning 30 modules"
(was factually wrong — the binary literally prints module_count()
which is 30).
- Status line: updated to mention the 2 ported-but-unverified
modules and mirror the README phrasing.
- docs/LAUNCH.md left as a dated v0.5.0 launch snapshot.
Build verification: `docker run gcc:latest make clean && make` —
links into a 30-module skeletonkey ELF on Linux. macOS dev box still
hits the pre-existing dirty_pipe header gap; unchanged.
.gitignore: added /skeletonkey to exclude the top-level build
artifact (the existing modules/*/skeletonkey only covered per-module
binaries; the root one was getting picked up by `git add -A`).
Two new page-cache-write LPE modules, both ported from the public V12
security PoCs (github.com/v12-security/pocs):
- dirtydecrypt (CVE-2026-31635): rxgk missing-COW in-place decrypt.
rxgk_decrypt_skb() decrypts spliced page-cache pages before the HMAC
check, corrupting the page cache of a read-only file. Sibling of
Copy Fail / Dirty Frag in the rxrpc subsystem.
- fragnesia (CVE-2026-46300): XFRM ESP-in-TCP skb_try_coalesce() loses
the SHARED_FRAG marker, so the ESP-in-TCP receive path decrypts
page-cache pages in place. A latent bug exposed by the Dirty Frag
fix (f4c50a4034e6). Retires the old _stubs/fragnesia_TBD stub.
Both wrap the PoC exploit primitive in the skeletonkey_module
interface: detect/exploit/cleanup, an --active /tmp sentinel probe,
--no-shell support, and embedded auditd + sigma rules. The exploit
body runs in a forked child so the PoC's exit()/die() paths cannot
tear down the dispatcher. The fragnesia port drops the upstream PoC's
ANSI TUI (incompatible with a shared dispatcher); the exploit
mechanism is reproduced faithfully. Linux-only code is guarded with
#ifdef __linux__ so the modules still compile on non-Linux dev boxes.
VERIFICATION: ported, NOT yet validated end-to-end on a
vulnerable-kernel VM. The CVE fix commits are not pinned, so detect()
is precondition-only (PRECOND_FAIL / TEST_ERROR, never a blind
VULNERABLE) and --auto will not fire them unless --active confirms.
macOS stub-path compiles verified locally; the Linux exploit-path
build is covered by CI (build.yml, ubuntu) only. See each MODULE.md.
Wiring: core/registry.h, skeletonkey.c, Makefile, CVES.md, ROADMAP.md.
iamroot.c: bump IAMROOT_VERSION from 0.1.0-phase1 → 0.1.0
README.md: replace "bootstrap phase" status with v0.1.0 corpus
breakdown (13🟢 / 7🟡 across 2016→2026 timeline)
CVES.md: redefine 🟡 to mean "primitive fires + groom + witness,
stops short of cred-overwrite chain — refuses to claim
root unless empirically demonstrated"; flip 7 entries
from 🔵 → 🟡; add the two missing 🟢 entries
(cgroup_release_agent, overlayfs_setuid); extend the
operations matrix from 7 → 20 rows.
ROADMAP.md: mark all Phase-7 items landed; add Phase 8 covering
full-chain promotions (nf_tables / xtcompat / af_packet
prioritized — each has a public reference exploit;
IAMROOT's no-fabricated-offsets rule means each needs
an env-var offset table or System.map auto-resolve).
Build clean on Debian 6.12.86; iamroot --version reports 0.1.0.
dirty_pipe detect: active sentinel probe (Phase 1.5-ish improvement)
- New dirty_pipe_active_probe(): creates a /tmp probe file with known
sentinel bytes, fires the Dirty Pipe primitive against it, re-reads
via the page cache, returns true if the poisoning landed.
- detect() gated on ctx->active_probe: --scan does version-only check
(fast, no side effects); --scan --active fires the empirical probe
and overrides version inference with the empirical verdict. Catches
silent distro backports that don't bump uname() version.
- Three verdicts now distinguishable:
(a) version says patched, no active probe → 'patched (version-only)'
(b) version says vulnerable, --active fires + probe lands → CONFIRMED
(c) version says vulnerable, --active fires + probe blocked → 'likely
patched via distro backport'
- Probe is safe: only /tmp, no /etc/passwd.
nf_tables CVE-2024-1086 (detect-only, new module):
- Famous Notselwyn UAF in nft_verdict_init. Affects 5.14 ≤ K, fixed
mainline 6.8 with backports landing in 5.4.269 / 5.10.210 / 5.15.149
/ 6.1.74 / 6.6.13 / 6.7.2.
- detect() checks: kernel version range, AND unprivileged user_ns clone
availability (the exploit's reachability gate — kernel-vulnerable
but userns-locked-down hosts report PRECOND_FAIL, signalling that
the kernel still needs patching but unprivileged path is closed).
- Ships auditd + sigma detection rules: unshare(CLONE_NEWUSER) chained
with setresuid(0,0,0) on a previously-non-root process is the
exploit's canonical telltale.
- Full Notselwyn-style exploit (cross-cache UAF → arbitrary R/W → cred
overwrite or modprobe_path hijack) is the next commit.
9 modules total now. CVES.md and ROADMAP.md updated.
- copy_fail_family/iamroot_modules.c: two new bridge functions
- copy_fail_family_mitigate: calls existing mitigate_apply() which
blacklists algif_aead + esp4 + esp6 + rxrpc, sets
kernel.apparmor_restrict_unprivileged_userns=1, drops caches.
- copy_fail_family_cleanup: heuristic-routed cleanup. If the
mitigation conf file (/etc/modprobe.d/dirtyfail-mitigations.conf)
exists → mitigate_revert(). Otherwise → try_revert_passwd_page_cache()
to evict /etc/passwd from page cache.
- All 5 copy_fail_family modules' .mitigate and .cleanup fields now
point at these shared family-wide handlers (the mitigation is
family-wide, not per-CVE).
- dirty_pipe and entrybleed: no --mitigate offered (no canonical
patches / only-fix-is-upgrade). Documented in ROADMAP.
Verified end-to-end on kctf-mgr as non-root user:
iamroot --mitigate copy_fail → 'mitigate requires root' (correct)
iamroot --cleanup copy_fail → 'no mitigation conf; evicting page cache'
CVES.md gains a per-module ops table; ROADMAP.md marks Phase 6 partial.
- .github/workflows/build.yml: matrix of {gcc, clang} x {default,
debug} builds on every push + PR. Smoke tests after build:
--version, --list, --scan, --detect-rules auditd, --detect-rules
sigma. Build failure breaks merge gate.
- Static-build job runs continue-on-error (glibc + NSS issue with
static linking — getpwnam pulls in NSS at runtime; legacy DIRTYFAIL
Makefile noted this. Revisit with musl-gcc to get a truly portable
static binary).
- Kernel-VM matrix placeholder commented at the bottom of build.yml.
Real kernel matrix needs self-hosted runners or a paid VM service —
out of scope for tonight, in scope for Phase 4 followup.
- Implements the Dirty Pipe primitive: prepare_pipe() fills+drains a
pipe to plant the stale PIPE_BUF_FLAG_CAN_MERGE flag in every
pipe_buffer slot; dirty_pipe_write() splices 1 byte from the target
file at offset-1 (seeding the slot with the file's page) then write()s
the payload, which the buggy kernel merges back into the page cache.
- find_passwd_uid_field() + revert_passwd_page_cache() inlined in the
module. Two-of-two duplication acceptable; extraction into core/host
triggers when a third module needs the same helpers (Phase 1.5).
- dirty_pipe_exploit() resolves current euid via getpwuid, locates the
user's UID field in /etc/passwd, replaces it with same-length zeros
('0000' for a 4-digit UID), then execlp's su <user> -c /bin/sh.
Auto-refuses if detect() reports patched. --no-shell mode plants the
write and returns. Cleanup mode evicts /etc/passwd from page cache.
- _GNU_SOURCE redefine warning fixed: cmdline -D already passes it.
Verified end-to-end on kernel 6.12.86 (patched):
iamroot --scan → dirty_pipe reports OK (patched)
iamroot --exploit dirty_pipe --i-know → refuses cleanly
CI-validation against vulnerable kernel (Ubuntu 20.04 / 5.13) is Phase 4.
CVES.md: dirty_pipe 🔵 → 🟢. ROADMAP.md: Phase 2 marked complete.
- modules/entrybleed_cve_2023_0458/ (promoted out of _stubs):
- iamroot_modules.{c,h}: full EntryBleed primitive (rdtsc_start/end
+ prefetchnta + KASLR-slot timing sweep) wired into the standard
iamroot_module interface. x86_64 only; ARM/other gracefully
return IAMROOT_PRECOND_FAIL.
- detect(): reads /sys/.../vulnerabilities/meltdown to decide
KPTI status. Mitigation: PTI → VULNERABLE. Not affected → OK.
- exploit(): sweeps the 16MiB KASLR range, prints leaked kbase
(and KASLR slide). JSON-mode emits {"kbase":"0x..."} to stdout.
- entrybleed_leak_kbase_lib(off) declared as a public library
helper so future LPE chains needing a stage-1 leak can just
#include the module's header and call it.
- entry_SYSCALL_64 slot offset overridable via
IAMROOT_ENTRYBLEED_OFFSET (default 0x5600000 for lts-6.12.x).
- __always_inline fallback added since glibc/Linux-kernel macro
isn't universal; module now builds clean under macOS clangd lint
and on musl.
- iamroot.c registers entrybleed alongside the other families;
Makefile gains it as a separate object set.
Verified end-to-end on kctf-mgr (Debian 6.12.86):
iamroot --exploit entrybleed --i-know
→ [+] entrybleed: leaked kbase = 0xffffffff8d800000
This is the FIRST WORKING-EXPLOIT module in IAMROOT (5
copy_fail_family modules wrap existing code from DIRTYFAIL;
dirty_pipe is detect-only). EntryBleed is x86_64 stage-1 brick
that future chains can compose.
- core/kernel_range.{c,h}: branch-aware patched-version comparison.
Every future module needs 'is the host kernel in the affected
range?'; centralized here. Models stable-branch backports
(e.g. 5.10.102, 5.15.25) so a 5.15.20 host correctly reports
VULNERABLE while a 5.15.50 host reports OK.
- modules/dirty_pipe_cve_2022_0847/ (promoted out of _stubs):
- iamroot_modules.{c,h}: dirty_pipe module exposing detect() that
parses /proc/version and compares against the four known patched
branches (5.10.102, 5.15.25, 5.16.11, 5.17+ inherited). Returns
IAMROOT_OK / IAMROOT_VULNERABLE / IAMROOT_TEST_ERROR with stderr
hints in human-readable scan mode.
- exploit() returns IAMROOT_PRECOND_FAIL with a 'not yet
implemented' message; landing the actual exploit needs Phase 1.5
extraction of passwd/su helpers into core/.
- detect/auditd.rules: splice() syscall + passwd/shadow file watches
- detect/sigma.yml: non-root modification of /etc/passwd|shadow|sudoers
- iamroot.c main() calls iamroot_register_dirty_pipe() alongside
the copy_fail_family registration.
- Makefile gains the dirty_pipe family as a separate object set.
Verified end-to-end on kctf-mgr (kernel 6.12.86): build clean, 6
modules in --list, --scan correctly reports dirty_pipe as patched,
JSON output ingest-ready.