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:
@@ -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_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
|
||||
# All DIRTYFAIL .c files contribute; skeletonkey_modules.c is the bridge.
|
||||
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) \
|
||||
$(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.
|
||||
# Links against the same module objects as the main binary minus the
|
||||
# top-level dispatcher (which provides main(); the test has its own).
|
||||
# Tests — `make test` builds and runs both unit-test binaries.
|
||||
#
|
||||
# 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_SRCS := $(TEST_DIR)/test_detect.c
|
||||
TEST_OBJS := $(patsubst %.c,$(BUILD)/%.o,$(TEST_SRCS))
|
||||
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
|
||||
|
||||
@@ -207,8 +222,14 @@ $(BIN): $(ALL_OBJS)
|
||||
$(TEST_BIN): $(TEST_ALL_OBJS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ -lpthread $(P2TR_LIBS)
|
||||
|
||||
test: $(TEST_BIN)
|
||||
@echo "[*] running test suite ($(TEST_BIN))"
|
||||
$(TEST_KR_BIN): $(TEST_KR_ALL_OBJS)
|
||||
$(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)
|
||||
|
||||
# Generic compile: any .c → corresponding .o under build/
|
||||
@@ -223,7 +244,7 @@ static: LDFLAGS += -static
|
||||
static: clean $(BIN)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD) $(BIN) $(TEST_BIN)
|
||||
rm -rf $(BUILD) $(BIN) $(TEST_BIN) $(TEST_KR_BIN)
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
*
|
||||
* Simple flat array. Resized in chunks of 16. We never expect more
|
||||
* 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"
|
||||
|
||||
@@ -48,4 +48,11 @@ void skeletonkey_register_dirtydecrypt(void);
|
||||
void skeletonkey_register_fragnesia(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 */
|
||||
|
||||
@@ -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
@@ -1035,35 +1035,11 @@ static int cmd_one(const struct skeletonkey_module *m, const char *op,
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
/* Bring up the module registry. As new families land, add their
|
||||
* register_* call here. */
|
||||
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();
|
||||
/* Bring up the module registry. New module families register
|
||||
* themselves via skeletonkey_register_all_modules() in
|
||||
* core/registry.c — add the new register_*() call there so the
|
||||
* test binary picks it up automatically. */
|
||||
skeletonkey_register_all_modules();
|
||||
|
||||
enum mode mode = MODE_SCAN;
|
||||
struct skeletonkey_ctx ctx = {0};
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include "../core/module.h"
|
||||
#include "../core/host.h"
|
||||
#include "../core/registry.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -62,6 +63,23 @@ extern const struct skeletonkey_module pwnkit_module;
|
||||
static int g_pass = 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)
|
||||
{
|
||||
switch (r) {
|
||||
@@ -89,6 +107,7 @@ static void run_one(const char *test_name,
|
||||
ctx.json = true; /* silence per-module log lines */
|
||||
|
||||
skeletonkey_result_t got = m->detect(&ctx);
|
||||
mark_tested(m->name);
|
||||
if (got == want) {
|
||||
printf("[+] PASS %-40s %s → %s\n",
|
||||
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 ────────────────────────────────────────────────── */
|
||||
|
||||
/* 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",
|
||||
&nft_payload_module, &h_kernel_5_14_userns_ok,
|
||||
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
|
||||
fprintf(stderr, "[i] non-Linux platform: detect() bodies are stubbed; "
|
||||
"tests skipped (would tautologically pass).\n");
|
||||
@@ -495,6 +641,12 @@ static void run_all(void)
|
||||
int main(void)
|
||||
{
|
||||
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();
|
||||
fprintf(stderr, "\n=== RESULTS: %d passed, %d failed ===\n",
|
||||
g_pass, g_fail);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user