Commit Graph

159 Commits

Author SHA1 Message Date
KaraZajac e3aa70f208 docs: make README accurate for v0.10.0; sync verifications.c to reality
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
README badge + accuracy pass:
- Add a "11 root-verified out-of-band" badge; the dynamic release badge now
  shows v0.10.0.
- Reflect the exploit-verification milestone: 11 modules confirmed landing uid=0
  out-of-band, four false-EXPLOIT_OK bugs fixed.
- Correct stale counts against the runtime's own generated footer:
    * empirically-verified 29 -> 31 (dirty_cow + sudo_host graduated this
      release; refluxfs's record was also missing from the binary)
    * not-yet-verified 12 -> 10
    * test harness 88 -> 148 (33 kernel_range + 115 detect)
    * KEV "13 of 40" -> "13 of 41" (matches docs/KEV_CROSSREF.md)
- Move sudo_samedit from the 🟡 opt-in-full-chain list to 🟢 lands-root
  (16 full-chain), since it now roots directly.

core/verifications.c: add the three real records the binary was missing
(refluxfs on Rocky 9.8, dirty_cow on mainline 4.8.0, sudo_host on Ubuntu
22.04). `skeletonkey --list` now reports "31 empirically verified", matching
the README. Build + unit harness green (33 + 115).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
v0.10.0
2026-07-24 00:25:49 -04:00
KaraZajac 56f9e4d0dc release: v0.10.0 — the exploit-verification release
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
release / build (arm64) (push) Waiting to run
release / build (x86_64) (push) Waiting to run
release / build (x86_64-static / musl) (push) Waiting to run
release / build (arm64-static / musl) (push) Waiting to run
release / release (push) Blocked by required conditions
Bumps 0.9.14 -> 0.10.0 (skeletonkey.c, README, docs/index.html) and prepends
v0.10.0 release notes.

Milestone: the corpus moved from detect-verified to out-of-band exploit-verified.
11 modules confirmed landing uid=0 in a VM (witnessed independently, never
self-reported); four modules that falsely reported EXPLOIT_OK without ever
getting root were fixed (pwnkit, ptrace_traceme, dirty_pipe, dirty_cow); a
full false-EXPLOIT_OK audit was closed (every success claim now backed by a real
out-of-band check). New/rewritten working exploits: ptrace_traceme, sudo_samedit,
overlayfs_setuid (libfuse), dirty_pipe, dirty_cow. Plus fixes to a systemic
userns uid_map bug, the kptr_restrict offset resolver, a su-over-pty hang, a
readback buffer overflow, and an old-header portability issue; overlayfs upgraded
to a direct uid=0 witness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-24 00:03:53 -04:00
KaraZajac af01d112a5 docs: netfilter_xtcompat (CVE-2021-22555) — empirical note on primitive difficulty
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Attempted the most tractable kernel primitive to gauge the frontier. Kernel
confirmed vulnerable (focal GA 5.4.0-26 and a provisioned mainline 5.8.0, both
pre the 5.4.0-77/5.8.0-53 fix). But Andy Nguyen's well-regarded single-file
public exploit consistently fails at STAGE 1 ("could not corrupt any primary
message") on both — it is tuned for Ubuntu's exact 5.8.0-48-generic slab config
(freelist randomisation / memcg kmem accounting / SLUB merge), which mainline
kernels don't match, and Ubuntu's EOL 5.8.0-48 HWE debs aren't readily
sourceable. Recorded in docs/EXPLOITED.md: kernel primitives are
config-and-version-specific exploit-dev — even a reference exploit needs its
exact target kernel image + per-target tuning + a full port, a per-primitive
multi-session effort. No code change; documentation of the empirical result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 23:49:28 -04:00
KaraZajac 73c7d09445 overlayfs: direct uid=0 witness; broaden false-OK audit (all OK sites clean)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
overlayfs previously claimed EXPLOIT_OK from a getxattr proxy (the persisted
security.capability xattr is the bug's signature) then exec-transferred the
cap'd payload. Tightened to a DIRECT witness: the payload now takes a proof-path
arg and, running as root (setuid(0) works because of the persisted cap), drops a
root-owned proof + setuid bash; the module forks it, then reports OK only after
stat() confirms the proof is root-owned. Re-verified on focal 5.4.0-26 (uid 1000
-> root-owned proof + -rwsr-xr-x root:root bash).

Broadened the false-EXPLOIT_OK audit from exec-transfer callers to EVERY
EXPLOIT_OK return site. The copy_fail_family root-pop (exploit_su.c) runs
verify_plant() — confirming the shellcode actually landed in the setuid binary's
page cache, reverting otherwise — before exec'ing it, so it's proxy-verified like
dirtydecrypt/fragnesia, not a blind exec-transfer. Conclusion recorded in
docs/EXPLOITED.md: no false-OKs remain beyond the four already fixed; every OK is
backed by a real out-of-band check (root-owned artifact stat, getxattr, passwd
grep, setuid(0) gate, or page-cache verify_plant).

Host build + unit harness green (33 + 115).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 23:37:57 -04:00
KaraZajac 65588599ff dirty_cow: verify end-to-end on 4.8.0; robust su helper (dirty_pipe too)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Verified the dirty_cow fix end-to-end on a genuinely Dirty-COW-vulnerable
mainline 4.8.0 kernel (installed the kernel.ubuntu.com 4.8.0 deb on a 16.04
image + virtio-rng for entropy). A standalone built verbatim from the module's
primitive + escalation raced root's password field, authenticated as root over
a pty, planted a root-owned setuid bash, and left /etc/passwd byte-identical.
(The full skeletonkey binary won't compile on xenial's 4.4-era uapi headers —
unrelated nft_* modules use newer kernel constants — so the verbatim standalone
stands in for --exploit dirty_cow there.)

Robustness fix (both dirty_cow AND dirty_pipe): the su-over-pty step now POLLS
for the password prompt and hard-caps at 20s. The previous fixed-delay write
raced su's prompt setup and HUNG on xenial; without a cap that would block the
revert and leave /etc/passwd poisoned. Now su can never hang the module, and the
revert always runs.

Also: netfilter_xtcompat now includes <linux/if.h> for IFNAMSIZ (ip_tables.h
doesn't pull it transitively on older kernel headers, e.g. Ubuntu 16.04) — a
real portability fix surfaced by building on xenial.

Host build + unit harness green (33 + 115).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 23:26:12 -04:00
KaraZajac 68dac6c063 dirty_cow: fix false-EXPLOIT_OK (same 3 bugs as dirty_pipe) + audit the corpus
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
False-EXPLOIT_OK audit: the dispatcher's run_callback_isolated reports OK when the
exploit execve()s (FD_CLOEXEC closes the result pipe -> parent reads EOF), so any
exploit whose main path execs a not-guaranteed-root target lies. Audited every
exec*-calling module (results in docs/EXPLOITED.md).

dirty_cow had the identical 3-bug pattern to the pre-fix dirty_pipe:
  1. raced the CALLER's UID field to "0000" then ran `su self` -> still needs the
     caller's password, never rooted anything;
  2. execlp'd su -> dispatcher's exec-transfer path reported a FALSE EXPLOIT_OK;
  3. reverted via drop_caches (needs root) -> left the running /etc/passwd
     corrupted when run unprivileged.
Plus a latent overflow: the success-check readback[16] was too small for a
payload > 16 bytes.

Fix (byte-for-byte the verified dirty_pipe technique): race ROOT's password field
to a known $6$ hash -> authenticate as root over a pty -> plant a root-owned proof
+ setuid bash -> revert by racing the original bytes back through the Dirty COW
primitive (no root / no drop_caches). Success judged only by the OOB artifact;
readback buffer bumped to 512. cleanup() re-reverts idempotently.

The escalation half is identical to dirty_pipe (verified end-to-end); the COW
primitive itself needs a pre-4.8.3 kernel to land, which no cached VM has, so on
patched hosts it now fails HONESTLY with no corruption. Unit harness green.

Other exec-transfer modules audited clean or already-fixed; two (overlayfs proxy,
sudoedit_editor unverified reporting) noted for tightening.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 23:01:54 -04:00
KaraZajac 678a37b2f5 dirty_pipe: fix 3 bugs — working CVE-2022-0847 exploit that lands real root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Verified out-of-band on a genuinely pre-fix mainline 5.16.0 kernel (installed
from kernel.ubuntu.com onto the jammy image; every cached cloud image was
pre-5.8 or backport-patched). `skeletonkey --exploit dirty_pipe` (uid 1000) ->
EXPLOIT_OK + a -rwsr-xr-x root:root bash, and /etc/passwd is byte-identical
afterward.

Three real bugs in the shipped module:
  1. Wrong escalation: it flipped the CALLER's UID field to "0000" and ran
     `su <self>`, which still demands the caller's password -> never rooted
     anything.
  2. False EXPLOIT_OK: it execlp'd su, so the dispatcher's "exec transferred ->
     clean exit = OK" path reported success even on su's auth failure.
  3. Dangerous revert: revert_passwd_page_cache() used drop_caches, which needs
     root -> as an unprivileged caller it left the running system's /etc/passwd
     page cache corrupted (broke sshd user resolution in testing).

Rewrite (AabyssZG-style, self-contained + verifying):
  - overwrite ROOT's password field with a known $6$ crypt hash via the Dirty
    Pipe primitive (the longer hash clobbers into following lines, transiently);
  - authenticate as root over a pty with the matching password (su reads the
    password from the controlling tty, not stdin);
  - plant a root-owned proof + setuid bash; judge success ONLY by stat()'ing the
    root-owned artifact;
  - revert the page cache by writing the saved original bytes back through the
    Dirty Pipe primitive itself — no root, no drop_caches, nothing persists.

cleanup() re-reverts idempotently. Unit harness green (33 + 115).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 22:50:39 -04:00
KaraZajac 82ba6e0d08 sudo_samedit: working CVE-2021-3156 (Baron Samedit) exploit — lands real root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The shipped module drove a structural sudoedit trigger with no heap-grooming
offsets and honestly reported EXPLOIT_FAIL. Ported blasty's proven technique:
the `sudoedit -s` set_cmnd() unescape overflow overwrites a glibc NSS
service_user, so the subsequent NSS lookup dlopen's an attacker-planted
libnss_X/'P0P_SH3LLZ_ .so.2' from CWD, whose constructor runs while sudo is
still root.

Implementation follows the corpus's runtime-compile pattern: build the NSS
payload on the target (unique -DSK_PROOF/-DSK_ROOTBASH), lay out the libnss_X/
dir, exec sudoedit with the crafted argv (AAAA..\ / \ / BBBB..\) and env
(null_stomp × "\\", "X/P0P_SH3LLZ_", padded LC_ALL) from that CWD, and verify
root by stat()'ing the root-owned artifacts — never self-report. Grooming
lengths are libc-family specific (Ubuntu 56/54/63/212, Debian 64/49/60/214);
a null_stomp_len ±8 sweep (blasty brute.sh's axis) is the fallback for drift.

Verified out-of-band on Ubuntu 18.04.0 / sudo 1.8.21p2 / libc-2.27, as uid 1000
(non-sudoer — the overflow precedes the sudoers/password check): `skeletonkey
--exploit sudo_samedit` -> EXPLOIT_OK + a -rwsr-xr-x root:root bash, primary
lengths landing first try. Gated on cc + sudoedit; cleanup() removes artifacts
and the scratch build dir. Unit harness green (33 + 115).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 22:22:11 -04:00
KaraZajac 635f7d2d24 ptrace_traceme: working CVE-2019-13272 exploit — lands real root (x86_64)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The bundled sequence had the mechanism backwards (it PTRACE_ATTACHed the
now-root parent) and never rooted anything. Replaced it with the proven Jann
Horn (Project Zero #1903) / bcoles technique:

  - a "middle" process execs setuid pkexec (euid 0 for a window);
  - its child spins until it sees middle is euid 0, calls PTRACE_TRACEME
    (recording middle's ROOT creds as ptracer_cred — the bug), then execs
    pkexec itself. The traced setuid exec is NOT degraded because ptracer_cred
    is root, so the child becomes real root, still traced;
  - staged execveat() self-re-exec injects the payload as root.

The staged self-re-exec needs the exploit to exist as its own binary with an
argv[0] stage dispatcher, so the proven PoC is embedded verbatim
(ptrace_helper_src.h — only spawn_shell() changed, to plant a root-owned proof
+ setuid bash instead of only an interactive shell), compiled on the target at
runtime with unique -DSK_PROOF/-DSK_ROOTBASH paths, run, and verified by
stat()'ing the root-owned artifacts (never self-report).

Verified out-of-band on Ubuntu 18.04.0 / 4.15.0-50: `skeletonkey --exploit
ptrace_traceme` as uid 1000 -> EXPLOIT_OK + a -rwsr-xr-x root:root bash.
Real-world precondition, honestly reported: pkexec must authorize an
auto-discovered implicit-active=yes helper, which needs an active local session
(desktop) or a permissive polkit policy; over inactive ssh it returns "Not
authorized" and the module reports EXPLOIT_FAIL with that diagnosis. Gated on a
C compiler + pkexec; x86_64 only (register-level injection). cleanup() removes
the artifacts. Unit harness green (33 + 115).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 22:14:19 -04:00
KaraZajac 1bdbe011b0 docs: nf_tables (CVE-2024-1086) kernel confirmed vulnerable; module gap scoped
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Ran the full methodology on the flagship kernel primitive:
- Built Notselwyn's public universal PoC on jammy 5.15.0-25 (below patched
  branch 5.15.149) and ran it: hit the deliberate post-root `kernel BUG at
  mm/slub.c:379` / panic — the cross-cache slab corruption fired. Kernel is
  genuinely exploitable.
- Diffed technique: the module is an honest trigger+groom SCAFFOLD (real
  NFT_GOTO+NFT_DROP double-free + msg_msg cg-96 groom) whose pipapo arb-write is
  FALLBACK-DEPTH (exact pipapo_elem layout is a documented TODO) -> honest
  EXPLOIT_FAIL. Notselwyn uses a heavier universal cross-cache -> dirty-pagetable
  technique (~2000 LOC, multi-file, static libnftnl/libmnl, no per-kernel
  offsets). Completing the module's full-chain is substantial dedicated
  exploit-dev, not a spot-the-bug fix.

The piece that was actually broken and is now fixed+pushed is the offset
resolver (cd9bea6): env-provided offsets were wiped under kptr_restrict, which
blocked the entire --full-chain path. Recorded in docs/EXPLOITED.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 21:55:39 -04:00
KaraZajac cd9bea6399 offsets: fix env override being wiped under kptr_restrict (unblocks all full-chains)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
skeletonkey_offsets_resolve() runs sources in priority order: env vars first,
then /proc/kallsyms. On any default host kallsyms returns all-zero addresses
(kptr_restrict), and parse_symfile treated "all zero" by UNCONDITIONALLY zeroing
modprobe_path/init_task — clobbering the values apply_env had just set from
SKELETONKEY_MODPROBE_PATH / SKELETONKEY_INIT_TASK. Net effect: the documented
env-var offset override silently did nothing, so every --full-chain kernel
primitive reported "offsets couldn't be resolved" even when correct offsets were
supplied. Now the all-zero path only clears fields it tagged OFFSETS_FROM_KALLSYMS
itself, preserving env (and table/System.map) values.

Verified on Ubuntu 22.04.0 / 5.15.0-25: with SKELETONKEY_MODPROBE_PATH set, the
resolver now reports "modprobe_path=0x... (env)", the modprobe_path finisher
engages, and nf_tables' pipapo arb-write fires. (nf_tables itself still returns
an honest EXPLOIT_FAIL: the reconstructed double-free arb-write doesn't reliably
land the write yet — recorded in docs/EXPLOITED.md.) This fix is the prerequisite
for verifying any of the ~13 primitive full-chains. Unit harness green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 21:41:02 -04:00
KaraZajac 6960d2076d docs: record sudo_host (CVE-2025-32462) confirmed landing root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
sudo_host works as shipped — no code change. VM-verified on Ubuntu 22.04.0 /
sudo 1.9.9-1ubuntu2: with a host-restricted sudoers rule (scoped to a host that
is neither the current hostname nor ALL) and a resolvable host name,
`sudo -h <host> <cmd>` runs as uid 0 (the -h/--host authorization-logic flaw).
Module confirmed EXPLOIT_OK with a real uid=0 witness and popped a root shell.
The only preconditions are environmental (the host-restricted rule, common with
fleet-wide/LDAP/SSSD sudoers, supplied via SKELETONKEY_SUDO_HOST) — not a bug.

VERIFICATIONS.jsonl + EXPLOITED.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 21:34:24 -04:00
KaraZajac 70972e0c9d sudoedit_editor: fix CVE-2023-22809 exploit to actually land root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The module built the right EDITOR="helper -- /etc/passwd" injection but failed
two ways, both now fixed (VM-verified root on Ubuntu 22.04.0 / sudo
1.9.9-1ubuntu2):

1. Writable-CWD guard: sudoedit refuses to edit any file whose parent dir is
   user-writable. The injected "--" is resolved relative to CWD, so running
   from home/tmp made sudoedit abort "--: editing files in a writable
   directory" before the editor ran. Fix: chdir("/") in the sudoedit child.

2. Wrong tmp copy: sudoedit hands the editor one tmp copy per file, named
   <basename>.XXXXXX. The helper wrote argv[argc-1] — the sudoers-authorized
   COVER file (e.g. motd), not the target. Fix: the helper now matches the
   target's basename prefix (passed via SKEL_TARGET) to pick the right copy.

Result: /etc/passwd gains a skel::0:0 row; `su skel` -> uid=0(root).
docs/EXPLOITED.md + VERIFICATIONS.jsonl updated. Unit harness green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 21:32:44 -04:00
KaraZajac 6edf78f765 overlayfs_setuid: working CVE-2023-0386 exploit (libfuse) — lands real root
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The shipped module used a bogus chown-the-merged-view technique that never
worked. Rewrote it as a faithful port of the public PoC (xkaneiki): a libfuse
filesystem exports a setuid-root /file (st_uid=0, mode 04777), mounted in the
init ns via the setuid fusermount helper; then unshare(USER|NS) + overlay with
that FUSE mount as lowerdir; open(merged/file, O_WRONLY) triggers copy-up which
materialises upper/file as a genuine setuid-root binary; the unprivileged parent
execs it for real root.

VM-verified landing real root on Ubuntu 22.04.0 / 5.15.0-25 (uid=0 witnessed
out-of-band: /tmp/skeletonkey-ovlsu-pwned shows uid=0(root), and the dropped
setuid /tmp/.suid_bash runs with euid=0).

Four things were each required and took isolation to find:
  1. Overlay refuses a userns-mounted FUSE lowerdir (ENOSYS) -> FUSE must be
     mounted in the init ns via fusermount (libfuse). A raw /dev/fuse server was
     tried and abandoned (fragile; destabilised the kernel on malformed INIT).
  2. fuse2 low-level API (fuse_mount/fuse_new/fuse_loop_mt, empty args); fuse_main
     advertises copy_file_range caps that ENOSYS at copy-up with no fallback.
  3. read_buf callback (copy-up splice read path).
  4. ioctl callback (copy-up's FS_IOC_GETFLAGS) — the last missing piece.

libfuse is linked conditionally via pkg-config (fuse/fuse3), matching the
pack2theroot+libglib precedent; without it the module stubs to PRECOND_FAIL.
Makefile detects it and adds $(OSU_LIBS) to the link. Module header + opsec
notes rewritten to the real technique; docs/EXPLOITED.md updated. 148-test unit
harness green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 21:26:13 -04:00
KaraZajac 3edad37184 docs: overlayfs_setuid debug outcome — kernel confirmed vulnerable, raw /dev/fuse fragile, use libfuse
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Confirmed CVE-2023-0386 vulnerable on jammy 5.15.0-25.25 empirically (upstream
xkaneiki libfuse PoC pops root). Isolated the raw /dev/fuse port's failures:
overlay refuses a userns-mounted FUSE lowerdir (ENOSYS) so FUSE must be mounted
in the init ns via the setuid fusermount3 helper; got the fd-passing handshake +
mount working, but the server EINVALs on read after FUSE_INIT (non-blocking fd,
needs poll) and the flaky raw server repeatedly wedged/rebooted the VM. Raw
protocol reimplementation is fragile and can destabilise the target -> use
libfuse (proven, matches pack2theroot conditional-lib precedent). Ledger updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 20:57:27 -04:00
KaraZajac 58b44ebc43 docs: record overlayfs_setuid (FUSE port needed) + sudoedit_editor findings
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
overlayfs_setuid (CVE-2023-0386) investigated on Ubuntu 22.04.0 / 5.15.0-25
(genuinely vulnerable): the non-FUSE chown copy-up yields upper/file owned by
uid 1000 (no escalation), and overlay refuses a userns-mounted FUSE lowerdir
with ENOSYS (plain overlay-in-userns works). A working exploit must mount FUSE
in the init ns via the setuid fusermount helper (fd-passing protocol) then
overlay in the userns — a substantial dedicated port. A raw /dev/fuse server
was written and reverted after hitting the ENOSYS wall.

sudoedit_editor (CVE-2023-22809) on sudo 1.9.9 returns EXPLOIT_FAIL — the
SUDO_EDITOR/-- arg injection reaches sudoedit's writable-dir guard; needs
target-file tuning and module debugging. Tractable next.

Ledger + VERIFICATIONS.jsonl updated. No code change (overlayfs_setuid reverted
to baseline; not committing a second non-working variant).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 20:34:58 -04:00
KaraZajac e01aa99ec6 af_packet2: fix uid_map (read uid/gid before unshare) — same bug as cgroup
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
af_packet2 read getuid()/getgid() AFTER unshare(CLONE_NEWUSER), so it wrote
"0 65534 1" to uid_map/gid_map (65534 = nobody, the initial unmapped id) and
the write was rejected EPERM — the userns-root mapping silently failed and the
CAP_NET_RAW primitive could not fire. Capture the outer uid/gid before unshare.

Audited every userns module for the pattern; only cgroup_release_agent (fixed
in 8c45b2b) and af_packet2 had it. Updated docs/EXPLOITED.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 20:18:03 -04:00
KaraZajac 8c45b2beb8 cgroup_release_agent: fix uid_map (read uid pre-unshare) + add CLONE_NEWCGROUP
Two real bugs found via VM verification (Ubuntu 20.04.0 / 5.4.0-26):

1. uid_map EPERM: the child captured getuid()/getgid() AFTER
   unshare(CLONE_NEWUSER), where they return 65534 (nobody, the initial
   unmapped id), so it wrote "0 65534 1" to uid_map — rejected with EPERM.
   Now the outer uid/gid are read BEFORE unshare, yielding the correct
   "0 <uid> 1" single-uid self-map. (overlayfs got this right, which is why
   it worked; this module didn't.)

2. mount EPERM: the unprivileged cgroup-v1 mount needs a private cgroup
   namespace. Added CLONE_NEWCGROUP (with a fallback) — mounting an unused v1
   controller then succeeds.

With both fixed the userns+cgroupns+mount setup is correct. Note: on a stock
systemd host every v1 controller is already mounted (its release_agent is
owned by init-root and unwritable from the userns) and creating a fresh named
hierarchy is refused, so a bare unprivileged user cannot own release_agent —
CVE-2022-0492 is reachable in a container context (CAP_SYS_ADMIN / an ownable
cgroup), consistent with the module's "host root from rootless container"
framing. Verified out-of-band; see docs/EXPLOITED.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 20:15:51 -04:00
KaraZajac 59cc2be065 ptrace_traceme: stop the false EXPLOIT_OK; add exploit-verification ledger
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
ptrace_traceme reported EXPLOIT_OK while obtaining no root on a genuinely
vulnerable host (Ubuntu 18.04.2 / 4.15.0-50). The bundled ptrace sequence is a
non-working placeholder — it PTRACE_ATTACHes to the tracer, which by then has
reparented to init, so the attach fails EPERM; the parent then execve'd the
setuid trigger and the dispatcher's exec-transfer path reported a false OK.

Now the parent fires the trigger in a grandchild, stays alive, and verifies
euid==0 before claiming success — otherwise honest EXPLOIT_FAIL with a pointer
to the real CVE-2019-13272 technique (Jann Horn / bcoles) that still needs
porting.

Adds docs/EXPLOITED.md: the exploit-verification ledger (root witnessed OUT OF
BAND, not from the module's self-report). Confirmed landing root: refluxfs,
overlayfs, pwnkit (after its gconv fix), sudo_runas_neg1. Needs PoC ports:
ptrace_traceme, overlayfs_setuid (CVE-2023-0386 FUSE copy-up), sudo_samedit
(heap), cgroup_release_agent. ~30 modules still exploit-untested.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 19:59:48 -04:00
KaraZajac 24b839eccf pwnkit: fix the gconv re-injection layout so the exploit actually lands root
The exploit reported EXPLOIT_OK but did NOT get root on a genuinely vulnerable
target (Ubuntu 20.04.0, polkit 0.105-26ubuntu1). pkexec printed "Cannot run
program pwnkit" + glibc "Could not open converter from UTF-8 to PWNKIT" and no
root shell was obtained — a false positive from the dispatcher's "execve
transferred → clean exit = OK" path.

Root cause: the module built workdir/pwnkit/{gconv-modules,PWNKIT.so} correctly
but omitted the two pieces that make the GCONV_PATH trick fire:
  1. a directory literally named "GCONV_PATH=." containing an executable
     "pwnkit", so pkexec's g_find_program_in_path() resolves argv[0] and
     RE-INJECTS GCONV_PATH=./pwnkit into the sanitised environment; and
  2. chdir(workdir) so the re-injected relative "./pwnkit" resolves to the
     gconv dir.
It also set an absolute GCONV_PATH=<workdir>/pwnkit directly in envp, which
pkexec strips (GCONV_PATH is on polkit's blacklist) — removed.

VM-verified after the fix on Ubuntu 20.04.0 / 5.4.0-26 (polkit 0.105-26ubuntu1):
unprivileged sk -> uid=0(root), confirmed out-of-band (wrote /root/, read
/etc/shadow). overlayfs (CVE-2021-3493) also confirmed landing root on the same
host; sudo_samedit returns an honest EXPLOIT_FAIL there (heap not landed).

Found via the new qemu exploit-verification sweep (root confirmed out-of-band,
not from the module's self-report).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 19:40:04 -04:00
KaraZajac c55adc1840 refluxfs: drop the redundant standalone verify harness
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
The own-files reachability harness tools/verify-vm/refluxfs_verify.c is
superseded by the module itself: the safe default --exploit does the same
own-files reachability confirmation, and --full-chain provides the stronger
end-to-end proof (actual root). Remove the standalone and repoint the three
doc references (MODULE.md, RELEASE_NOTES.md, targets.yaml) at the module's
full-chain verification. The 4/4 own-files measurement it produced is kept
as a historical data point, just without the now-deleted reproducer.

No code depended on it; 148-test unit harness still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 18:50:58 -04:00
KaraZajac 5c18b678a5 refluxfs: wire the --full-chain /etc/passwd root pop (🟡 trigger -> 🟢 full chain)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Promotes refluxfs (CVE-2026-64600) to a 🟢 full-chain module. VM-verified
end-to-end on Rocky Linux 9.8 / 5.14.0-687.10.1.el9_8.0.1.x86_64 under
qemu/KVM, unprivileged uid=1000, SELinux Enforcing:

  skeletonkey --exploit refluxfs --i-know --full-chain

reflink-clones /etc/passwd, races the CoW window (32 writers / 8 helpers),
strips root's password field on-disk (root -> root::, the public PoC's
technique), evicts the stale page cache, verifies via O_DIRECT and returns
EXPLOIT_OK. `su root` (empty password) then gives uid 0. 3/3 wins on a
private-extent target (1244/3716/7913 rounds, 4-30s).

Safety properties (this bug rewrites the block device permanently, unlike
the page-cache 🟢 modules):
  - Crafts the payload FIRST and refuses unless it can preserve both root
    and the invoking user's line; every other passwd line is kept
    byte-for-byte. A tail-truncating port drops sshd/nobody/the caller and
    bricks login (hit exactly this during development).
  - Backs /etc/passwd up before the race; restores on failure; cleanup()
    restores it (run as root after the pop).
  - Destructive path gated behind --full-chain. Plain --exploit / --auto
    run only the safe own-files trigger (EXPLOIT_FAIL), unchanged.

Exploitability constraint discovered during verification (NOT in the Qualys
writeup): the race only fires when the target's extent is PRIVATE going in.
The block starts at refcount 2 (target + attacker clone), the concurrent CoW
drops it to 1, and the stale writer reads "1 -> private". A file already
reflink-shared with a third file keeps a post-CoW refcount > 1 and is NOT
attackable via that target. Stock Rocky 9 ships /etc/passwd pre-shared and
was unattackable across ~41,000 rounds; rewriting it to a private extent
(byte-identical content, as any useradd/passwd/vipw does) made it fall in
~2,000 rounds. So the exploitable state is the normal administered state.

Also fixed a page-cache staleness bug in the win path: the overwrite
bypasses the target inode, so its clean cached pages are never invalidated
and a buffered read (getpwnam in `su`, or the module's own verify) would see
the OLD passwd. The module now issues POSIX_FADV_DONTNEED on a win and
verifies via O_DIRECT.

detect() --active gains a per-target extent-privacy check: it reports
whether /etc/passwd is private (attackable) or already-shared (not).

88-test unit harness still green (148 total). Docs updated: MODULE.md
(full-chain flow, private-extent precondition, verification tables),
NOTICE.md, CVES.md (🟢 + tier/ops tables), README (15 full-chain / 13
primitive; lands-root list), RELEASE_NOTES, targets.yaml.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 18:45:32 -04:00
KaraZajac e46a32f11e modules: add refluxfs (CVE-2026-64600, "RefluXFS" XFS reflink CoW ILOCK race)
build / build (clang / debug) (push) Waiting to run
build / build (clang / default) (push) Waiting to run
build / build (gcc / debug) (push) Waiting to run
build / build (gcc / default) (push) Waiting to run
build / sanitizers (ASan + UBSan) (push) Waiting to run
build / clang-tidy (push) Waiting to run
build / drift-check (CISA KEV + Debian tracker) (push) Waiting to run
build / static-build (push) Waiting to run
Adds the corpus's first XFS module and its first data-oriented kernel bug —
every other kernel entry corrupts memory; this one corrupts file contents.

xfs_direct_write_iomap_begin() reads the data-fork extent map under ILOCK,
then xfs_reflink_fill_cow_hole() drops ILOCK to wait for transaction log
space. On reacquiring it, the code re-queries the refcount btree at the
ORIGINAL imap->br_startblock and never re-reads the data fork. A second
O_DIRECT writer holding only IOLOCK completes a whole CoW cycle in that
window, so the first writer's stale mapping sees refcount 1, treats a
still-shared block as private, and writes to it in place — landing its data
on the reflink source file's on-disk blocks.

The primitive is an arbitrary overwrite of the on-disk contents of any
readable file, which has three consequences that drive the design:
  - No offsets, no ROP, no KASLR/SMEP/SMAP; SELinux, containers and seccomp
    are all irrelevant.
  - The victim's inode is never written, so mtime/ctime/size never change
    and nothing is logged — FIM and `-w /etc/passwd -p wa` cannot see it.
  - The change persists across reboots.

Introduced 4.11 (3c68d44a2b49); fixed 2f4acd0fcd86 (mainline 7.2-rc4,
merged 2026-07-16), stable backports 7.1.4 / 6.18.39 / 6.12.96. Exposure is
distro-shaped: RHEL/CentOS/Rocky/Alma/Oracle/CloudLinux 8-10, Fedora Server
>= 31 and Amazon Linux 2023 ship XFS+reflink by default.

detect() is not a pure version gate — reachability here is safely
observable, so it pairs the backport table with a real storage precondition
(writable XFS via statfs XFS_SUPER_MAGIC, deliberately not via a successful
FICLONE since btrfs implements that too and is unaffected). --active
confirms reflink via FICLONE; SKELETONKEY_XFS_ASSUME_REFLINK=1/0 overrides.
On rpm-family hosts it warns that vendors backport without bumping the
upstream version, so the verdict speaks only to the upstream base.

exploit() forks a child that works only in a private mkdtemp scratch dir on
two files it owns: it establishes a shared extent (FICLONE, corroborated by
FIEMAP_EXTENT_SHARED) plus an O_DIRECT gate, then races a hard-bounded
8 writers / 2 helpers / 16 rounds / 2s and stops, reading the donor back
with O_DIRECT. Deliberately under-driven, and it never clones or targets a
file it does not own — the /etc/passwd overwrite -> su -> root step is
documented but NOT bundled. Always returns EXPLOIT_FAIL.

Safety rank 55, far above bad_epoll (12) and ghostlock (11): a won race
corrupts 4 KiB of our own scratch file and cannot touch kernel memory, so
there is no oops/KASAN/panic path.

Detection inverts the usual advice. auditd/sigma anchor on ioctl request
0x40049409 (FICLONE, matched exactly) and openat O_DIRECT; falco adds the
cross-uid reflink condition; and the yara rule is genuinely the right tool
here, matching the on-disk artifact because FIM is structurally blind.

VM-VERIFIED 2026-07-23 — the corpus's first rpm-family verification, taking
the empirical count to 29 of 41 CVEs. Rocky Linux 9.8 /
5.14.0-687.10.1.el9_8.0.1.x86_64 under qemu/KVM, stock GenericCloud layout
with no provisioner changes (root is XFS with reflink=1 out of the box).
detect() -> VULNERABLE, --active FICLONE witness confirmed reflink, phase A
observed FIEMAP_EXTENT_SHARED on a real shared extent, scratch self-cleaned,
clean build on el9 gcc. The underlying bug was separately confirmed winnable
on that kernel via tools/verify-vm/refluxfs_verify.c at the public PoC's
parameters (32 writers / 8 helpers, 60s): 4/4 runs won, first divergence
after 69/114/170/494 rounds. The shipped under-driven trigger did NOT win in
its 2s budget on that same vulnerable kernel — intended behaviour, and
exactly why a non-win must never be read as "patched".

14 new detect() unit rows (148 tests total, 0 failures). Bumps to v0.9.14.

Credit: Qualys Threat Research Unit (blog by Saeed Abbasi; the technical
advisory credits model-assisted kernel analysis performed with Anthropic),
and the upstream XFS maintainers who fixed it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0118iUgHY44hdRtANgyCmu7y
2026-07-23 17:49:40 -04:00
Kara Zajac d466fbfdcb docs: cache-bust og:image to ?v=2 (force fresh card past Cloudflare/social cache)
build / build (clang / debug) (push) Has been cancelled
build / build (clang / default) (push) Has been cancelled
build / build (gcc / debug) (push) Has been cancelled
build / build (gcc / default) (push) Has been cancelled
build / sanitizers (ASan + UBSan) (push) Has been cancelled
build / clang-tidy (push) Has been cancelled
build / drift-check (CISA KEV + Debian tracker) (push) Has been cancelled
build / static-build (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:27:15 -04:00
Kara Zajac a8bc81c54c docs: regenerate og.png with skeletonkey.netslum.io URL
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:25:55 -04:00
Kara Zajac b7027a1749 docs: point OG/canonical URLs to skeletonkey.netslum.io (new home)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:15:53 -04:00
KaraZajac 03324c8542 modules: add ghostlock (CVE-2026-43499, "GhostLock" rtmutex/futex requeue-PI stack UAF)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Adds the ghostlock module for CVE-2026-43499 ("GhostLock", VEGA / Nebula
Security's "IonStack part II") — a ~15-year race UAF on kernel STACK memory in
the rtmutex/futex requeue-PI path (kernel/locking/rtmutex.c). On the -EDEADLK
deadlock-rollback, remove_waiter() runs against `current` instead of the waiter
task, so a concurrent sched_setattr()-driven PI-chain priority walk on another
CPU clears pi_blocked_on on the wrong task and leaves an on-stack rt_mutex_waiter
dangling. Reachable by any unprivileged user (CVSS 7.8, PR:L) — plain
futex(2) + sched_setattr(2), no userns/CONFIG beyond CONFIG_FUTEX_PI. The
corpus's first rtmutex/futex-PI module and first kernel-stack UAF (all others
are heap/slab).

Introduced 2.6.39; fixed 3bfdc63936dd (7.1-rc1), stable backports
7.0.4 / 6.18.27 / 6.12.86 / 6.6.140 / 6.1.175; 5.15/5.10/5.4/4.19 affected with
no upstream fix. CWE-416; not in KEV.

detect() is a pure version gate over the five-branch backport table. exploit()
forks an isolated child that (A) deterministically confirms the -EDEADLK
remove_waiter() rollback path is reachable (safe — no concurrent walk means no
dangling pointer; validated on real hardware) then (B) exercises the actual race
a hard-bounded 24 iters / 2s with a sibling-CPU sched_setattr(SCHED_BATCH) storm,
and stops. Deliberately under-driven: no copy_from_user widening, no stack-frame
spray/reoccupation, and the KernelSnitch leak -> forged-waiter ->
fops/ashmem/pipe R/W -> cred-patch chain (Android/Pixel-specific, per-build
offsets) is NOT bundled. Returns EXPLOIT_FAIL. Lowest --auto safety rank (11) —
a won race corrupts the kernel stack. Unlike most races it ships a real
detection signature (futex requeue-PI returning EDEADLK + sibling sched_setattr);
auditd/sigma anchor on sched_setattr, falco/eBPF on the EDEADLK tell; no yara.

Wired: registry, Makefile, safety rank 11, version 0.9.13, 9 detect() test rows
(incl. 6.13.0 -> VULNERABLE, the multi-branch "newer than all" case), CVE
metadata (CWE-416 / T1068 / not-KEV; cve_metadata.c + KEV_CROSSREF.md regenerated
-> 13 of 40), README + CVES.md + website counts (45 modules / 40 CVEs),
RELEASE_NOTES v0.9.13, verify-vm target. Also corrects pre-existing
docs/index.html drift left by v0.9.12 (body counts stuck at 43/38 and a missing
bad_epoll corpus pill). Tests: 33 kernel_range + 101 detect, all pass.

Credit: VEGA / Nebula Security (nebusec.ai, NebuSec/CyberMeowfia, Apache-2.0);
upstream fix 3bfdc63936dd (Keenan Dong / Thomas Gleixner).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUq4DGXSPBmPkAyJ9WnM9n
v0.9.13
2026-07-13 15:43:00 -04:00
KaraZajac 95589e26cb modules: add bad_epoll (CVE-2026-46242, "Bad Epoll" epoll teardown race UAF)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Jaeyoung Chung's kernelCTF "Bad Epoll": a race-condition use-after-free in
fs/eventpoll.c. ep_remove() clears file->f_ep under f_lock but keeps using
the file (hlist_del_rcu + unlock) while a concurrent __fput() frees the
still-referenced struct eventpoll. Reachable by any unprivileged user with
no userns / CONFIG / capability; weaponised via cross-cache to a struct
file, /proc/self/fdinfo arb-read, and ROP. Introduced 58c9b016e128 (6.4),
fixed a6dc643c6931 (7.1-rc1), stable backport 7.0.13. CWE-416; not in KEV.

The corpus's first epoll / VFS-teardown module. Shipped as a reconstructed,
deliberately under-driven trigger on the stackrot / nft_catchall contract:
detect() is a pure version gate (>= 6.4 and below the on-branch fix; no
userns precondition -- epoll needs none); exploit() forks a CPU-pinned
child that exercises the ep_remove-vs-__fput close window a hard-bounded 48
attempts / 2s and returns EXPLOIT_FAIL. It does not grind the race to a
win, does not do the cross-cache reclaim, and does not bundle the fdinfo
R/W + ROP (a won race frees a live struct file and rarely trips KASAN ->
silent-corruption risk).

Wired: registry, Makefile, safety rank (12 -- lowest in the corpus; a
kernel race is the least predictable class), 5 detect() test rows (version
gating), CVE metadata (sorted insert, CWE-416 / T1068 / not-KEV), README +
CVES.md + website counts (44/39), RELEASE_NOTES v0.9.12, and a verify-vm
target (sweep pending). Detection rules are intentionally weak/structural
(epoll ubiquitous, rarely KASAN) -- post-exploitation euid-0 transition,
no yara. Also corrects pre-existing README drift in the "not yet verified"
count (8 -> 11). Not VM-verified, so the verified count stays 28 of 39.
Version 0.9.12. Credit: Jaeyoung Chung (J-jaeyoung).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KUq4DGXSPBmPkAyJ9WnM9n
v0.9.12
2026-07-04 19:35:33 -04:00
KaraZajac 4d0a0e2443 modules: add nft_catchall (CVE-2026-23111, nf_tables catch-all abort UAF)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
The newest nftables LPE: a use-after-free in the nf_tables transaction-
abort path. An inverted condition (a stray '!') in
nft_map_catchall_activate() makes the abort path process active catch-all
map elements instead of skipping them; a catch-all GOTO element drives a
chain's use-count to zero so a following DELCHAIN frees it while the
catch-all verdict still references it -> UAF, escalatable from an
unprivileged user (userns + nftables) via modprobe_path/selinux_state ROP.
Fixed upstream by f41c5d1; CWE-416, CVSS 7.8; not in CISA KEV. Public
reproduction by FuzzingLabs.

Takes the corpus to 43 modules / 38 CVEs.

🟡 reconstructed trigger, primitive-only, NOT VM-verified — same contract
as nf_tables (CVE-2024-1086). detect() version-gates (catch-all elems
~5.13; Debian backports 6.1.164/6.12.73/6.18.10, 7.0+ inherits) AND
requires unprivileged user_ns clone (else PRECOND_FAIL). exploit() forks
an isolated child, builds a verdict map with a catch-all GOTO element,
provokes an aborting batch to drive the abort-path UAF, observes slabinfo,
and returns EXPLOIT_FAIL — the per-kernel leak + R/W + modprobe_path ROP
is not bundled. kernel_range table verified drift-clean against the live
Debian tracker (the 6.18 branch / 6.18.10 fix is the real forky/sid
backport; the earlier 7.0.10 figure was wrong).

Wired: registry, Makefile, safety rank (35), 6 detect() test rows (version
+ userns gating), CVE_METADATA.json + cve_metadata.c + KEV_CROSSREF.md
(sorted insert, CWE-416/T1068/not-KEV), README + CVES.md + website counts
(43/38) + yellow pill, RELEASE_NOTES v0.9.11, verify-vm target. Credit:
FuzzingLabs + upstream fix f41c5d1. Version 0.9.11.
v0.9.11
2026-06-08 17:42:19 -04:00
KaraZajac 050731396d docs: record partial VM verification of cifswitch (CVE-2026-46243)
Verified 2026-06-08 on Ubuntu 24.04.4 / kernel 6.8.0-117-generic under
QEMU/HVF (offline: cloud image + payload iso, no guest networking):

- modprobe cifs registers the cifs.spnego key type (cifs-utils not needed
  to reach the primitive).
- Independent python3 ctypes add_key('cifs.spnego', forged
  uid/creduid/upcall_target) ACCEPTED (user-key control also accepted);
  module exploit() independently reported 'primitive CONFIRMED' then the
  honest EXPLOIT_FAIL.
- detect() returned PRECOND_FAIL without cifs-utils and VULNERABLE under
  SKELETONKEY_CIFS_ASSUME_PRESENT=1.

Still pending (so cifswitch stays 🟡 and is NOT counted as a verified
end-to-end CVE; verified count stays 28 of 37): a patched kernel
(>=6.12.90/7.0.10) to prove add_key is REJECTED there (probe discriminates
fixed-from-vulnerable), and the full namespace+NSS root-pop. Recorded in
NOTICE.md, CVES.md, RELEASE_NOTES.md, and tools/verify-vm/targets.yaml.
2026-06-08 13:53:29 -04:00
KaraZajac ada56b0db3 modules: add cifswitch (CVE-2026-46243, Asim Manizada's CIFSwitch)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
CIFSwitch is the newest kernel-7-era LPE not already in the corpus: a
~19-year-old logic flaw in fs/smb/client/cifs_spnego.c where the
cifs.spnego request-key type accepts key descriptions created by
userspace (add_key(2)/request_key(2)) without verifying the request came
from the in-kernel CIFS client. The description's authority-bearing
fields (pid/uid/creduid/upcall_target) are trusted by the root cifs.upcall
helper; with user+mount namespace tricks an unprivileged user coerces
cifs.upcall into loading an attacker NSS module as root. Fixed upstream by
3da1fdf4efbc (merged 7.1-rc5); CWE-20; not in CISA KEV.

Takes the corpus to 42 modules / 37 CVEs.

🟡 honest port — full chain not VM-verified. detect() gates on the kernel
version (Debian backports 5.10.257/6.1.174/6.12.90/7.0.10) AND on the
cifs userspace path (cifs.upcall / cifs.spnego request-key rule), so a
vulnerable kernel without cifs-utils is PRECOND_FAIL not a false positive
(override via SKELETONKEY_CIFS_ASSUME_PRESENT=1/0). exploit() fires only
the non-destructive add_key(2) cifs.spnego probe (no upcall, loads
nothing, revoked immediately) and returns EXPLOIT_FAIL without a euid-0
witness — the namespace+NSS root-pop is not bundled until VM-verified.
--mitigate blocklists the cifs module; --cleanup reverts.

Wired everywhere: registry, Makefile, safety rank (86), 6 detect() test
rows (env-driven precondition override), CVE_METADATA.json + cve_metadata.c
+ KEV_CROSSREF.md (sorted insert, CWE-20/T1068/not-KEV), README + CVES.md
+ website counts (42/37) and a yellow module pill, RELEASE_NOTES v0.9.10,
verify-vm target (sweep pending). Credit: Asim Manizada. Version 0.9.10.
v0.9.10
2026-06-08 11:07:27 -04:00
KaraZajac 28a9289989 fix: normalize CVE_METADATA.json to sorted order (drift-check)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
The weekly drift-check stayed red after v0.9.9's KEV+CWE fixes because of a
latent ordering bug: sudo_host (CVE-2025-32462), added in v0.9.8, was
appended to the end of CVE_METADATA.json instead of its sorted position.
check_drift compares the record list in discover_cves() sorted order, so
the misplaced entry read as drift independent of its field values.
Regenerated via tools/refresh-cve-metadata.py — sorted order restored, all
field values reconfirmed against CISA KEV + NVD in a clean fetch.
v0.9.9
2026-06-08 10:30:25 -04:00
KaraZajac e457b22c1f release v0.9.9: install.sh needs no sudo + CVE metadata drift fix
install.sh never escalates to sudo. The installer defaulted to
/usr/local/bin and fell back to `sudo mv`, prompting for a password on
exactly the unprivileged accounts a privilege-escalation tool targets. It
now uses /usr/local/bin only when already writable and otherwise installs
to a per-user $HOME/.local/bin (honoring XDG_BIN_HOME), no sudo ever. An
explicit SKELETONKEY_PREFIX is honored and errors rather than escalating.
The documented one-liner prepends ~/.local/bin to PATH so it resolves on a
fresh login, and the quickstart drops the misleading sudo from --scan /
--audit / --auto.

CVE metadata drift (the failing weekly drift-check):
  - CVE-2022-0492 (cgroup_release_agent) entered CISA KEV 2026-06-02;
    corpus now 13 of 36 modules cover KEV-listed CVEs.
  - CVE-2026-46333 (ptrace_pidfd) gained CWE-269 from NVD (was unclassified
    at module-add time).
Refreshed CVE_METADATA.json, generated cve_metadata.c, and KEV_CROSSREF.md;
README + website KEV counts and version bumped to 0.9.9.
2026-06-08 10:16:24 -04:00
KaraZajac 60579f1602 release v0.9.8: two new LPE modules (ptrace_pidfd, sudo_host)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Tags the two modules added this cycle (already on main): ptrace_pidfd (CVE-2026-46333, Qualys __ptrace_may_access/pidfd_getfd credential-fd theft, bd63aab) and sudo_host (CVE-2025-32462, Stratascale sudo -h/--host policy bypass, dd5f4fa).

This commit bumps the version strings (skeletonkey.c, README, docs/index.html) and prepends the v0.9.8 RELEASE_NOTES entry. Corpus is now 41 modules / 36 CVEs / 28 verified. Tagging fires release.yml, which rebuilds + publishes the four prebuilt binaries via the Node-24 artifact actions bumped in v0.9.7.
v0.9.8
2026-06-02 09:03:05 -04:00
KaraZajac dd5f4fa06d modules: add sudo_host (CVE-2025-32462, Stratascale sudo --host policy bypass)
Second new module this cycle; sibling of sudo_chwoot (CVE-2025-32463, same Stratascale/Rich Mirch disclosure). sudo's -h/--host option — meant only to pair with -l/--list — was honored when running a command, so a sudoers rule scoped to a host other than the current machine (and not ALL) is usable via 'sudo -h <host> <cmd>' for local root. Affects sudo 1.8.8 -> 1.9.17p0; fixed 1.9.17p1. CWE-863, CVSS 8.8 (not in KEV).

detect(): version-gate [1.8.8, 1.9.17p0] via ctx->host->sudo_version (the host-restricted rule itself isn't probeable unprivileged, so VULNERABLE means 'vulnerable sudo present'). exploit(): discovers an abusable host-restricted rule from readable sudoers (or SKELETONKEY_SUDO_HOST), witnesses with 'sudo -n -h <host> id -u', pops 'sudo -h <host> /bin/bash' (SKELETONKEY_SUDO_CMD) only on a uid-0 witness; honest EXPLOIT_FAIL + operator guidance otherwise. Shared 'sudo' family; structural, arch=any; safety rank 96. auditd/sigma/falco rules, NOTICE.md (Rich Mirch / Stratascale) + MODULE.md, 4 detect() test rows.

Wiring: registry, Makefile, cve_metadata (+JSON), verify-vm/targets.yaml (ubuntu1804 sudo 1.8.21p2 target, sweep pending). Docs: README + CVES.md + docs/index.html counts 40->41 modules / 35->36 CVEs; not-yet-verified lists + corpus pill.
2026-06-02 08:43:09 -04:00
KaraZajac 3d9db6b93e tests: add ptrace_pidfd (CVE-2026-46333) detect() coverage
Six detect() rows over synthetic host fingerprints: predates-gate at pidfd_getfd's 5.6 introduction (4.4 / 5.5.99 -> OK), vulnerable window (5.15.5 / 6.12.87 -> VULNERABLE), exact trixie backport (6.12.88 -> OK), and mainline inheritance (7.1.0 -> OK). Matches the harness per-module coverage convention; clears ptrace_pidfd from the coverage-gap report.
2026-06-02 08:29:10 -04:00
KaraZajac bd63aabd64 modules: add ptrace_pidfd (CVE-2026-46333, Qualys ptrace/pidfd_getfd cred-steal)
New module for Qualys's 2026-05-20 disclosure: a __ptrace_may_access logic flaw leaves a process dropping privileges briefly reachable past its dumpable boundary; pidfd_getfd(2) steals root-opened fds / authenticated channels from it. Default-distro, no userns, arch-agnostic (fd-steal, no shellcode).

detect(): version-pinned, predates-gate at pidfd_getfd's 5.6 introduction; kernel_range from Debian backports (5.10.251/6.1.172/6.12.88/7.0.7), drift-check clean. exploit(): spawns a setuid victim, pidfd_open()s it, sweeps pidfd_getfd() over its fd table during the cred-drop window, reports any uid-0-owned fd captured from a non-root context. Honest EXPLOIT_FAIL without a euid-0 witness; not yet VM-verified. mitigate(): yama ptrace_scope=2; cleanup() reverts. auditd/sigma/falco rules, NOTICE.md (Qualys TRU credit) + MODULE.md, safety rank 84.

Wiring: registry, Makefile, cve_metadata (+JSON source), verify-vm/targets.yaml (ubuntu2204 + mainline 5.15.5 target, sweep pending). Docs: README + CVES.md + docs/index.html counts 39->40 modules / 34->35 CVEs; added to not-yet-verified lists + corpus pill.
2026-06-02 08:26:24 -04:00
KaraZajac 1663df69d1 release v0.9.7: kernel_range drift fix + CI Node 24 readiness
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Tags the maintenance work landed since v0.9.6. The fragnesia drift fix (35c33df) and checkout v4->v6 bump (6c148e2) are already on main; this commit adds the remaining CI Node-24 bumps + version strings.

release.yml: upload-artifact v4->v7, download-artifact v4->v8, softprops/action-gh-release v2->v3 (last of the Node-20-era actions; GitHub forces node24 on 2026-06-16). Reviewed each changelog — our default-zip/unique-name upload + full-set download is unaffected by the major-version breaking changes (opt-in direct uploads, download-by-ID path).

Version bumped to 0.9.7 (skeletonkey.c, README, docs/index.html) + v0.9.7 RELEASE_NOTES entry. Tagging this commit fires release.yml — the end-to-end test of the new artifact actions, incl. the Alpine/musl static job under node24.
v0.9.7
2026-06-01 11:55:31 -04:00
KaraZajac 6c148e276a ci: bump actions/checkout v4 -> v6 (Node 24 readiness)
GitHub forces the Node 24 runtime on 2026-06-16; checkout@v4 runs on the deprecated Node 20. checkout v6.0.2 declares runs.using: node24. All 9 usages (5 in build.yml, 4 in release.yml) are bare checkouts with no inputs, so the major bump is a drop-in.

Still on Node-20-era majors in release.yml, deferred (multi-major jumps with breaking changes, and release.yml only runs on tag push): upload-artifact v4->v7, download-artifact v4->v8, softprops/action-gh-release v2->v3.
2026-06-01 11:40:05 -04:00
KaraZajac 35c33df16f fragnesia: add 5.10.257 kernel_range entry (Debian bullseye backport)
Weekly drift-check (build.yml schedule cron) went red 2026-06-01: Debian's security tracker now lists CVE-2026-46300 as fixed on the 5.10 branch (bullseye 5.10.257), a branch fragnesia's kernel_patched_from table didn't model. detect() would false-positive VULNERABLE on a patched bullseye 5.10.257+ host.

Adding {5,10,257} clears the only MISSING finding; refresh-kernel-ranges.py now exits 0. The 10 remaining drifted modules are INFO-only 'more permissive' entries the check tolerates.
2026-06-01 11:05:05 -04:00
KaraZajac 25c2afc3e9 release v0.9.6: --auto no longer prompts for sudo password
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
sudo_runas_neg1 and sudoedit_editor's detect() bodies invoked
'sudo -ln' (intending list + non-interactive). Some sudoers / PAM
configurations have been observed prompting for a password anyway
when the flags are bundled. That meant skeletonkey --auto --i-know
could hang on a sudo password prompt during the corpus scan — bad
ergonomics for an LPE tool whose whole point is to get root without
already having it.

Fix: write '-n -l' as separate flags, redirect stdin from /dev/null
so sudo cannot fall back to reading the tty even if PAM tries to
coerce one. Belt-and-suspenders against any tty prompt during --auto.
v0.9.6
2026-05-28 21:50:26 -04:00
KaraZajac 13fbbce618 release v0.9.5: kernel_range drift cleanup (12 modules)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
v0.9.4 fixed cve_metadata drift but exposed kernel_range drift which
had been hidden behind it. Applied refresh-kernel-ranges.py --patch
recommendations: 11 TOO_TIGHT findings (false-positive risk — our
threshold later than Debian's earliest known fix) + 8 MISSING (Debian
has fixes for branches we didn't model) across 12 modules.

All changes are strictly correctness-improving: detect() now correctly
returns OK on kernels Debian has on record as patched, instead of
false-positiving VULNERABLE.

The biggest single fix is reverting fragnesia from {7,0,10} (NVD) to
{7,0,9} (Debian's backported fix). I introduced that off-by-one in
v0.9.4 from misreading NVD's versionEndExcluding semantics.

Build's kernel_range drift step now exits 0 with 0 TOO_TIGHT + 0 MISSING.
v0.9.5
2026-05-28 14:51:15 -04:00
KaraZajac bb5ca48fe1 ci: enable workflow_dispatch on build workflow
The drift-check job's if-gate already honors workflow_dispatch but
the trigger itself was never added to the on: block. Without it,
'gh workflow run build.yml' fails with 422. Found while validating
the v0.9.4 drift fix — wanted to confirm drift-check now passes
without waiting for next Monday's cron.
2026-05-28 13:37:35 -04:00
KaraZajac 4454d8148e release v0.9.4: drift unblock, fragnesia range fix, infra docs
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
- Sync docs/CVE_METADATA.json + KEV_CROSSREF.md to match the
  hand-applied core/cve_metadata.c entries from v0.9.3. Nightly
  drift-check (red since 2026-05-25) now passes. Pintheft's CWE
  landed as CWE-787 from NVD (was NULL in the hand-applied entry).
- Fix fragnesia (CVE-2026-46300) range table. Per NVD: bug entered
  at 5.11 SKBFL_SHARED_FRAG, vulnerable through 5.15.207 / 6.1.173 /
  6.6.140 / 6.12.90 / 6.18.32 / 7.0.9, fixed at .208/.174/.141/
  .91/.33/.10. Prior table had one entry {7,0,9} — off-by-one and
  missing every other backport. Added predates-5.11 introduction gate
  + test row.
- Update tools/verify-vm/README.md to document the v0.9.x infra:
  mainline kernel pinning via kernel.ubuntu.com, per-module
  provisioner hooks, two-phase prep→reboot→verify with post-reboot
  kernel confirmation, GRUB_DEFAULT pinning.
- Add curl fallback for NVD lookups in refresh-cve-metadata.py.
  Mirrors the CISA path's existing fallback. Prevents the silent
  Python urlopen hang seen during v0.9.3 prep (55-min stuck on
  CLOSE_WAIT socket; 30s timeout never fired).
v0.9.4
2026-05-28 13:33:28 -04:00
KaraZajac fa0228df9b release v0.9.3: CVE metadata refresh (KEV 10→12) + dirtydecrypt bug fix
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
CVE metadata refresh:
- Added 8 entries to core/cve_metadata.c for the v0.8.0 + v0.9.0 module
  CVEs. Two are CISA-KEV-listed:
  - CVE-2018-14634 mutagen_astronomy (2026-01-26, CWE-190)
  - CVE-2025-32463 sudo_chwoot       (2025-09-29, CWE-829)
- Populated via direct curl when refresh-cve-metadata.py's Python urlopen
  hung on CISA's HTTP/2 endpoint for ~55 min — same data, different
  transport.

dirtydecrypt module bug fix:
- dd_detect() was wrongly gating 'predates the bug' on kernel < 7.0
- Per NVD CVE-2026-31635: bug entered at 6.16.1 stable; vulnerable
  through 6.18.22 / 6.19.12 / 7.0-rc7; fixed at 6.18.23 / 6.19.13 / 7.0
- Fix: predates-gate now uses 6.16.1; patched_branches[] adds {6,18,23}
- Re-verified: dirtydecrypt now correctly returns VULNERABLE on mainline
  6.19.7 instead of OK. Previously a false negative on real vulnerable
  kernels.

Footer goes from '10 in CISA KEV' to '12 in CISA KEV'. Verified count
stays at 28 but dirtydecrypt's record is now a TRUE VULNERABLE match
(was OK match).
v0.9.3
2026-05-24 01:17:58 -04:00
KaraZajac d52fcd5512 docs: sweep stale counts to match v0.9.2 binary state
Audit found several user-facing surfaces still carrying old numbers
from earlier releases. Brought everything in line with the binary's
authoritative footer ('39 modules · 10 KEV · 28 verified · 7 any').

README.md:
- Status section: v0.9.0 → v0.9.2 framing; describe the 22 → 28
  verification arc (v0.9.1 + v0.9.2)
- '119 detection rules' → 151 (current bundled count)
- '10 of 26 KEV-listed' → '10 of 34'
- 'Not yet verified (4 of 26 CVEs)' → '(6 of 34 CVEs)' with the new
  honest list (vmwgfx, dirty_cow, mutagen_astronomy, pintheft,
  vsock_uaf, fragnesia) and the reason each is blocked
- Example --auto output: 31 → 39 modules

docs/index.html:
- '22 of 26 CVEs confirmed' → '28 of 34', mainline kernel list expanded
  (5.4.0-26 / 5.15.5 / 6.1.10 / 6.19.7)
- Corpus section '26 CVEs across 10 years' → '34 CVEs'
- '26 CVEs, 10-year span' (author list intro) → '34 CVEs'
- Footer feature list '22 of 26' → '28 of 34'
- KEV stat chip 11 → 10 (matches binary; the anticipated 11th from
  metadata refresh hasn't been added yet)
- '119 detection rules' → '151' (two occurrences)

docs/og.svg + og.png:
- KEV chip 11 → 10 (matches binary)

CVES.md:
- '31 modules' → '39 modules covering 34 CVEs'
- Rewrote the unverified-rows note to match the actual 6-module list

No content changes to RELEASE_NOTES.md or ROADMAP.md — those entries
correctly describe state at the time they were written.
2026-05-24 00:09:21 -04:00
KaraZajac 66cca39a55 release v0.9.2: dirtydecrypt verified on mainline 6.19.7 (22 → 28)
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Verifies CVE-2026-31635 dirtydecrypt's OK path on a kernel that
predates the bug: 'kernel predates the rxgk RESPONSE-handling code
added in 7.0' — match. Confirms detect() doesn't false-positive on
older 6.x kernels.

Attempted fragnesia (CVE-2026-46300) but mainline 7.0.5 .debs depend
on libssl3t64 / libelf1t64 (t64-transition libs from Ubuntu 24.04+ /
Debian 13+). No Parallels-supported Vagrant box ships those yet —
dpkg --force-depends leaves the kernel package in iHR state with no
/boot/vmlinuz. Marked manual: true with rationale.

Verifier infrastructure: pin-mainline now uses dpkg --force-depends as
a fallback so partial-install state can at least be inspected.
v0.9.2
2026-05-24 00:03:35 -04:00
KaraZajac 92396a0d6d tests: fix 2 test rows with wrong expected verdicts (v0.9.0 regression)
The build workflow (sanitizer job) has been red since v0.9.0 because two
test rows asserted verdicts that don't match what detect() actually
returns:

- udisks_libblockdev: I expected PRECOND_FAIL (udisksd absent in CI), got
  VULNERABLE. GHA ubuntu-24.04 runners ship udisks2 by default; detect()
  does direct path_exists() stat() calls (not host-fixture lookups) so
  it sees the binary and gates pass. Rewritten as 'udisksd present → VULNERABLE'.

- sudo_runas_neg1: I expected PRECOND_FAIL (no (ALL,!root) grant), got OK.
  detect() treats 'no grant' as 'not exploitable from this user' → OK, not
  'missing precondition' → PRECOND_FAIL. Updated expectation.

The release workflow doesn't run the sanitizer job and has been passing
through these failures; the build workflow caught them. Both expectations
are now honest about what detect() does on CI.
2026-05-23 23:38:55 -04:00
KaraZajac 8ac041a295 release v0.9.1: VM verification sweep 22 → 27
release / build (arm64) (push) Has been cancelled
release / build (x86_64) (push) Has been cancelled
release / build (x86_64-static / musl) (push) Has been cancelled
release / build (arm64-static / musl) (push) Has been cancelled
release / release (push) Has been cancelled
Five more CVEs empirically confirmed end-to-end against real Linux VMs:
- CVE-2019-14287 sudo_runas_neg1 (Ubuntu 18.04 + sudoers grant)
- CVE-2020-29661 tioscpgrp        (Ubuntu 20.04 pinned to 5.4.0-26)
- CVE-2024-26581 nft_pipapo       (Ubuntu 22.04 + mainline 5.15.5)
- CVE-2025-32463 sudo_chwoot      (Ubuntu 22.04 + sudo 1.9.16p1 from source)
- CVE-2025-6019  udisks_libblockdev (Debian 12 + udisks2 + polkit rule)

Required real plumbing work:
- Per-module provisioner hook (tools/verify-vm/provisioners/<module>.sh)
- Two-phase provision in verify.sh (prep → reboot if needed → verify)
  fixes silent-fail where new kernel installed but VM never rebooted
- GRUB_DEFAULT pinning in both pin-kernel and pin-mainline blocks
  (kernel downgrades like 5.4.0-169 → 5.4.0-26 now actually boot the target)
- Old-mainline URL fallback in pin-mainline (≤ 4.15 debs at /v$KVER/ not /amd64/)

mutagen_astronomy marked manual: true — mainline 4.14.70 kernel-panics on
Ubuntu 18.04's rootfs ('Failed to execute /init (error -8)' — kernel config
mismatch). Genuinely needs a CentOS 6 / Debian 7 image.
v0.9.1
2026-05-23 23:35:02 -04:00
KaraZajac 270ddc1681 verify-vm: per-module provisioner hook + old-mainline URL fallback
Adds tools/verify-vm/provisioners/<module>.sh hook so per-module setup
(build vulnerable sudo from source, drop polkit allow rule, add sudoers
grant) lives in checked-in scripts rather than manual VM steps. Vagrantfile
runs the script as root before build-and-verify if it exists.

Also fixes mainline kernel fetch to fall back from /v${KVER}/amd64/ to
/v${KVER}/ for old kernels (≤ ~4.15) where debs aren't under the amd64
subdir, and accepts both 'linux-image-' (old) and 'linux-image-unsigned-'
(new) deb names.

Wires up 4 previously-deferred targets to expect VULNERABLE:
- sudo_chwoot: builds sudo 1.9.16p1 from upstream into /usr/local
- udisks_libblockdev: installs udisks2 + polkit rule for vagrant user
- mutagen_astronomy: pins mainline 4.14.70 (one below the .71 fix)
- sudo_runas_neg1: adds (ALL,!root) sudoers grant
2026-05-23 22:36:02 -04:00