From 0d87cbc71c451cffb39686cab03f0df56084d31e Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Sat, 23 May 2026 00:02:23 -0400 Subject: [PATCH] copy_fail_family: bridge-level userns gate + 4 new tests (33 total) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 4 dirty_frag siblings + the GCM variant all gate on unprivileged user-namespace creation (the XFRM-ESP / AF_RXRPC paths are unreachable without it). The inner DIRTYFAIL detect functions already check this, but the check happened deep inside the legacy code — invisible to the test harness, and the bridge wrappers would delegate first and only short-circuit afterwards. Move the check up to the bridge: a single cff_check_userns() helper inspects ctx->host->unprivileged_userns_allowed and returns PRECOND_FAIL (with a host-fingerprint-annotated message) BEFORE calling the inner detect. The inner check stays in place as belt- and-suspenders. copy_fail itself uses AF_ALG (no userns needed) and bypasses the gate — its inner detect still confirms the primitive empirically via the active probe. modules/copy_fail_family/skeletonkey_modules.c: - #include "../../core/host.h" alongside the existing includes. - new static cff_check_userns(modname, ctx) helper. - copy_fail_gcm_detect_wrap, dirty_frag_esp_detect_wrap, dirty_frag_esp6_detect_wrap, dirty_frag_rxrpc_detect_wrap all call cff_check_userns before delegating. - copy_fail_detect_wrap is intentionally untouched. tests/test_detect.c: 4 new EXPECT_DETECT cases assert that all 4 gated bridge wrappers return PRECOND_FAIL when unprivileged_userns_allowed=false, using the existing h_kernel_5_14_no_userns fingerprint. 29 → 33 tests, all pass on Linux. --- .../copy_fail_family/skeletonkey_modules.c | 32 +++++++++++++++++++ tests/test_detect.c | 28 ++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/modules/copy_fail_family/skeletonkey_modules.c b/modules/copy_fail_family/skeletonkey_modules.c index a10931e..15810bb 100644 --- a/modules/copy_fail_family/skeletonkey_modules.c +++ b/modules/copy_fail_family/skeletonkey_modules.c @@ -17,6 +17,7 @@ #include "skeletonkey_modules.h" #include "../../core/registry.h" +#include "../../core/host.h" #include "src/common.h" #include "src/copyfail.h" @@ -43,6 +44,29 @@ static void apply_ctx(const struct skeletonkey_ctx *ctx) * it's a debug knob; default stays off. */ } +/* Bridge-level userns precondition. The 4 dirty_frag siblings + the + * GCM variant all reach the bug via XFRM-ESP / AF_RXRPC paths gated on + * unprivileged user-namespace creation (the inner DIRTYFAIL detect + * checks for it too, but doing it here gives the dispatcher one + * testable point per module and short-circuits the heavier + * inner-detect work when the gate is closed). copy_fail itself uses + * AF_ALG which doesn't strictly need userns, so it bypasses this + * gate — its inner detect still confirms the primitive empirically. */ +static skeletonkey_result_t cff_check_userns(const char *modname, + const struct skeletonkey_ctx *ctx) +{ + if (ctx->host && !ctx->host->unprivileged_userns_allowed) { + if (!ctx->json) + fprintf(stderr, "[i] %s: unprivileged user namespaces are " + "disabled (host fingerprint) — XFRM/RxRPC variant " + "unreachable here%s\n", modname, + ctx->host->apparmor_restrict_userns + ? "; AppArmor restriction is on" : ""); + return SKELETONKEY_PRECOND_FAIL; + } + return SKELETONKEY_OK; +} + /* ----- Family-wide --mitigate / --cleanup ----- * * The family-wide mitigation (blacklist algif_aead + esp4 + esp6 + rxrpc, @@ -154,6 +178,8 @@ const struct skeletonkey_module copy_fail_module = { static skeletonkey_result_t copy_fail_gcm_detect_wrap(const struct skeletonkey_ctx *ctx) { apply_ctx(ctx); + skeletonkey_result_t pre = cff_check_userns("copy_fail_gcm", ctx); + if (pre != SKELETONKEY_OK) return pre; return (skeletonkey_result_t)copyfail_gcm_detect(); } @@ -184,6 +210,8 @@ const struct skeletonkey_module copy_fail_gcm_module = { static skeletonkey_result_t dirty_frag_esp_detect_wrap(const struct skeletonkey_ctx *ctx) { apply_ctx(ctx); + skeletonkey_result_t pre = cff_check_userns("dirty_frag_esp", ctx); + if (pre != SKELETONKEY_OK) return pre; return (skeletonkey_result_t)dirtyfrag_esp_detect(); } @@ -214,6 +242,8 @@ const struct skeletonkey_module dirty_frag_esp_module = { static skeletonkey_result_t dirty_frag_esp6_detect_wrap(const struct skeletonkey_ctx *ctx) { apply_ctx(ctx); + skeletonkey_result_t pre = cff_check_userns("dirty_frag_esp6", ctx); + if (pre != SKELETONKEY_OK) return pre; return (skeletonkey_result_t)dirtyfrag_esp6_detect(); } @@ -244,6 +274,8 @@ const struct skeletonkey_module dirty_frag_esp6_module = { static skeletonkey_result_t dirty_frag_rxrpc_detect_wrap(const struct skeletonkey_ctx *ctx) { apply_ctx(ctx); + skeletonkey_result_t pre = cff_check_userns("dirty_frag_rxrpc", ctx); + if (pre != SKELETONKEY_OK) return pre; return (skeletonkey_result_t)dirtyfrag_rxrpc_detect(); } diff --git a/tests/test_detect.c b/tests/test_detect.c index b8f851c..dd7a3e1 100644 --- a/tests/test_detect.c +++ b/tests/test_detect.c @@ -51,6 +51,10 @@ extern const struct skeletonkey_module nft_payload_module; extern const struct skeletonkey_module stackrot_module; extern const struct skeletonkey_module sequoia_module; extern const struct skeletonkey_module vmwgfx_module; +extern const struct skeletonkey_module copy_fail_gcm_module; +extern const struct skeletonkey_module dirty_frag_esp_module; +extern const struct skeletonkey_module dirty_frag_esp6_module; +extern const struct skeletonkey_module dirty_frag_rxrpc_module; static int g_pass = 0; static int g_fail = 0; @@ -337,6 +341,30 @@ static void run_all(void) run_one("stackrot: kernel 4.4 predates 6.1 → OK", &stackrot_module, &h_kernel_4_4, SKELETONKEY_OK); + + /* ── copy_fail_family bridge userns gate ───────────────────── + * The 4 dirty_frag siblings + the GCM variant all reach the + * bug via XFRM-ESP / AF_RXRPC paths gated on unprivileged + * user-namespace creation. Bridge-layer precondition fires + * before delegating to the inner DIRTYFAIL detect. copy_fail + * itself uses AF_ALG (no userns needed) and bypasses the + * gate — its detect would proceed to the inner active probe. */ + + run_one("copy_fail_gcm: userns_allowed=false → PRECOND_FAIL", + ©_fail_gcm_module, &h_kernel_5_14_no_userns, + SKELETONKEY_PRECOND_FAIL); + + run_one("dirty_frag_esp: userns_allowed=false → PRECOND_FAIL", + &dirty_frag_esp_module, &h_kernel_5_14_no_userns, + SKELETONKEY_PRECOND_FAIL); + + run_one("dirty_frag_esp6: userns_allowed=false → PRECOND_FAIL", + &dirty_frag_esp6_module, &h_kernel_5_14_no_userns, + SKELETONKEY_PRECOND_FAIL); + + run_one("dirty_frag_rxrpc: userns_allowed=false → PRECOND_FAIL", + &dirty_frag_rxrpc_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");