test harness: kernel_range unit tests + coverage report + register_all helper

Three coupled improvements to the test harness:

1. New tests/test_kernel_range.c — 32 pure unit tests covering
   kernel_range_is_patched(), skeletonkey_host_kernel_at_least(),
   and skeletonkey_host_kernel_in_range(). These are the central
   comparison primitives every module routes through; a regression
   in any of them silently mis-classifies entire CVE families. Tests
   cover exact boundary, one-below, mainline-only, multi-LTS,
   between-branch, and NULL-safety cases. Builds and runs
   cross-platform (no Linux syscalls).

2. tests/test_detect.c additions:
   - mk_host(base, major, minor, patch, release) builder so new
     fingerprint-based tests don't duplicate 14-line struct literals
     to override one (major, minor, patch) triple.
   - Post-run coverage report that iterates the runtime registry and
     warns about modules without at least one direct test row. Output
     is informational (no CI fail) so coverage grows incrementally.
   - 7 new boundary tests for the kernel_patched_from entries added
     by tools/refresh-kernel-ranges.py (commit 8de46e2):
       - af_unix_gc 6.4.12 → VULNERABLE / 6.4.13 → OK
       - vmwgfx 5.10.127 → OK
       - nft_set_uaf 5.10.179 → OK / 6.1.27 → OK
       - nft_payload 5.10.162 → OK
       - nf_tables 5.10.209 → OK

3. core/registry_all.c — extracts the 27-line 'call every
   skeletonkey_register_<family>()' enumeration from skeletonkey.c
   into a shared helper. skeletonkey.c main() now calls
   skeletonkey_register_all_modules() once; the detect-test main()
   does the same. Kept in its own translation unit so registry.c
   stays standalone for the lean kernel_range unit-test binary
   (which links core/ only, no modules).

Makefile: builds two test binaries now —
  skeletonkey-test     — detect() integration tests (full corpus)
  skeletonkey-test-kr  — kernel_range unit tests (core/ only)
'make test' runs both.

Verification:
  - macOS: 32/32 kernel_range tests pass; detect tests skipped
    (non-Linux platform, stubbed bodies).
  - Linux (docker gcc:latest): 32/32 kernel_range + 51/51 detect.
    Coverage report identifies 2 modules without direct tests
    (copy_fail, entrybleed) out of 31 registered.

Test counts: 44 -> 83 (+39).
This commit is contained in:
2026-05-23 01:09:30 -04:00
parent 8de46e212e
commit 8243817f7e
7 changed files with 466 additions and 37 deletions
+29 -8
View File
@@ -23,6 +23,11 @@ BIN := skeletonkey
CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c core/host.c CORE_SRCS := core/registry.c core/kernel_range.c core/offsets.c core/finisher.c core/host.c
CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS)) CORE_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(CORE_SRCS))
# Register-every-module helper. Lives in its own translation unit so
# the kernel_range unit-test binary can link just CORE_OBJS without
# pulling in every module symbol via registry_all.o.
REGISTRY_ALL_OBJ := $(BUILD)/core/registry_all.o
# Family: copy_fail_family # Family: copy_fail_family
# All DIRTYFAIL .c files contribute; skeletonkey_modules.c is the bridge. # All DIRTYFAIL .c files contribute; skeletonkey_modules.c is the bridge.
CFF_DIR := modules/copy_fail_family CFF_DIR := modules/copy_fail_family
@@ -186,16 +191,26 @@ MODULE_OBJS := $(CFF_OBJS) $(DP_OBJS) $(EB_OBJS) $(PK_OBJS) $(NFT_OBJS) \
$(SAM_OBJS) $(SEQ_OBJS) $(SUE_OBJS) $(VMW_OBJS) \ $(SAM_OBJS) $(SEQ_OBJS) $(SUE_OBJS) $(VMW_OBJS) \
$(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS) $(DDC_OBJS) $(FGN_OBJS) $(P2TR_OBJS)
ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(MODULE_OBJS) ALL_OBJS := $(TOP_OBJ) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
# Tests — `make test` builds and runs the detect() unit-test harness. # Tests — `make test` builds and runs both unit-test binaries.
# Links against the same module objects as the main binary minus the #
# top-level dispatcher (which provides main(); the test has its own). # skeletonkey-test — detect() integration tests against
# synthetic host fingerprints. Links
# the full module corpus.
# skeletonkey-test-kr — pure unit tests for kernel_range +
# host comparison helpers. Tiny binary
# (core/ only); runs cross-platform.
TEST_DIR := tests TEST_DIR := tests
TEST_SRCS := $(TEST_DIR)/test_detect.c TEST_SRCS := $(TEST_DIR)/test_detect.c
TEST_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(TEST_SRCS)) TEST_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(TEST_SRCS))
TEST_BIN := skeletonkey-test TEST_BIN := skeletonkey-test
TEST_ALL_OBJS := $(TEST_OBJS) $(CORE_OBJS) $(MODULE_OBJS) TEST_ALL_OBJS := $(TEST_OBJS) $(CORE_OBJS) $(REGISTRY_ALL_OBJ) $(MODULE_OBJS)
TEST_KR_SRCS := $(TEST_DIR)/test_kernel_range.c
TEST_KR_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(TEST_KR_SRCS))
TEST_KR_BIN := skeletonkey-test-kr
TEST_KR_ALL_OBJS := $(TEST_KR_OBJS) $(CORE_OBJS)
.PHONY: all clean debug static help test .PHONY: all clean debug static help test
@@ -207,8 +222,14 @@ $(BIN): $(ALL_OBJS)
$(TEST_BIN): $(TEST_ALL_OBJS) $(TEST_BIN): $(TEST_ALL_OBJS)
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
test: $(TEST_BIN) $(TEST_KR_BIN): $(TEST_KR_ALL_OBJS)
@echo "[*] running test suite ($(TEST_BIN))" $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
test: $(TEST_BIN) $(TEST_KR_BIN)
@echo "[*] running kernel_range unit tests ($(TEST_KR_BIN))"
./$(TEST_KR_BIN)
@echo
@echo "[*] running detect() integration tests ($(TEST_BIN))"
./$(TEST_BIN) ./$(TEST_BIN)
# Generic compile: any .c → corresponding .o under build/ # Generic compile: any .c → corresponding .o under build/
@@ -223,7 +244,7 @@ static: LDFLAGS += -static
static: clean $(BIN) static: clean $(BIN)
clean: clean:
rm -rf $(BUILD) $(BIN) $(TEST_BIN) rm -rf $(BUILD) $(BIN) $(TEST_BIN) $(TEST_KR_BIN)
help: help:
@echo "Targets:" @echo "Targets:"
+5
View File
@@ -3,6 +3,11 @@
* *
* Simple flat array. Resized in chunks of 16. We never expect more * Simple flat array. Resized in chunks of 16. We never expect more
* than a few dozen modules, so this is fine. * than a few dozen modules, so this is fine.
*
* The canonical "register every family" enumeration lives in
* registry_all.c — kept separate so this file links into the
* standalone kernel_range unit-test binary without pulling in every
* module's symbol.
*/ */
#include "registry.h" #include "registry.h"
+7
View File
@@ -48,4 +48,11 @@ void skeletonkey_register_dirtydecrypt(void);
void skeletonkey_register_fragnesia(void); void skeletonkey_register_fragnesia(void);
void skeletonkey_register_pack2theroot(void); void skeletonkey_register_pack2theroot(void);
/* Call every skeletonkey_register_<family>() above in canonical order.
* Single source of truth so the main binary and the test binary stay
* in sync — adding a new module is one register_* declaration here
* and one call inside skeletonkey_register_all_modules() in
* core/registry.c (the test harness picks it up automatically). */
void skeletonkey_register_all_modules(void);
#endif /* SKELETONKEY_REGISTRY_H */ #endif /* SKELETONKEY_REGISTRY_H */
+46
View File
@@ -0,0 +1,46 @@
/*
* SKELETONKEY — canonical "register every module family" enumeration.
*
* Kept in its own translation unit so registry.c stays standalone:
* the kernel_range unit-test binary links registry.c (for the basic
* register / count / find API) without pulling in every module's
* symbol. The main binary and detect-integration test link this
* file too and get the full lineup.
*
* Adding a new module is one new register_<family>() declaration in
* registry.h plus one call below — the integration test picks it up
* via skeletonkey_register_all_modules() in its main().
*/
#include "registry.h"
void skeletonkey_register_all_modules(void)
{
skeletonkey_register_copy_fail_family();
skeletonkey_register_dirty_pipe();
skeletonkey_register_entrybleed();
skeletonkey_register_pwnkit();
skeletonkey_register_nf_tables();
skeletonkey_register_overlayfs();
skeletonkey_register_cls_route4();
skeletonkey_register_dirty_cow();
skeletonkey_register_ptrace_traceme();
skeletonkey_register_netfilter_xtcompat();
skeletonkey_register_af_packet();
skeletonkey_register_fuse_legacy();
skeletonkey_register_stackrot();
skeletonkey_register_af_packet2();
skeletonkey_register_cgroup_release_agent();
skeletonkey_register_overlayfs_setuid();
skeletonkey_register_nft_set_uaf();
skeletonkey_register_af_unix_gc();
skeletonkey_register_nft_fwd_dup();
skeletonkey_register_nft_payload();
skeletonkey_register_sudo_samedit();
skeletonkey_register_sequoia();
skeletonkey_register_sudoedit_editor();
skeletonkey_register_vmwgfx();
skeletonkey_register_dirtydecrypt();
skeletonkey_register_fragnesia();
skeletonkey_register_pack2theroot();
}
+5 -29
View File
@@ -1035,35 +1035,11 @@ static int cmd_one(const struct skeletonkey_module *m, const char *op,
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
/* Bring up the module registry. As new families land, add their /* Bring up the module registry. New module families register
* register_* call here. */ * themselves via skeletonkey_register_all_modules() in
skeletonkey_register_copy_fail_family(); * core/registry.c — add the new register_*() call there so the
skeletonkey_register_dirty_pipe(); * test binary picks it up automatically. */
skeletonkey_register_entrybleed(); skeletonkey_register_all_modules();
skeletonkey_register_pwnkit();
skeletonkey_register_nf_tables();
skeletonkey_register_overlayfs();
skeletonkey_register_cls_route4();
skeletonkey_register_dirty_cow();
skeletonkey_register_ptrace_traceme();
skeletonkey_register_netfilter_xtcompat();
skeletonkey_register_af_packet();
skeletonkey_register_fuse_legacy();
skeletonkey_register_stackrot();
skeletonkey_register_af_packet2();
skeletonkey_register_cgroup_release_agent();
skeletonkey_register_overlayfs_setuid();
skeletonkey_register_nft_set_uaf();
skeletonkey_register_af_unix_gc();
skeletonkey_register_nft_fwd_dup();
skeletonkey_register_nft_payload();
skeletonkey_register_sudo_samedit();
skeletonkey_register_sequoia();
skeletonkey_register_sudoedit_editor();
skeletonkey_register_vmwgfx();
skeletonkey_register_dirtydecrypt();
skeletonkey_register_fragnesia();
skeletonkey_register_pack2theroot();
enum mode mode = MODE_SCAN; enum mode mode = MODE_SCAN;
struct skeletonkey_ctx ctx = {0}; struct skeletonkey_ctx ctx = {0};
+152
View File
@@ -24,6 +24,7 @@
#include "../core/module.h" #include "../core/module.h"
#include "../core/host.h" #include "../core/host.h"
#include "../core/registry.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
@@ -62,6 +63,23 @@ extern const struct skeletonkey_module pwnkit_module;
static int g_pass = 0; static int g_pass = 0;
static int g_fail = 0; static int g_fail = 0;
/* Record which modules at least one test row touched, so the harness
* can print a "modules without direct coverage" warning at the end.
* Linear append + scan is fine; we have <50 modules. The list is
* static-sized at SKELETONKEY_MAX_TESTED_MODULES; bump if we ever
* exceed it. */
#define SKELETONKEY_MAX_TESTED_MODULES 128
static const char *g_tested_modules[SKELETONKEY_MAX_TESTED_MODULES];
static size_t g_tested_count = 0;
static void mark_tested(const char *name)
{
for (size_t i = 0; i < g_tested_count; i++)
if (strcmp(g_tested_modules[i], name) == 0) return;
if (g_tested_count < SKELETONKEY_MAX_TESTED_MODULES)
g_tested_modules[g_tested_count++] = name;
}
static const char *result_str(skeletonkey_result_t r) static const char *result_str(skeletonkey_result_t r)
{ {
switch (r) { switch (r) {
@@ -89,6 +107,7 @@ static void run_one(const char *test_name,
ctx.json = true; /* silence per-module log lines */ ctx.json = true; /* silence per-module log lines */
skeletonkey_result_t got = m->detect(&ctx); skeletonkey_result_t got = m->detect(&ctx);
mark_tested(m->name);
if (got == want) { if (got == want) {
printf("[+] PASS %-40s %s → %s\n", printf("[+] PASS %-40s %s → %s\n",
test_name, m->name, result_str(got)); test_name, m->name, result_str(got));
@@ -102,6 +121,31 @@ static void run_one(const char *test_name,
} }
} }
/* mk_host: derive a fingerprint from a base + a kernel override.
*
* The most common new-test shape is "I want fingerprint X but with a
* specific (major, minor, patch) — to nail a backport-boundary or
* predates-the-bug case". Doing this with a fresh struct literal each
* time obscures the *one* thing that's different. mk_host() does the
* copy + overlay, named release string included.
*
* Returns a struct VALUE so the caller stores it in a stack local and
* passes &h. No heap. The release string is the caller's responsibility
* (we don't synthesize from numerics to avoid implying a real release
* naming convention). */
#ifdef __linux__
static struct skeletonkey_host
mk_host(struct skeletonkey_host base, int major, int minor, int patch,
const char *release)
{
base.kernel.major = major;
base.kernel.minor = minor;
base.kernel.patch = patch;
base.kernel.release = release;
return base;
}
#endif
/* ── fingerprints ────────────────────────────────────────────────── */ /* ── fingerprints ────────────────────────────────────────────────── */
/* Linux 6.12.76 (Debian 13), no userns, no D-Bus, not Ubuntu — a /* Linux 6.12.76 (Debian 13), no userns, no D-Bus, not Ubuntu — a
@@ -486,6 +530,108 @@ static void run_all(void)
run_one("nft_payload: vuln kernel 5.14 + userns ok → VULNERABLE", run_one("nft_payload: vuln kernel 5.14 + userns ok → VULNERABLE",
&nft_payload_module, &h_kernel_5_14_userns_ok, &nft_payload_module, &h_kernel_5_14_userns_ok,
SKELETONKEY_VULNERABLE); SKELETONKEY_VULNERABLE);
/* ── drift-entry boundary coverage ────────────────────────────
* These tests guard the kernel_patched_from entries added by the
* tools/refresh-kernel-ranges.py drift batch (commit 8de46e2).
* Each entry has a "just-below" + "exact" pair so a regression
* that drops or off-by-ones the entry is caught immediately. */
/* af_unix_gc {6, 4, 13} — Debian forky stable backport. The bug is
* reachable as a plain unprivileged user (AF_UNIX needs no caps and
* no userns), so 6.4.12 returns VULNERABLE rather than
* PRECOND_FAIL — the just-below-boundary verdict the table
* decides. */
struct skeletonkey_host h_af_unix_6_4_12 =
mk_host(h_kernel_5_14_no_userns, 6, 4, 12, "6.4.12-test");
run_one("af_unix_gc: 6.4.12 (one below new entry) → VULNERABLE",
&af_unix_gc_module, &h_af_unix_6_4_12,
SKELETONKEY_VULNERABLE);
struct skeletonkey_host h_af_unix_6_4_13 =
mk_host(h_kernel_5_14_no_userns, 6, 4, 13, "6.4.13-test");
run_one("af_unix_gc: 6.4.13 (exact new entry) → OK via patch table",
&af_unix_gc_module, &h_af_unix_6_4_13,
SKELETONKEY_OK);
/* vmwgfx {5, 10, 127} — Debian bullseye stable backport. Below the
* entry, detect proceeds past the version check and fails the
* AF_VSOCK / /dev/dri probe in CI → PRECOND_FAIL. At the exact
* entry, kernel_range_is_patched short-circuits → OK. */
struct skeletonkey_host h_vmwgfx_5_10_127 =
mk_host(h_kernel_5_14_no_userns, 5, 10, 127, "5.10.127-test");
run_one("vmwgfx: 5.10.127 (exact new entry) → OK via patch table",
&vmwgfx_module, &h_vmwgfx_5_10_127,
SKELETONKEY_OK);
/* nft_set_uaf {5, 10, 179} (harmonised from 5.10.180) — exact entry
* patches via table. */
struct skeletonkey_host h_nft_set_5_10_179 =
mk_host(h_kernel_5_14_no_userns, 5, 10, 179, "5.10.179-test");
run_one("nft_set_uaf: 5.10.179 (harmonised entry) → OK via patch table",
&nft_set_uaf_module, &h_nft_set_5_10_179,
SKELETONKEY_OK);
/* nft_set_uaf {6, 1, 27} (harmonised from 6.1.28) — exact entry
* patches via table. */
struct skeletonkey_host h_nft_set_6_1_27 =
mk_host(h_kernel_5_14_no_userns, 6, 1, 27, "6.1.27-test");
run_one("nft_set_uaf: 6.1.27 (harmonised entry) → OK via patch table",
&nft_set_uaf_module, &h_nft_set_6_1_27,
SKELETONKEY_OK);
/* nft_payload {5, 10, 162} (harmonised from 5.10.163) — exact entry. */
struct skeletonkey_host h_nft_payload_5_10_162 =
mk_host(h_kernel_5_14_no_userns, 5, 10, 162, "5.10.162-test");
run_one("nft_payload: 5.10.162 (harmonised entry) → OK via patch table",
&nft_payload_module, &h_nft_payload_5_10_162,
SKELETONKEY_OK);
/* nf_tables {5, 10, 209} (harmonised from 5.10.210) — exact entry. */
struct skeletonkey_host h_nf_tables_5_10_209 =
mk_host(h_kernel_5_14_no_userns, 5, 10, 209, "5.10.209-test");
run_one("nf_tables: 5.10.209 (harmonised entry) → OK via patch table",
&nf_tables_module, &h_nf_tables_5_10_209,
SKELETONKEY_OK);
/* ── coverage report ─────────────────────────────────────────
* Iterate the runtime registry (populated by skeletonkey_register_*
* calls in main()) and warn for any module that was not touched
* by at least one run_one() row above. Doesn't fail CI — listing
* is informational so we can grow coverage incrementally without
* blocking the build. */
{
size_t n_reg = skeletonkey_module_count();
size_t missing = 0;
for (size_t i = 0; i < n_reg; i++) {
const struct skeletonkey_module *m =
skeletonkey_module_at(i);
if (!m) continue;
bool found = false;
for (size_t j = 0; j < g_tested_count; j++) {
if (strcmp(g_tested_modules[j], m->name) == 0) {
found = true; break;
}
}
if (!found) {
if (missing++ == 0) {
fprintf(stderr,
"\n[i] coverage: module(s) without "
"a direct detect() test row:\n");
}
fprintf(stderr, " - %s\n", m->name);
}
}
if (missing) {
fprintf(stderr, "[i] coverage: total %zu module(s) "
"need test rows (registry has %zu, tests touched %zu)\n",
missing, n_reg, g_tested_count);
} else {
fprintf(stderr, "[i] coverage: every registered module "
"has at least one direct test row (%zu/%zu)\n",
g_tested_count, n_reg);
}
}
#else #else
fprintf(stderr, "[i] non-Linux platform: detect() bodies are stubbed; " fprintf(stderr, "[i] non-Linux platform: detect() bodies are stubbed; "
"tests skipped (would tautologically pass).\n"); "tests skipped (would tautologically pass).\n");
@@ -495,6 +641,12 @@ static void run_all(void)
int main(void) int main(void)
{ {
fprintf(stderr, "=== SKELETONKEY detect() unit tests ===\n\n"); fprintf(stderr, "=== SKELETONKEY detect() unit tests ===\n\n");
/* Populate the runtime registry so the post-run coverage report
* can iterate every module the main binary would. Same call used
* by skeletonkey.c main(). */
skeletonkey_register_all_modules();
run_all(); run_all();
fprintf(stderr, "\n=== RESULTS: %d passed, %d failed ===\n", fprintf(stderr, "\n=== RESULTS: %d passed, %d failed ===\n",
g_pass, g_fail); g_pass, g_fail);
+222
View File
@@ -0,0 +1,222 @@
/*
* tests/test_kernel_range.c — unit tests for the central kernel
* version-comparison helpers in core/kernel_range.c and core/host.c.
*
* These helpers are the foundation of the host-fingerprint pattern:
* every module that gates on kernel version routes through
* skeletonkey_host_kernel_at_least(),
* skeletonkey_host_kernel_in_range(), or kernel_range_is_patched().
* A regression in any of them silently mis-classifies entire CVE
* families. The detect() integration tests in test_detect.c exercise
* these indirectly via real modules; this file pins them down with
* direct boundary-condition assertions so failures point at the right
* file.
*
* Cross-platform: pure logic, no Linux syscalls. Runs identically on
* macOS dev builds and Linux CI.
*/
#include "../core/kernel_range.h"
#include "../core/host.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static int g_pass = 0;
static int g_fail = 0;
#define EXPECT(name, cond) do { \
if (cond) { \
printf("[+] PASS %s\n", (name)); \
g_pass++; \
} else { \
fprintf(stderr, "[-] FAIL %s\n", (name)); \
g_fail++; \
} \
} while (0)
/* ── kernel_range_is_patched ────────────────────────────────────────── */
static void test_kernel_range_is_patched(void)
{
/* Common single-branch-plus-mainline table: backport on 5.15.42,
* mainline fix at 5.17.0. */
static const struct kernel_patched_from pf_5_15_5_17[] = {
{5, 15, 42},
{5, 17, 0},
};
const struct kernel_range r1 = { pf_5_15_5_17, 2 };
struct kernel_version v;
v = (struct kernel_version){5, 15, 42, NULL};
EXPECT("range: exact backport boundary (5.15.42) → patched",
kernel_range_is_patched(&r1, &v));
v = (struct kernel_version){5, 15, 41, NULL};
EXPECT("range: one below backport (5.15.41) → vulnerable",
!kernel_range_is_patched(&r1, &v));
v = (struct kernel_version){5, 15, 100, NULL};
EXPECT("range: well above backport on same branch (5.15.100) → patched",
kernel_range_is_patched(&r1, &v));
v = (struct kernel_version){5, 17, 0, NULL};
EXPECT("range: mainline fix exact (5.17.0) → patched",
kernel_range_is_patched(&r1, &v));
v = (struct kernel_version){5, 16, 0, NULL};
EXPECT("range: between branches (5.16.0) → vulnerable",
!kernel_range_is_patched(&r1, &v));
v = (struct kernel_version){5, 14, 999, NULL};
EXPECT("range: branch below all entries (5.14.999) → vulnerable",
!kernel_range_is_patched(&r1, &v));
v = (struct kernel_version){6, 12, 0, NULL};
EXPECT("range: newer mainline branch (6.12.0) → patched via inheritance",
kernel_range_is_patched(&r1, &v));
/* Mainline-only entry — common pattern for a fresh CVE with no
* stable backports yet. */
static const struct kernel_patched_from pf_7_0_only[] = {
{7, 0, 0},
};
const struct kernel_range r2 = { pf_7_0_only, 1 };
v = (struct kernel_version){6, 19, 99, NULL};
EXPECT("mainline-only: kernel below mainline (6.19.99) → vulnerable",
!kernel_range_is_patched(&r2, &v));
v = (struct kernel_version){7, 0, 0, NULL};
EXPECT("mainline-only: at mainline (7.0.0) → patched",
kernel_range_is_patched(&r2, &v));
v = (struct kernel_version){7, 5, 0, NULL};
EXPECT("mainline-only: above mainline (7.5.0) → patched",
kernel_range_is_patched(&r2, &v));
/* Multi-LTS table mirroring real af_unix_gc layout. */
static const struct kernel_patched_from pf_multi[] = {
{4, 14, 326},
{4, 19, 295},
{5, 4, 257},
{5, 10, 197},
{5, 15, 130},
{6, 1, 51},
{6, 4, 13},
{6, 5, 0},
};
const struct kernel_range r3 = { pf_multi, 8 };
v = (struct kernel_version){5, 10, 196, NULL};
EXPECT("multi-LTS: 5.10.196 (one below backport) → vulnerable",
!kernel_range_is_patched(&r3, &v));
v = (struct kernel_version){5, 10, 197, NULL};
EXPECT("multi-LTS: 5.10.197 (exact backport) → patched",
kernel_range_is_patched(&r3, &v));
v = (struct kernel_version){6, 4, 12, NULL};
EXPECT("multi-LTS: 6.4.12 (just-added entry, below) → vulnerable",
!kernel_range_is_patched(&r3, &v));
v = (struct kernel_version){6, 4, 13, NULL};
EXPECT("multi-LTS: 6.4.13 (just-added entry, exact) → patched",
kernel_range_is_patched(&r3, &v));
v = (struct kernel_version){6, 2, 0, NULL};
EXPECT("multi-LTS: 6.2.0 (between LTS branches, no match) → vulnerable",
!kernel_range_is_patched(&r3, &v));
v = (struct kernel_version){5, 8, 0, NULL};
EXPECT("multi-LTS: 5.8.0 (between LTS branches) → vulnerable",
!kernel_range_is_patched(&r3, &v));
/* NULL safety. */
v = (struct kernel_version){5, 15, 42, NULL};
EXPECT("null safety: NULL range → false",
!kernel_range_is_patched(NULL, &v));
EXPECT("null safety: NULL version → false",
!kernel_range_is_patched(&r1, NULL));
}
/* ── skeletonkey_host_kernel_at_least ───────────────────────────────── */
static void test_host_kernel_at_least(void)
{
struct skeletonkey_host h = {0};
h.kernel.major = 6; h.kernel.minor = 12; h.kernel.patch = 5;
EXPECT("at_least: 6.12.5 ≥ 6.12.5 → true (exact)",
skeletonkey_host_kernel_at_least(&h, 6, 12, 5));
EXPECT("at_least: 6.12.5 ≥ 6.12.4 → true",
skeletonkey_host_kernel_at_least(&h, 6, 12, 4));
EXPECT("at_least: 6.12.5 ≥ 6.12.6 → false",
!skeletonkey_host_kernel_at_least(&h, 6, 12, 6));
EXPECT("at_least: 6.12.5 ≥ 6.11.999 → true (lower minor)",
skeletonkey_host_kernel_at_least(&h, 6, 11, 999));
EXPECT("at_least: 6.12.5 ≥ 6.13.0 → false (higher minor)",
!skeletonkey_host_kernel_at_least(&h, 6, 13, 0));
EXPECT("at_least: 6.12.5 ≥ 5.0.0 → true (lower major)",
skeletonkey_host_kernel_at_least(&h, 5, 0, 0));
EXPECT("at_least: 6.12.5 ≥ 7.0.0 → false (higher major)",
!skeletonkey_host_kernel_at_least(&h, 7, 0, 0));
/* NULL host → false (don't crash). */
EXPECT("at_least: NULL host → false",
!skeletonkey_host_kernel_at_least(NULL, 5, 0, 0));
/* Unpopulated host (major == 0) → false on any positive threshold:
* a zero kernel version means we never probed; modules should
* fail-safe by treating "unknown" as "below". */
struct skeletonkey_host h_zero = {0};
EXPECT("at_least: zeroed host (major=0) → false on any threshold",
!skeletonkey_host_kernel_at_least(&h_zero, 5, 0, 0));
}
/* ── skeletonkey_host_kernel_in_range ───────────────────────────────── */
static void test_host_kernel_in_range(void)
{
struct skeletonkey_host h = {0};
/* Window [5.8.0, 5.17.0) — the classic mainline introduction/fix
* pattern used by dirty_pipe and several others. */
h.kernel = (struct kernel_version){5, 8, 0, NULL};
EXPECT("in_range: 5.8.0 in [5.8.0, 5.17.0) → true (lo inclusive)",
skeletonkey_host_kernel_in_range(&h, 5, 8, 0, 5, 17, 0));
h.kernel = (struct kernel_version){5, 16, 999, NULL};
EXPECT("in_range: 5.16.999 in [5.8.0, 5.17.0) → true (inside)",
skeletonkey_host_kernel_in_range(&h, 5, 8, 0, 5, 17, 0));
h.kernel = (struct kernel_version){5, 17, 0, NULL};
EXPECT("in_range: 5.17.0 in [5.8.0, 5.17.0) → false (hi exclusive)",
!skeletonkey_host_kernel_in_range(&h, 5, 8, 0, 5, 17, 0));
h.kernel = (struct kernel_version){5, 7, 999, NULL};
EXPECT("in_range: 5.7.999 below 5.8.0 → false",
!skeletonkey_host_kernel_in_range(&h, 5, 8, 0, 5, 17, 0));
h.kernel = (struct kernel_version){6, 0, 0, NULL};
EXPECT("in_range: 6.0.0 above 5.17 → false",
!skeletonkey_host_kernel_in_range(&h, 5, 8, 0, 5, 17, 0));
/* NULL host. */
EXPECT("in_range: NULL host → false",
!skeletonkey_host_kernel_in_range(NULL, 5, 8, 0, 5, 17, 0));
}
int main(void)
{
fprintf(stderr, "=== SKELETONKEY kernel_range unit tests ===\n\n");
test_kernel_range_is_patched();
test_host_kernel_at_least();
test_host_kernel_in_range();
fprintf(stderr, "\n=== RESULTS: %d passed, %d failed ===\n",
g_pass, g_fail);
return g_fail ? 1 : 0;
}