Both modules' detect() was precondition-only because we didn't know the
mainline fix commits at port time. Debian's security tracker now
provides them — pinning here turns detect() into a proper version-
based verdict (still with --active for empirical override).
dirtydecrypt (CVE-2026-31635):
- Fix commit a2567217ade970ecc458144b6be469bc015b23e5 in mainline 7.0
('rxrpc: fix oversized RESPONSE authenticator length check').
- Debian tracker confirms older stable branches (5.10 / 6.1 / 6.12) as
<not-affected, vulnerable code not present>: the rxgk RESPONSE-
handling code was added in 7.0.
- kernel_range table: { {7, 0, 0} }
- detect() pre-checks 'kernel < 7.0 -> SKELETONKEY_OK (predates)' then
consults the table. With --active, the /tmp sentinel probe overrides
empirically (catches pre-fix 7.0-rc kernels the version check
reports as patched).
fragnesia (CVE-2026-46300):
- Fix in mainline 7.0.9 per Debian tracker ('linux unstable: 7.0.9-1
fixed'). Older Debian-stable branches (bullseye 5.10 / bookworm 6.1
/ trixie 6.12) are still marked vulnerable as of 2026-05-22 - no
backports yet.
- kernel_range table: { {7, 0, 9} }
- detect() keeps the userns + carrier preconditions, then consults
the table: 7.0.9+ -> OK; older branches without an explicit backport
entry -> VULNERABLE (version-only). --active confirms empirically.
- Table is intentionally minimal so distros that DO backport in the
future flow into 'patched' once their branch lands an entry; until
then, the conservative VULNERABLE verdict on unfixed branches is
correct.
Other changes:
- module struct .kernel_range strings updated from 'fix commit not
yet pinned' to the actual pinned-version prose.
- module_safety_rank bumped 86 -> 87 for both modules (version-pinned
detect is now real; still below the verified copy_fail family at
88 so --auto prefers verified modules when both apply).
- Both modules now #include core/kernel_range.h inside their
#ifdef __linux__ block.
- MODULE.md verification-status sections rewritten: detect() is now
version-pinned; only the exploit body remains unverified.
- CVES.md note + inventory rows updated: dropped the 'precondition-
only' language for the pair; all three ported modules now have
pinned fix references.
- README ⚪ tier description + module list aligned to the new state.
Both detect()s smoke-tested in docker gcc:latest on kernel 6.12.76-
linuxkit: dirtydecrypt correctly reports OK ('predates the rxgk code
added in 7.0'); fragnesia + pack2theroot correctly report
PRECOND_FAIL (no userns / no D-Bus in container). Local macOS + Linux
builds both clean.
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).
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.
Previous staircase pattern was just trailing decoration — not real
key teeth. Redesigned the bit as a hanging rectangle with two
clearly-projecting notch-teeth on its right edge (the part that
engages a lock's wards). Switched to box-drawing chars for the bit
since they make sharper notches than 8/b/d glyphs; bow stays
ornate-ASCII style.
Bump 0.4.3 → 0.4.4.
Previous banner had a SKELETONKEY block-letter art that competed
with the skeleton-key drawing for visual attention. Simplified:
the key art is now the focal point, and SKELETONKEY is rendered
as plain spaced text below the drawing.
Slight refinement to the key art: bow is a bit larger (888 instead
of 88) to feel more substantial. Bit/teeth pattern unchanged.
Bump 0.4.2 → 0.4.3.
The v0.4.1 box-drawing key was minimalist — round bow, line shaft,
small bit. Replaced with a more detailed ornate skeleton-key
silhouette in the classic ASCII-art-of-keys tradition:
- Round bow with internal "hole" rendered via stylized 8/b/d/'
pattern (suggests the decorative loop you'd grip)
- Long shaft running right across the banner
- Bit at the end with a staircase notch pattern (the iconic
"key-tooth" descent showing the wards that engage the lock)
Same height as the previous banner. SKELETONKEY block letters
below unchanged.
Bump 0.4.1 → 0.4.2.
Replace the previous "circle + shaft + curl" silhouette (which read
more like a magnifying glass) with a proper skeleton-key anatomy:
- BOW: round decorative loop with center hole (the part you hold)
- SHAFT: long horizontal rod (= the body of the key)
- BIT: notched tooth hanging down from the shaft end (the part
that engages the lock — the iconic key-tooth profile)
Same change in skeletonkey.c BANNER and README.md.
Bump 0.4.0 → 0.4.1.