Three coordinated changes that build on the host_kernel_at_least
landed in 1571b88:
1. core/host gains skeletonkey_host_kernel_in_range(h, lo..., hi...)
— a [lo, hi) bounded-interval check for modules that want the
'vulnerable window' semantics directly. Implemented in terms of
host_kernel_at_least (so the comparison logic stays in one place).
No module uses it yet; available for new modules that want it.
2. 13 modules migrated off the manual
if (v->major < X || (v->major == X && v->minor < Y)) { ... }
pattern onto
if (!skeletonkey_host_kernel_at_least(ctx->host, X, Y, 0)) { ... }
One-line replacements, mechanical, no behavior change.
Migrated: af_packet2, dirty_pipe, fuse_legacy, netfilter_xtcompat,
nf_tables, nft_fwd_dup, nft_payload, nft_set_uaf, overlayfs,
overlayfs_setuid, ptrace_traceme, stackrot, vmwgfx. The repo now
has zero manual 'v->major < X' patterns — every predates-check
reads the same way.
3. tests/test_detect.c expanded from 17 to 29 cases. Adds:
Above-fix coverage on h_kernel_6_12 (10 modules previously
untested): af_packet, af_packet2, af_unix_gc, netfilter_xtcompat,
nft_set_uaf, nft_fwd_dup, nft_payload, stackrot, sequoia, vmwgfx.
Ancient-kernel predates coverage on h_kernel_4_4 (2 more cases):
nft_set_uaf (introduced 5.1), stackrot (introduced 6.1).
Detect-path test coverage now spans most of the corpus that
has a testable host-fingerprint gate. Untested modules from
here on are either userspace bugs whose detect() doesn't gate
on host fields (pwnkit, sudo_samedit, sudoedit_editor),
entrybleed (sysfs-direct, no host gate), or the copy_fail_family
bridge (no ctx->host integration yet).
Verification: Linux (docker gcc:latest, non-root user): 29/29 pass.
macOS (local): 31-module build clean, suite reports 'skipped —
Linux-only' as designed.
Completes the host-fingerprint refactor that started in c00c3b4. Every
module now consults the shared ctx->host (populated once at startup
by core/host.c) instead of re-doing uname / geteuid / /etc/os-release
parsing / fork+unshare(CLONE_NEWUSER) probes per detect().
Migrations applied per module (mechanical, no exploit logic touched):
1. #include "../../core/host.h" inside each module's #ifdef __linux__.
2. kernel_version_current(&v) -> ctx->host->kernel (with the
v -> v-> arrow-vs-dot fix for all later usage). Drops ~20 redundant
uname() calls across the corpus.
3. geteuid() == 0 (the 'already root, nothing to escalate' gate) ->
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
This is the key change that lets the unit test suite construct
non-root fingerprints regardless of the test process's actual euid.
4. Per-detect fork+unshare(CLONE_NEWUSER) probe helpers (named
can_unshare_userns / can_unshare_userns_mount across the corpus)
are removed wholesale; their call sites now consult
ctx->host->unprivileged_userns_allowed, which was probed once at
startup. Removes ~10 per-scan fork()s.
Modules touched by this commit (22):
Batch A (7): dirty_pipe, dirty_cow, ptrace_traceme, pwnkit,
cgroup_release_agent, overlayfs_setuid, and entrybleed
(no migration target — KPTI gate stays as direct sysfs
read; documented as 'no applicable pattern').
Batch B (7): nf_tables, cls_route4, netfilter_xtcompat, af_packet,
af_packet2, af_unix_gc, fuse_legacy.
Batch C (8): stackrot, nft_set_uaf, nft_fwd_dup, nft_payload,
sudo_samedit, sequoia, sudoedit_editor, vmwgfx.
Combined with the 4 modules already migrated (dirtydecrypt, fragnesia,
pack2theroot, overlayfs) and the 5-module copy_fail_family bridge,
the entire registered corpus now goes through ctx->host. The 4
'fork+unshare per detect()' helpers that existed across nf_tables,
cls_route4, netfilter_xtcompat, af_packet, af_packet2, fuse_legacy,
nft_set_uaf, nft_fwd_dup, nft_payload, sequoia,
cgroup_release_agent, and overlayfs_setuid are now gone — replaced by
the single startup probe in core/host.c.
Verification:
- Linux (docker gcc:latest + libglib2.0-dev): full clean build links
31 modules; tests/test_detect.c: 8/8 pass.
- macOS (local): full clean build links 31 modules (Mach-O, 172KB);
test suite reports skipped as designed on non-Linux.
Subsequent commits can add more EXPECT_DETECT cases in
tests/test_detect.c — the host-fingerprint paths in every module are
now uniformly testable via synthetic struct skeletonkey_host instances.
The README has been claiming "each module credits the original CVE
reporter and PoC author in its NOTICE.md" since v0.1.0, but only
copy_fail_family actually shipped one. Fixed.
modules/<name>/NOTICE.md (×19 new + 1 existing): per-module
research credit covering CVE ID, discoverer, original advisory
URL where public, upstream fix commit, IAMROOT's role.
iamroot.c: new --dump-offsets subcommand. Resolves kernel offsets
via the existing core/offsets.c four-source chain (env →
/proc/kallsyms → /boot/System.map → embedded table), then emits
a ready-to-paste C struct entry for kernel_table[]. Run once
as root on a target kernel build; upstream via PR. Eliminates
fabricating offsets — every shipped entry traces back to a
`iamroot --dump-offsets` invocation on a real kernel.
docs/OFFSETS.md: documents the --dump-offsets workflow.
CVES.md: notes the NOTICE.md convention + offset dump tool.
iamroot.c: bump IAMROOT_VERSION 0.3.0 → 0.3.1.
Each module now exposes an opt-in full-chain root-pop via --full-chain:
default --exploit behavior is unchanged (primitive-only, returns
EXPLOIT_FAIL). With --full-chain, after primitive lands, modules call
iamroot_finisher_modprobe_path() via a module-specific arb_write_fn
that re-uses the same trigger + slab groom to write a userspace
payload path into modprobe_path[], then exec a setuid bash dropped
by the kernel-invoked modprobe.
netfilter_xtcompat (+239): msg_msg m_list_next stride-seed FALLBACK
af_packet (+316): sk_buff data-pointer stride-seed FALLBACK
af_packet2 (+156): tp_reserve underflow + skb spray, LAST RESORT
nf_tables (+275): forged pipapo_elem with kaddr value-ptr
(Notselwyn offset 0x10), FALLBACK
cls_route4 (+251): msg_msg refill of UAF'd filter, FALLBACK
fuse_legacy (+291): m_ts overflow + MSG_COPY sanity gate,
FALLBACK (one of two modules with a real
post-write sanity check)
stackrot (+233): race-driver budget extended 3s → 30s when
--full-chain; honest <1% race-win/run
All seven honor verified-vs-claimed: arb_write_fn returns 0 for
"trigger structurally fired"; the shared finisher's setuid-bash
sentinel poll is the empirical arbiter. EXPLOIT_OK only when the
sentinel materializes within 3s of the modprobe_path trigger.
Build clean on Debian 6.12.86 (kctf-mgr); all 7 modules refuse
cleanly on both default and --full-chain paths via the existing
patched-kernel detect gate (short-circuits before the new branch).
netfilter_xtcompat (CVE-2021-22555): +597 LoC — Option B
Andy Nguyen's IPT_SO_SET_REPLACE 4-byte OOB write trigger;
msg_msg kmalloc-2k spray + sk_buff sidecar; MSG_COPY witness
+ slabinfo delta. No leak→modprobe_path chain (per-kernel
offsets refused), honest EXPLOIT_FAIL with continuation
roadmap.
stackrot (CVE-2023-3269): +619 LoC — Option C
Two-thread race driver (MAP_GROWSDOWN + mremap rotation vs
fork+fault) with cpu pinning + 3s budget; kmalloc-192 spray
for anon_vma/anon_vma_chain; race-iteration + signal
breadcrumb to /tmp/iamroot-stackrot.log. Honest reliability
note in module header: <1% race-win/run on a vulnerable
kernel — the public PoC averages minutes-to-hours and needs
a much wider VMA staging matrix to be reliable.
Both refuse cleanly on Debian 6.12.86 (kctf-mgr); build clean.
This closes out the detect-only → LPE port across the corpus.
All 22 registered modules now either fire a real primitive or
refuse honestly per the verified-vs-claimed bar.