Files
SKELETONKEY/tests/test_detect.c
T
leviathan 1571b88725 core/host: skeletonkey_host_kernel_at_least + 9 new detect() tests
core/host helper:
- Adds bool skeletonkey_host_kernel_at_least(h, M, m, p) — the
  canonical 'kernel >= X.Y.Z' check. Replaces the manual
  'v->major < X || (v->major == X && v->minor < Y)' pattern that
  many modules use for their 'predates the bug' pre-check. Returns
  false when h is NULL or h->kernel.major == 0 (degenerate cases),
  true otherwise iff the host kernel sorts at or above the supplied
  version.
- dirtydecrypt migrated as the demo: the 'kernel < 7.0 → predates'
  pre-check now reads 'if (!host_kernel_at_least(ctx->host, 7, 0, 0))'.
  Other modules still using the manual pattern continue to work
  unchanged; migrating them is incremental polish.

tests/test_detect.c expansion (8 → 17 cases):

New fingerprints:
- h_kernel_4_4    — ancient (Linux 4.4 LTS); used for 'predates the
                    bug' on dirty_pipe.
- h_kernel_6_12   — recent (Linux 6.12 LTS); above every backport
                    threshold in the corpus — modules report OK via
                    the 'patched by mainline inheritance' branch of
                    kernel_range_is_patched.
- h_kernel_5_14_no_userns — vulnerable-era kernel (5.14.0, past
                    every relevant predates check while below every
                    backport entry) with unprivileged_userns_allowed
                    deliberately false; lets the userns gate fire
                    after the version check confirms vulnerable.

New tests (9):
- dirty_pipe + kernel 4.4 → OK (predates 5.8 introduction)
- dirty_pipe + kernel 6.12 → OK (above every backport)
- dirty_cow + kernel 6.12 → OK (above 4.9 fix)
- ptrace_traceme + kernel 6.12 → OK (above 5.1.17 fix)
- cgroup_release_agent + kernel 6.12 → OK (above 5.17 fix)
- nf_tables + vuln kernel + userns=false → PRECOND_FAIL
- fuse_legacy + vuln kernel + userns=false → PRECOND_FAIL
- cls_route4 + vuln kernel + userns=false → PRECOND_FAIL
- overlayfs_setuid + vuln kernel + userns=false → PRECOND_FAIL

Process note: initial 8th and 9th userns tests failed because the
chosen test kernel (5.10.0) tripped each module's predates check
(nf_tables bug introduced 5.14; overlayfs_setuid 5.11). Switched to
5.14.0, which is past every predates threshold AND below every
backport entry in this batch — the version verdict is now genuinely
'vulnerable' and the userns gate fires next. The bug-finding tests
caught a real-but-narrow modeling gap in the original picks.

Verification:
- Linux (docker gcc:latest, non-root user): 17/17 pass.
- macOS (local): builds clean, suite reports 'skipped — Linux-only'
  as designed.
2026-05-22 23:52:10 -04:00

299 lines
11 KiB
C

/*
* tests/test_detect.c — detect() unit tests
*
* Each test builds a synthetic struct skeletonkey_host fingerprint
* (vulnerable / patched / specific-gate-closed) and asserts each
* module's detect() returns the expected verdict. Catches regressions
* in the host-fingerprint-consuming logic across the corpus.
*
* Coverage today is the four modules that already consume ctx->host:
* - dirtydecrypt (CVE-2026-31635)
* - fragnesia (CVE-2026-46300)
* - pack2theroot (CVE-2026-41651)
* - overlayfs (CVE-2021-3493)
* Coverage grows automatically as more modules migrate to ctx->host
* (see ROADMAP "core/host" follow-up).
*
* Why only Linux: every module's real detect() lives inside
* `#ifdef __linux__`; on non-Linux the stubs unconditionally return
* PRECOND_FAIL so the tests are tautologies. The harness compiles
* cross-platform but skips the assertions on non-Linux to keep the
* macOS dev build green while still preventing bit-rot of the test
* infrastructure.
*/
#include "../core/module.h"
#include "../core/host.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
extern const struct skeletonkey_module dirtydecrypt_module;
extern const struct skeletonkey_module fragnesia_module;
extern const struct skeletonkey_module pack2theroot_module;
extern const struct skeletonkey_module overlayfs_module;
extern const struct skeletonkey_module dirty_pipe_module;
extern const struct skeletonkey_module dirty_cow_module;
extern const struct skeletonkey_module ptrace_traceme_module;
extern const struct skeletonkey_module cgroup_release_agent_module;
extern const struct skeletonkey_module nf_tables_module;
extern const struct skeletonkey_module fuse_legacy_module;
extern const struct skeletonkey_module cls_route4_module;
extern const struct skeletonkey_module overlayfs_setuid_module;
static int g_pass = 0;
static int g_fail = 0;
static const char *result_str(skeletonkey_result_t r)
{
switch (r) {
case SKELETONKEY_OK: return "OK";
case SKELETONKEY_TEST_ERROR: return "TEST_ERROR";
case SKELETONKEY_VULNERABLE: return "VULNERABLE";
case SKELETONKEY_EXPLOIT_FAIL: return "EXPLOIT_FAIL";
case SKELETONKEY_PRECOND_FAIL: return "PRECOND_FAIL";
case SKELETONKEY_EXPLOIT_OK: return "EXPLOIT_OK";
}
return "???";
}
#ifdef __linux__
/* Suppress per-module banner chatter so the test output stays tidy.
* Modules respect ctx->json to mean "structured output mode; no banners"
* — see each module's `if (!ctx->json) fprintf(...)` pattern. */
static void run_one(const char *test_name,
const struct skeletonkey_module *m,
const struct skeletonkey_host *h,
skeletonkey_result_t want)
{
struct skeletonkey_ctx ctx = {0};
ctx.host = h;
ctx.json = true; /* silence per-module log lines */
skeletonkey_result_t got = m->detect(&ctx);
if (got == want) {
printf("[+] PASS %-40s %s → %s\n",
test_name, m->name, result_str(got));
g_pass++;
} else {
fprintf(stderr,
"[-] FAIL %-40s %s: want %s, got %s\n",
test_name, m->name,
result_str(want), result_str(got));
g_fail++;
}
}
/* ── fingerprints ────────────────────────────────────────────────── */
/* Linux 6.12.76 (Debian 13), no userns, no D-Bus, not Ubuntu — a
* deliberately neutered host that lets the host-fingerprint-only
* gates fire without falling into deeper module logic. */
static const struct skeletonkey_host h_pre7_no_userns_no_dbus = {
.kernel = { .major = 6, .minor = 12, .patch = 76,
.release = "6.12.76-test" },
.arch = "x86_64",
.nodename = "test",
.distro_id = "debian",
.distro_version_id = "13",
.distro_pretty = "Debian GNU/Linux 13",
.is_linux = true,
.is_debian_family = true,
.unprivileged_userns_allowed = false,
.has_dbus_system = false,
.has_systemd = true,
};
/* Fedora 43, no Debian family, userns allowed. */
static const struct skeletonkey_host h_fedora_no_debian = {
.kernel = { .major = 6, .minor = 14, .patch = 0,
.release = "6.14.0-fedora" },
.arch = "x86_64",
.nodename = "test",
.distro_id = "fedora",
.distro_version_id = "43",
.distro_pretty = "Fedora 43",
.is_linux = true,
.is_rpm_family = true,
.is_debian_family = false,
.unprivileged_userns_allowed = true,
.has_dbus_system = true,
.has_systemd = true,
};
/* Ubuntu 24.04, userns allowed, D-Bus running, Debian family
* (because Ubuntu has /etc/debian_version). Used as the "fragnesia
* preconditions OK" baseline — fragnesia should NOT short-circuit
* on userns/userspace gates here. */
static const struct skeletonkey_host h_ubuntu_24_userns_ok = {
.kernel = { .major = 6, .minor = 8, .patch = 0,
.release = "6.8.0-ubuntu" },
.arch = "x86_64",
.nodename = "test",
.distro_id = "ubuntu",
.distro_version_id = "24.04",
.distro_pretty = "Ubuntu 24.04 LTS",
.is_linux = true,
.is_debian_family = true,
.unprivileged_userns_allowed = true,
.has_dbus_system = true,
.has_systemd = true,
};
/* Ancient kernel that predates many bugs (Linux 4.4 LTS). Useful for
* the "kernel predates the bug → OK" path in dirty_pipe (bug
* introduced 5.8). */
static const struct skeletonkey_host h_kernel_4_4 = {
.kernel = { .major = 4, .minor = 4, .patch = 0,
.release = "4.4.0-ancient" },
.arch = "x86_64",
.nodename = "test",
.distro_id = "debian",
.is_linux = true,
.is_debian_family = true,
.unprivileged_userns_allowed = true,
};
/* Recent kernel (Linux 6.12 LTS). Above virtually every backport
* threshold in the corpus — modules should report OK via the
* "patched by mainline inheritance" branch of kernel_range_is_patched. */
static const struct skeletonkey_host h_kernel_6_12 = {
.kernel = { .major = 6, .minor = 12, .patch = 0,
.release = "6.12.0-recent" },
.arch = "x86_64",
.nodename = "test",
.distro_id = "debian",
.is_linux = true,
.is_debian_family = true,
.unprivileged_userns_allowed = true,
};
/* Vulnerable-era kernel (5.14.0) with userns DISABLED. Most
* netfilter / overlayfs / cgroup-class modules need both an in-range
* kernel AND unprivileged userns. Kernel 5.14 was deliberately
* chosen to clear every module's "predates the bug" pre-check in
* this batch (nf_tables introduced 5.14; overlayfs_setuid 5.11;
* cls_route4/fuse_legacy older still) while remaining below every
* stable-branch backport entry (5.15.x / 5.18.x / 5.19.x in the
* relevant tables). The version check therefore says "VULNERABLE by
* version", and the userns gate fires next. */
static const struct skeletonkey_host h_kernel_5_14_no_userns = {
.kernel = { .major = 5, .minor = 14, .patch = 0,
.release = "5.14.0-vuln-no-userns" },
.arch = "x86_64",
.nodename = "test",
.distro_id = "debian",
.is_linux = true,
.is_debian_family = true,
.unprivileged_userns_allowed = false,
};
#endif /* __linux__ */
/* ── tests ───────────────────────────────────────────────────────── */
static void run_all(void)
{
#ifdef __linux__
/* dirtydecrypt: kernel.major < 7 → predates the bug → OK */
run_one("dirtydecrypt: kernel 6.12 predates 7.0 → OK",
&dirtydecrypt_module, &h_pre7_no_userns_no_dbus,
SKELETONKEY_OK);
run_one("dirtydecrypt: kernel 6.14 (fedora) still predates → OK",
&dirtydecrypt_module, &h_fedora_no_debian,
SKELETONKEY_OK);
run_one("dirtydecrypt: kernel 6.8 (ubuntu) still predates → OK",
&dirtydecrypt_module, &h_ubuntu_24_userns_ok,
SKELETONKEY_OK);
/* fragnesia: userns disabled → XFRM gate closed → PRECOND_FAIL */
run_one("fragnesia: userns_allowed=false → PRECOND_FAIL",
&fragnesia_module, &h_pre7_no_userns_no_dbus,
SKELETONKEY_PRECOND_FAIL);
/* pack2theroot: not Debian family → PRECOND_FAIL */
run_one("pack2theroot: is_debian_family=false → PRECOND_FAIL",
&pack2theroot_module, &h_fedora_no_debian,
SKELETONKEY_PRECOND_FAIL);
/* pack2theroot: Debian family but no D-Bus socket → PRECOND_FAIL */
run_one("pack2theroot: has_dbus_system=false → PRECOND_FAIL",
&pack2theroot_module, &h_pre7_no_userns_no_dbus,
SKELETONKEY_PRECOND_FAIL);
/* overlayfs: distro != ubuntu → bug is Ubuntu-specific → OK */
run_one("overlayfs: distro=debian → not Ubuntu → OK",
&overlayfs_module, &h_pre7_no_userns_no_dbus,
SKELETONKEY_OK);
run_one("overlayfs: distro=fedora → not Ubuntu → OK",
&overlayfs_module, &h_fedora_no_debian,
SKELETONKEY_OK);
/* ── kernel-version-gate cases (post-migration coverage) ──── */
/* dirty_pipe: bug introduced in 5.8; kernel 4.4 predates → OK */
run_one("dirty_pipe: kernel 4.4 predates 5.8 → OK",
&dirty_pipe_module, &h_kernel_4_4,
SKELETONKEY_OK);
/* dirty_pipe: kernel 6.12 is above every backport entry → OK */
run_one("dirty_pipe: kernel 6.12 above all backports → OK",
&dirty_pipe_module, &h_kernel_6_12,
SKELETONKEY_OK);
/* dirty_cow: fix in mainline 4.9; kernel 6.12 is far above → OK */
run_one("dirty_cow: kernel 6.12 above 4.9 fix → OK",
&dirty_cow_module, &h_kernel_6_12,
SKELETONKEY_OK);
/* ptrace_traceme: fix in 5.1.17; kernel 6.12 above → OK */
run_one("ptrace_traceme: kernel 6.12 above 5.1.17 fix → OK",
&ptrace_traceme_module, &h_kernel_6_12,
SKELETONKEY_OK);
/* cgroup_release_agent: fix in mainline 5.17; kernel 6.12 above → OK */
run_one("cgroup_release_agent: kernel 6.12 above 5.17 fix → OK",
&cgroup_release_agent_module, &h_kernel_6_12,
SKELETONKEY_OK);
/* ── userns-gate cases ───────────────────────────────────── */
/* nf_tables: vulnerable kernel 5.10.0 + userns off → PRECOND_FAIL */
run_one("nf_tables: vuln kernel + userns=false → PRECOND_FAIL",
&nf_tables_module, &h_kernel_5_14_no_userns,
SKELETONKEY_PRECOND_FAIL);
/* fuse_legacy: vulnerable kernel + userns off → PRECOND_FAIL */
run_one("fuse_legacy: vuln kernel + userns=false → PRECOND_FAIL",
&fuse_legacy_module, &h_kernel_5_14_no_userns,
SKELETONKEY_PRECOND_FAIL);
/* cls_route4: vulnerable kernel + userns off → PRECOND_FAIL */
run_one("cls_route4: vuln kernel + userns=false → PRECOND_FAIL",
&cls_route4_module, &h_kernel_5_14_no_userns,
SKELETONKEY_PRECOND_FAIL);
/* overlayfs_setuid: vulnerable kernel (5.14, past the 5.11
* introduction and below every backport) + userns off
* → PRECOND_FAIL via userns gate */
run_one("overlayfs_setuid: vuln kernel + userns=false → PRECOND_FAIL",
&overlayfs_setuid_module, &h_kernel_5_14_no_userns,
SKELETONKEY_PRECOND_FAIL);
#else
fprintf(stderr, "[i] non-Linux platform: detect() bodies are stubbed; "
"tests skipped (would tautologically pass).\n");
#endif
}
int main(void)
{
fprintf(stderr, "=== SKELETONKEY detect() unit tests ===\n\n");
run_all();
fprintf(stderr, "\n=== RESULTS: %d passed, %d failed ===\n",
g_pass, g_fail);
return g_fail ? 1 : 0;
}