modules: migrate remaining 22 modules to ctx->host fingerprint

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.
This commit is contained in:
2026-05-22 23:43:20 -04:00
parent d05a46c5c6
commit 36814f272d
21 changed files with 345 additions and 381 deletions
@@ -50,6 +50,7 @@
#include "skeletonkey_modules.h"
#include "../../core/registry.h"
#include "../../core/kernel_range.h"
#include "../../core/host.h"
#include <stdio.h>
#include <stdlib.h>
@@ -115,19 +116,6 @@ static const struct kernel_range nft_set_uaf_range = {
* ------------------------------------------------------------------ */
#ifdef __linux__
static int can_unshare_userns(void)
{
pid_t pid = fork();
if (pid < 0) return -1;
if (pid == 0) {
if (unshare(CLONE_NEWUSER) == 0) _exit(0);
_exit(1);
}
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) && WEXITSTATUS(status) == 0;
}
static bool nf_tables_loaded(void)
{
FILE *f = fopen("/proc/modules", "r");
@@ -148,45 +136,43 @@ static skeletonkey_result_t nft_set_uaf_detect(const struct skeletonkey_ctx *ctx
(void)ctx;
return SKELETONKEY_PRECOND_FAIL;
#else
struct kernel_version v;
if (!kernel_version_current(&v)) {
fprintf(stderr, "[!] nft_set_uaf: could not parse kernel version\n");
const struct kernel_version *v = ctx->host ? &ctx->host->kernel : NULL;
if (!v || v->major == 0) {
if (!ctx->json) fprintf(stderr, "[!] nft_set_uaf: host fingerprint missing kernel version — bailing\n");
return SKELETONKEY_TEST_ERROR;
}
/* Bug introduced in 5.1 (anonymous-set support). Anything below
* predates it — report OK (not vulnerable to *this* CVE). */
if (v.major < 5 || (v.major == 5 && v.minor < 1)) {
if (v->major < 5 || (v->major == 5 && v->minor < 1)) {
if (!ctx->json) {
fprintf(stderr, "[i] nft_set_uaf: kernel %s predates the bug "
"(anonymous-set support landed in 5.1)\n", v.release);
"(anonymous-set support landed in 5.1)\n", v->release);
}
return SKELETONKEY_OK;
}
bool patched = kernel_range_is_patched(&nft_set_uaf_range, &v);
bool patched = kernel_range_is_patched(&nft_set_uaf_range, v);
if (patched) {
if (!ctx->json) {
fprintf(stderr, "[+] nft_set_uaf: kernel %s is patched\n", v.release);
fprintf(stderr, "[+] nft_set_uaf: kernel %s is patched\n", v->release);
}
return SKELETONKEY_OK;
}
int userns_ok = can_unshare_userns();
bool userns_ok = ctx->host->unprivileged_userns_allowed;
bool nft_loaded = nf_tables_loaded();
if (!ctx->json) {
fprintf(stderr, "[i] nft_set_uaf: kernel %s is in the vulnerable range\n",
v.release);
v->release);
fprintf(stderr, "[i] nft_set_uaf: unprivileged user_ns clone: %s\n",
userns_ok == 1 ? "ALLOWED" :
userns_ok == 0 ? "DENIED" :
"could not test");
userns_ok ? "ALLOWED" : "DENIED");
fprintf(stderr, "[i] nft_set_uaf: nf_tables module currently loaded: %s\n",
nft_loaded ? "yes" : "no (will autoload on first nft use)");
}
if (userns_ok == 0) {
if (!userns_ok) {
if (!ctx->json) {
fprintf(stderr, "[+] nft_set_uaf: kernel vulnerable but user_ns clone "
"denied → unprivileged exploit unreachable\n");
@@ -762,7 +748,8 @@ static skeletonkey_result_t nft_set_uaf_exploit(const struct skeletonkey_ctx *ct
fprintf(stderr, "[-] nft_set_uaf: refusing without --i-know gate\n");
return SKELETONKEY_EXPLOIT_FAIL;
}
if (geteuid() == 0) {
bool is_root = ctx->host ? ctx->host->is_root : (geteuid() == 0);
if (is_root) {
if (!ctx->json)
fprintf(stderr, "[i] nft_set_uaf: already running as root\n");
return SKELETONKEY_OK;