8243817f7e
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).
223 lines
8.3 KiB
C
223 lines
8.3 KiB
C
/*
|
|
* 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;
|
|
}
|