From a26f471ecf01e98d622ef9fea2fe6b0fead756fb Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Fri, 22 May 2026 23:06:15 -0400 Subject: [PATCH] dirtydecrypt + fragnesia: pin CVE fix commits, version-based detect() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both modules' detect() was precondition-only because we didn't know the mainline fix commits at port time. Debian's security tracker now provides them β€” pinning here turns detect() into a proper version- based verdict (still with --active for empirical override). dirtydecrypt (CVE-2026-31635): - Fix commit a2567217ade970ecc458144b6be469bc015b23e5 in mainline 7.0 ('rxrpc: fix oversized RESPONSE authenticator length check'). - Debian tracker confirms older stable branches (5.10 / 6.1 / 6.12) as : the rxgk RESPONSE- handling code was added in 7.0. - kernel_range table: { {7, 0, 0} } - detect() pre-checks 'kernel < 7.0 -> SKELETONKEY_OK (predates)' then consults the table. With --active, the /tmp sentinel probe overrides empirically (catches pre-fix 7.0-rc kernels the version check reports as patched). fragnesia (CVE-2026-46300): - Fix in mainline 7.0.9 per Debian tracker ('linux unstable: 7.0.9-1 fixed'). Older Debian-stable branches (bullseye 5.10 / bookworm 6.1 / trixie 6.12) are still marked vulnerable as of 2026-05-22 - no backports yet. - kernel_range table: { {7, 0, 9} } - detect() keeps the userns + carrier preconditions, then consults the table: 7.0.9+ -> OK; older branches without an explicit backport entry -> VULNERABLE (version-only). --active confirms empirically. - Table is intentionally minimal so distros that DO backport in the future flow into 'patched' once their branch lands an entry; until then, the conservative VULNERABLE verdict on unfixed branches is correct. Other changes: - module struct .kernel_range strings updated from 'fix commit not yet pinned' to the actual pinned-version prose. - module_safety_rank bumped 86 -> 87 for both modules (version-pinned detect is now real; still below the verified copy_fail family at 88 so --auto prefers verified modules when both apply). - Both modules now #include core/kernel_range.h inside their #ifdef __linux__ block. - MODULE.md verification-status sections rewritten: detect() is now version-pinned; only the exploit body remains unverified. - CVES.md note + inventory rows updated: dropped the 'precondition- only' language for the pair; all three ported modules now have pinned fix references. - README βšͺ tier description + module list aligned to the new state. Both detect()s smoke-tested in docker gcc:latest on kernel 6.12.76- linuxkit: dirtydecrypt correctly reports OK ('predates the rxgk code added in 7.0'); fragnesia + pack2theroot correctly report PRECOND_FAIL (no userns / no D-Bus in container). Local macOS + Linux builds both clean. --- CVES.md | 34 +++++---- README.md | 16 ++-- modules/dirtydecrypt_cve_2026_31635/MODULE.md | 30 ++++---- .../skeletonkey_modules.c | 74 +++++++++++++++---- modules/fragnesia_cve_2026_46300/MODULE.md | 28 +++---- .../skeletonkey_modules.c | 71 ++++++++++++++---- skeletonkey.c | 2 +- 7 files changed, 175 insertions(+), 80 deletions(-) diff --git a/CVES.md b/CVES.md index 59196c6..86c3470 100644 --- a/CVES.md +++ b/CVES.md @@ -30,22 +30,26 @@ below β€” CVE-2026-31402 β€” is a *candidate* with no module, not counted as a module.) > **Note on `dirtydecrypt` / `fragnesia` / `pack2theroot`:** all three -> are ported from public PoCs and are **not yet VM-verified** end-to-end. -> They are listed 🟑 in the table below but are **not** part of the +> are ported from public PoCs. The **exploit bodies** are not yet +> VM-verified end-to-end, so they're listed 🟑 but excluded from the > 28-module verified corpus. > -> `pack2theroot`'s `detect()` reads PackageKit's version directly from -> the daemon over D-Bus and compares against the **pinned fix release -> (1.3.5, commit `76cfb675`)** β€” so its verdict is high-confidence, -> grounded in upstream's own version metadata. +> All three now have **pinned fix commits and version-based +> `detect()`**: +> - `pack2theroot` reads PackageKit's `VersionMajor/Minor/Micro` over +> D-Bus and compares against fix release **1.3.5** (commit `76cfb675`). +> - `dirtydecrypt` uses the `kernel_range` model against mainline fix +> **`a2567217`** (Linux 7.0); kernels < 7.0 predate the vulnerable +> rxgk code per Debian's tracker. +> - `fragnesia` uses `kernel_range` against mainline **7.0.9**; older +> Debian-stable branches (5.10/6.1/6.12) are still listed vulnerable +> on Debian's tracker β€” backport entries will extend the table as +> distros publish them. > -> `dirtydecrypt` and `fragnesia` are precondition-only β€” their CVE fix -> commits are not yet pinned in the modules, so `detect()` returns -> `PRECOND_FAIL` / `TEST_ERROR` unless `--active` empirically fires the -> primitive against a `/tmp` sentinel. `--auto` auto-enables active -> probes (forked per module so a probe crash cannot tear down the -> scan), which lets all three become candidates on a vulnerable host. -> See each module's `MODULE.md`. +> `--auto` auto-enables active probes (forked per module so a probe +> crash cannot tear down the scan), which lets all three give an +> empirical confirmation on top of the version verdict. See each +> module's `MODULE.md`. Every module ships a `NOTICE.md` crediting the original CVE reporter and PoC author. `skeletonkey --dump-offsets` populates the @@ -85,8 +89,8 @@ root on a host can upstream their kernel's offsets via PR. | CVE-2021-33909 | Sequoia β€” `seq_file` size_t overflow β†’ kernel stack OOB | LPE (kernel stack OOB write) | mainline 5.13.4 / 5.10.52 / 5.4.134 (Jul 2021) | `sequoia` | 🟑 | Qualys Sequoia. `size_t`-to-`int` conversion in `seq_file` drives an OOB write off the kernel stack via a deeply-nested directory mount. Primitive-only β€” fires the overflow + records a witness; no portable cred chain. Branch backports: 5.13.4 / 5.10.52 / 5.4.134. Ships auditd rule. | | CVE-2023-22809 | sudoedit `EDITOR`/`VISUAL` `--` argv escape | LPE (userspace setuid sudoedit) | sudo 1.9.12p2 (Jan 2023) | `sudoedit_editor` | 🟒 | Structural argv-injection β€” an extra `--` in `EDITOR`/`VISUAL` makes setuid `sudoedit` open an attacker-chosen file as root. No kernel state, no offsets, no race. Affects sudo 1.8.0 ≀ V < 1.9.12p2. Ships auditd + sigma rules. | | CVE-2023-2008 | vmwgfx DRM buffer-object size-validation OOB | LPE (kernel R/W via kmalloc-512 OOB) | mainline 6.3-rc6 (Apr 2023) | `vmwgfx` | 🟑 | vmwgfx DRM `bo` size-validation gap β†’ OOB write in kmalloc-512. Affects 4.0 ≀ K < 6.3-rc6 on hosts with the `vmwgfx` module loaded (VMware guests). Primitive-only β€” fires the OOB + slab witness; no cred chain. Branch backports: 6.2.10 / 6.1.23. Ships auditd rule. | -| CVE-2026-31635 | DirtyDecrypt / DirtyCBC β€” rxgk missing-COW in-place decrypt | LPE (page-cache write into a setuid binary) | duplicate of an already-patched mainline flaw (fix commit not yet pinned) | `dirtydecrypt` | 🟑 | **Ported from the public V12 PoC, not yet VM-verified.** Sibling of Copy Fail / Dirty Frag in the rxgk (AFS rxrpc encryption) subsystem. `fire()` sliding-window page-cache write, ~256 fires/byte; rewrites the first 120 bytes of `/usr/bin/su` with a setuid-shell ELF. `--active` probe fires the primitive at a `/tmp` sentinel. detect() is precondition-only β€” see MODULE.md. x86_64. | -| CVE-2026-46300 | Fragnesia β€” XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | distro patches 2026-05-13; mainline fix followed (commit not yet pinned) | `fragnesia` | 🟑 | **Ported from the public V12 PoC, not yet VM-verified.** Latent bug exposed by the Dirty Frag fix (`f4c50a4034e6`). AF_ALG GCM keystream table + userns/netns + XFRM ESP-in-TCP splice trigger pair; rewrites the first 192 bytes of `/usr/bin/su`. Needs `CONFIG_INET_ESPINTCP` + unprivileged userns (the in-scope question the old `_stubs/fragnesia_TBD` raised β€” resolved: ships, reports PRECOND_FAIL when the userns gate is closed). PoC's ANSI TUI dropped in the port. x86_64. | +| CVE-2026-31635 | DirtyDecrypt / DirtyCBC β€” rxgk missing-COW in-place decrypt | LPE (page-cache write into a setuid binary) | mainline Linux 7.0 (commit `a2567217ade970ecc458144b6be469bc015b23e5`) | `dirtydecrypt` | 🟑 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Sibling of Copy Fail / Dirty Frag in the rxgk (AFS rxrpc encryption) subsystem. `fire()` sliding-window page-cache write, ~256 fires/byte; rewrites the first 120 bytes of `/usr/bin/su` with a setuid-shell ELF. detect() is version-pinned: kernels < 7.0 predate the vulnerable rxgk code (Debian: `` for 5.10/6.1/6.12); kernels β‰₯ 7.0 have the fix. `--active` probe fires the primitive at a `/tmp` sentinel for empirical override. x86_64. | +| CVE-2026-46300 | Fragnesia β€” XFRM ESP-in-TCP `skb_try_coalesce` SHARED_FRAG loss | LPE (page-cache write into a setuid binary) | mainline 7.0.9; older Debian-stable branches still unfixed as of 2026-05-22 | `fragnesia` | 🟑 | **Ported from the public V12 PoC, exploit body not yet VM-verified.** Latent bug exposed by the Dirty Frag fix (`f4c50a4034e6`). AF_ALG GCM keystream table + userns/netns + XFRM ESP-in-TCP splice trigger pair; rewrites the first 192 bytes of `/usr/bin/su`. Needs `CONFIG_INET_ESPINTCP` + unprivileged userns (the in-scope question the old `_stubs/fragnesia_TBD` raised β€” resolved: ships, reports PRECOND_FAIL when the userns gate is closed). detect() is version-pinned at 7.0.9; older branches that haven't backported yet are flagged VULNERABLE on the version check (override empirically via `--active`). PoC's ANSI TUI dropped in the port. x86_64. | | CVE-2026-41651 | Pack2TheRoot β€” PackageKit `InstallFiles` TOCTOU | LPE (userspace D-Bus daemon β†’ `.deb` postinst as root) | PackageKit 1.3.5 (commit `76cfb675`, 2026-04-22) | `pack2theroot` | 🟑 | **Ported from the public Vozec PoC, not yet VM-verified.** Two back-to-back `InstallFiles` D-Bus calls β€” first `SIMULATE` (polkit bypass + queues a GLib idle), then immediately `NONE` + malicious `.deb` (overwrites the cached flags before the idle fires). GLib priority ordering makes the overwrite deterministic, not a race. Disclosure by **Deutsche Telekom security**. Affects PackageKit 1.0.2 β†’ 1.3.4 β€” default-enabled on Ubuntu Desktop, Debian, Fedora, Rocky/RHEL via Cockpit. `detect()` reads `VersionMajor/Minor/Micro` over D-Bus β†’ high-confidence verdict (vs. precondition-only for dirtydecrypt/fragnesia). Debian-family only (PoC's built-in `.deb` builder). Needs `libglib2.0-dev` at build time; Makefile autodetects via `pkg-config gio-2.0` and falls through to a stub when absent. | ## Operations supported per module diff --git a/README.md b/README.md index c46e087..7056ef7 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ for every CVE in the bundle β€” same project for red and blue teams. |---|---|---| | 🟒 Full chain | **14** | Lands root (or its canonical capability) end-to-end. No per-kernel offsets needed. | | 🟑 Primitive | **14** | Fires the kernel primitive + grooms the slab + records a witness. Default returns `EXPLOIT_FAIL` honestly. Pass `--full-chain` to engage the shared `modprobe_path` finisher (needs offsets β€” see [`docs/OFFSETS.md`](docs/OFFSETS.md)). | -| βšͺ Ported, unverified | **3** | `dirtydecrypt`, `fragnesia`, `pack2theroot`. Built and registered, but **not yet validated end-to-end** β€” for the page-cache pair `detect()` is precondition-only; for `pack2theroot` the fix release IS pinned (high-confidence verdict). `--auto` auto-enables `--active` so the probes turn into definitive verdicts on a vulnerable host. Excluded from the 28-module verified counts above. | +| βšͺ Ported, unverified | **3** | `dirtydecrypt`, `fragnesia`, `pack2theroot`. Built and registered with **version-pinned `detect()`** (Linux 7.0 / 7.0.9 / PackageKit 1.3.5 respectively), but the **exploit bodies** are not yet validated end-to-end. `--auto` auto-enables `--active` to confirm empirically on top of the version verdict. Excluded from the 28-module verified counts above. | **🟒 Modules that land root on a vulnerable host:** copy_fail family Γ—5 Β· dirty_pipe Β· dirty_cow Β· pwnkit Β· overlayfs @@ -66,13 +66,13 @@ netfilter_xtcompat Β· stackrot Β· sudo_samedit Β· sequoia Β· vmwgfx **βšͺ Ported-but-unverified (not in the counts above):** dirtydecrypt (CVE-2026-31635) Β· fragnesia (CVE-2026-46300) Β· -pack2theroot (CVE-2026-41651) β€” ported from public PoCs, **not yet -VM-validated**. The two page-cache writes (dirtydecrypt, fragnesia) -have precondition-only `detect()` because the CVE fix commits are not -yet pinned in the modules. `pack2theroot` is a userspace D-Bus -PackageKit TOCTOU; its fix release (PackageKit 1.3.5, commit -`76cfb675`) is pinned and `detect()` reads the daemon's version over -D-Bus β€” high-confidence verdict. +pack2theroot (CVE-2026-41651) β€” ported from public PoCs, **exploit +bodies not yet VM-validated**. All three have version-pinned `detect()`: +`dirtydecrypt` against mainline fix commit `a2567217` in Linux 7.0; +`fragnesia` against mainline 7.0.9 (older Debian-stable branches still +unfixed); `pack2theroot` against PackageKit fix release 1.3.5 +(commit `76cfb675`), version read from the daemon over D-Bus. +`--auto` auto-enables `--active` to confirm empirically on top. See [`CVES.md`](CVES.md) for per-module CVE, kernel range, and detection status. diff --git a/modules/dirtydecrypt_cve_2026_31635/MODULE.md b/modules/dirtydecrypt_cve_2026_31635/MODULE.md index f8f7d49..986efe1 100644 --- a/modules/dirtydecrypt_cve_2026_31635/MODULE.md +++ b/modules/dirtydecrypt_cve_2026_31635/MODULE.md @@ -59,19 +59,23 @@ The exploit rewrites the first 120 bytes of a setuid-root binary This module is a **faithful port** of , compiled -into the SKELETONKEY module interface. It has **not** been validated -end-to-end against a known-vulnerable kernel inside the SKELETONKEY CI -matrix. +into the SKELETONKEY module interface. The **exploit body** has not +been validated end-to-end against a known-vulnerable kernel inside the +SKELETONKEY CI matrix. -`detect()` deliberately does **not** return a kernel-version-based -patched/vulnerable verdict: the CVE-2026-31635 fix commit is not yet -pinned here, and fabricating a `kernel_patched_from` table would -violate the project's no-fabrication rule (`CVES.md`). Instead: +**`detect()` is now version-pinned** against the mainline fix commit +[`a2567217ade970ecc458144b6be469bc015b23e5`][fix] (Linux 7.0): kernels +< 7.0 predate the vulnerable rxgk RESPONSE-handling code (Debian +tracker confirms older stable branches as ), kernels β‰₯ 7.0 have the fix. With `--active`, the +detector runs the rxgk primitive against a `/tmp` sentinel and reports +empirically β€” catches pre-fix 7.0-rc kernels and any distro rebuilds +the version check misses. -- preconditions missing β†’ `PRECOND_FAIL` -- preconditions present, no `--active` β†’ `TEST_ERROR` ("cannot - determine passively") so `--auto` does not fire it blind -- `--active` β†’ empirical VULNERABLE / OK via the `/tmp` sentinel probe +[fix]: https://git.kernel.org/linus/a2567217ade970ecc458144b6be469bc015b23e5 -**Before promoting to 🟒:** pin the fix commit + branch-backport -thresholds, add a `kernel_range`, and validate on a vulnerable VM. +**Before promoting to 🟒:** validate the exploit end-to-end on a 7.0-rc +kernel that pre-dates commit `a2567217ade…`. The Debian tracker entry +for CVE-2026-31635 is the source of truth for branch-backport +thresholds; extend the `kernel_range` table when distros publish +stable backports. diff --git a/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c b/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c index a9402e5..34d2a41 100644 --- a/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c +++ b/modules/dirtydecrypt_cve_2026_31635/skeletonkey_modules.c @@ -45,6 +45,7 @@ /* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level * Makefile; do not redefine here (warning: redefined). */ +#include "../../core/kernel_range.h" #include #include #include @@ -655,12 +656,49 @@ static int dd_active_probe(void) return result; } +/* + * CVE-2026-31635 affects kernels with the rxgk RESPONSE-handling code + * (CONFIG_RXGK). Per Debian's tracker, the vulnerable code was + * introduced in the 7.0 development cycle β€” older mainline branches + * (bullseye 5.10 / bookworm 6.1 / trixie 6.12) are . The fix is upstream commit + * a2567217ade970ecc458144b6be469bc015b23e5 ("rxrpc: fix oversized + * RESPONSE authenticator length check"), shipped in Linux 7.0. + * + * The detect logic therefore is: + * - kernel < 7.0 β†’ SKELETONKEY_OK (predates the bug) + * - kernel β‰₯ 7.0 β†’ consult kernel_range; 7.0+ has the fix + * - --active β†’ empirical override (catches pre-fix 7.0-rc kernels + * or weird distro rebuilds the version check missed) + */ +static const struct kernel_patched_from dirtydecrypt_patched_branches[] = { + {7, 0, 0}, /* mainline fix commit a2567217 landed in Linux 7.0 */ +}; +static const struct kernel_range dirtydecrypt_range = { + .patched_from = dirtydecrypt_patched_branches, + .n_patched_from = sizeof(dirtydecrypt_patched_branches) / + sizeof(dirtydecrypt_patched_branches[0]), +}; + static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx) { dd_verbose = !ctx->json; - struct utsname u; - uname(&u); + struct kernel_version v; + if (!kernel_version_current(&v)) { + if (!ctx->json) + fprintf(stderr, "[!] dirtydecrypt: could not parse kernel version\n"); + return SKELETONKEY_TEST_ERROR; + } + + /* Predates the bug: rxgk RESPONSE-handling code was added in 7.0. */ + if (v.major < 7) { + if (!ctx->json) + fprintf(stderr, "[i] dirtydecrypt: kernel %s predates the rxgk " + "RESPONSE-handling code added in 7.0 β€” not applicable\n", + v.release); + return SKELETONKEY_OK; + } /* Precondition: AF_RXRPC must be reachable for the primitive. */ int s = socket(AF_RXRPC, SOCK_DGRAM, PF_INET); @@ -680,6 +718,8 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx) return SKELETONKEY_PRECOND_FAIL; } + bool patched_by_version = kernel_range_is_patched(&dirtydecrypt_range, &v); + if (ctx->active_probe) { if (!ctx->json) fprintf(stderr, "[*] dirtydecrypt: running active sentinel " @@ -689,30 +729,34 @@ static skeletonkey_result_t dd_detect(const struct skeletonkey_ctx *ctx) if (!ctx->json) fprintf(stderr, "[!] dirtydecrypt: ACTIVE PROBE " "CONFIRMED β€” rxgk in-place decrypt corrupts " - "the page cache (kernel %s)\n", u.release); + "the page cache (kernel %s)\n", v.release); return SKELETONKEY_VULNERABLE; } if (p == 0) { if (!ctx->json) fprintf(stderr, "[+] dirtydecrypt: active probe did " - "not land β€” primitive blocked (patched)\n"); + "not land β€” primitive blocked (likely patched%s)\n", + patched_by_version ? "" : ", or distro silently fixed"); return SKELETONKEY_OK; } if (!ctx->json) fprintf(stderr, "[?] dirtydecrypt: active probe machinery " - "failed; falling back to precondition verdict\n"); + "failed; falling back to version verdict\n"); } - /* No version-based verdict: the CVE-2026-31635 fix commit is not - * pinned in this module yet (see MODULE.md). Preconditions are - * present but patched/vulnerable cannot be determined passively β€” - * report TEST_ERROR so --auto does not fire blind. Use --active to - * confirm empirically, or --exploit dirtydecrypt --i-know directly. */ + if (patched_by_version) { + if (!ctx->json) + fprintf(stderr, "[+] dirtydecrypt: kernel %s is patched " + "(commit a2567217 in Linux 7.0; version-only check β€” " + "use --active to confirm)\n", v.release); + return SKELETONKEY_OK; + } if (!ctx->json) - fprintf(stderr, "[?] dirtydecrypt: AF_RXRPC reachable on kernel %s; " - "patch-level cannot be determined passively.\n" - " Confirm with: skeletonkey --scan --active\n", u.release); - return SKELETONKEY_TEST_ERROR; + fprintf(stderr, "[!] dirtydecrypt: kernel %s appears VULNERABLE " + "(in 7.0-rc window before commit a2567217; version-only)\n" + " Confirm empirically: skeletonkey --scan --active\n", + v.release); + return SKELETONKEY_VULNERABLE; } /* ---- exploit ------------------------------------------------------ */ @@ -897,7 +941,7 @@ const struct skeletonkey_module dirtydecrypt_module = { .cve = "CVE-2026-31635", .summary = "rxgk missing-COW in-place decrypt β†’ page-cache write into a setuid binary", .family = "dirtydecrypt", - .kernel_range = "kernels exposing AF_RXRPC + rxgk (security index 6); fix commit not yet pinned", + .kernel_range = "Linux 7.0 (vulnerable rxgk code added in 7.0); mainline fix commit a2567217 in 7.0", .detect = dd_detect, .exploit = dd_exploit, .mitigate = NULL, diff --git a/modules/fragnesia_cve_2026_46300/MODULE.md b/modules/fragnesia_cve_2026_46300/MODULE.md index 3741ded..d75b3c2 100644 --- a/modules/fragnesia_cve_2026_46300/MODULE.md +++ b/modules/fragnesia_cve_2026_46300/MODULE.md @@ -68,18 +68,20 @@ The exploit mechanism itself is reproduced faithfully. This module is a **faithful port** of , compiled -into the SKELETONKEY module interface. It has **not** been validated -end-to-end against a known-vulnerable kernel inside the SKELETONKEY CI -matrix. +into the SKELETONKEY module interface. The **exploit body** has not +been validated end-to-end against a known-vulnerable kernel inside the +SKELETONKEY CI matrix. -`detect()` deliberately does **not** return a kernel-version-based -patched/vulnerable verdict: the CVE-2026-46300 fix commit is not yet -pinned here. Instead: +**`detect()` is now version-pinned**: the Fragnesia fix ships in +mainline Linux **7.0.9** (Debian tracker source-of-truth, `linux +unstable: 7.0.9-1 fixed`). The `kernel_range` table marks the 7.0.x +branch patched at `7.0.9`; older Debian-stable branches (5.10 / 6.1 / +6.12) are currently still vulnerable per the tracker. With `--active`, +the detector runs the full ESP-in-TCP primitive against a `/tmp` file +and reports empirically β€” catches stable-branch backports the version +table doesn't know about, and CONFIG_INET_ESPINTCP=n kernels where the +primitive is structurally unreachable. -- preconditions missing β†’ `PRECOND_FAIL` -- preconditions present, no `--active` β†’ `TEST_ERROR` so `--auto` does - not fire it blind -- `--active` β†’ empirical VULNERABLE / OK via the `/tmp` sentinel probe - -**Before promoting to 🟒:** pin the fix commit + branch-backport -thresholds, add a `kernel_range`, and validate on a vulnerable VM. +**Before promoting to 🟒:** validate the exploit end-to-end on a +≀ 7.0.8 kernel. Extend the `kernel_range` table with backport +thresholds for 5.10 / 6.1 / 6.12 as distros publish them. diff --git a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c index ec70b49..c9d68f9 100644 --- a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c +++ b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c @@ -52,6 +52,7 @@ /* _GNU_SOURCE / _FILE_OFFSET_BITS are passed via -D in the top-level * Makefile; do not redefine here (warning: redefined). */ +#include "../../core/kernel_range.h" #include #include #include @@ -894,12 +895,44 @@ static int fg_active_probe(void) return result; } +/* + * CVE-2026-46300 is a latent skb_try_coalesce() bug exposed by the + * Dirty Frag remediation (commit f4c50a4034e6) which landed in Linux + * 7.0. The Fragnesia fix shipped in the 7.0.x stable series at 7.0.9 + * per Debian's tracker (linux unstable: 7.0.9-1 fixed). Older Debian + * stable branches (bullseye 5.10, bookworm 6.1, trixie 6.12) are + * still marked vulnerable as of 2026-05-22 β€” backports may follow. + * + * The detect logic: + * - kernel β‰₯ 7.0.9 β†’ patched on the 7.0.x branch + * - kernel on 5.10/6.1/6.12 (other branches without a backport + * entry in this table) β†’ version says + * VULNERABLE; --active confirms empirically + * - --active β†’ empirical override (catches distro silent + * backports and unfixed 7.0.x ≀ 7.0.8) + * + * Stable-branch backports for 5.10 / 6.1 / 6.12 β€” when they ship β€” + * extend the table with the matching {major, minor, patch} entry. + */ +static const struct kernel_patched_from fragnesia_patched_branches[] = { + {7, 0, 9}, /* mainline + 7.0.x stable: fix lands at 7.0.9 */ +}; +static const struct kernel_range fragnesia_range = { + .patched_from = fragnesia_patched_branches, + .n_patched_from = sizeof(fragnesia_patched_branches) / + sizeof(fragnesia_patched_branches[0]), +}; + static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) { fg_verbose = !ctx->json; - struct utsname u; - uname(&u); + struct kernel_version v; + if (!kernel_version_current(&v)) { + if (!ctx->json) + fprintf(stderr, "[!] fragnesia: could not parse kernel version\n"); + return SKELETONKEY_TEST_ERROR; + } if (!fg_userns_allowed()) { if (!ctx->json) @@ -917,6 +950,8 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) return SKELETONKEY_PRECOND_FAIL; } + bool patched_by_version = kernel_range_is_patched(&fragnesia_range, &v); + if (ctx->active_probe) { if (!ctx->json) fprintf(stderr, "[*] fragnesia: running active probe " @@ -926,30 +961,36 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) if (!ctx->json) fprintf(stderr, "[!] fragnesia: ACTIVE PROBE " "CONFIRMED β€” ESP-in-TCP coalesce corrupts " - "the page cache (kernel %s)\n", u.release); + "the page cache (kernel %s)\n", v.release); return SKELETONKEY_VULNERABLE; } if (p == 0) { if (!ctx->json) fprintf(stderr, "[+] fragnesia: active probe did " - "not land β€” primitive blocked (patched, " - "or CONFIG_INET_ESPINTCP off)\n"); + "not land β€” primitive blocked (likely " + "patched%s, or CONFIG_INET_ESPINTCP off)\n", + patched_by_version ? "" : "; distro may have " + "backported, or Dirty Frag is unpatched here"); return SKELETONKEY_OK; } if (!ctx->json) fprintf(stderr, "[?] fragnesia: active probe machinery " - "failed; falling back to precondition verdict\n"); + "failed; falling back to version verdict\n"); } - /* No version-based verdict: the CVE-2026-46300 fix commit is not - * pinned in this module yet (see MODULE.md). Report TEST_ERROR so - * --auto does not fire blind; use --active to confirm. */ + if (patched_by_version) { + if (!ctx->json) + fprintf(stderr, "[+] fragnesia: kernel %s is patched " + "(7.0.9+; version-only check β€” use --active to " + "confirm)\n", v.release); + return SKELETONKEY_OK; + } if (!ctx->json) - fprintf(stderr, "[?] fragnesia: userns+XFRM preconditions present " - "on kernel %s; patch-level cannot be determined " - "passively.\n Confirm with: skeletonkey --scan --active\n", - u.release); - return SKELETONKEY_TEST_ERROR; + fprintf(stderr, "[!] fragnesia: kernel %s appears VULNERABLE " + "(no backport entry for this branch; version-only)\n" + " Confirm empirically: skeletonkey --scan --active\n", + v.release); + return SKELETONKEY_VULNERABLE; } /* ---- exploit ------------------------------------------------------ */ @@ -1110,7 +1151,7 @@ const struct skeletonkey_module fragnesia_module = { .cve = "CVE-2026-46300", .summary = "XFRM ESP-in-TCP skb_try_coalesce SHARED_FRAG loss β†’ page-cache write", .family = "fragnesia", - .kernel_range = "kernels with CONFIG_INET_ESPINTCP after the Dirty Frag fix; fix commit not yet pinned", + .kernel_range = "Linux with CONFIG_INET_ESPINTCP and the Dirty Frag fix; mainline fix in 7.0.9 (older branches still unfixed)", .detect = fg_detect, .exploit = fg_exploit, .mitigate = NULL, diff --git a/skeletonkey.c b/skeletonkey.c index 0e54136..47af057 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -677,7 +677,7 @@ static int module_safety_rank(const char *n) if (!strncmp(n, "copy_fail", 9) || !strncmp(n, "dirty_frag", 10)) return 88; /* verified page-cache writes */ if (!strcmp(n, "dirtydecrypt") || - !strcmp(n, "fragnesia")) return 86; /* ported page-cache writes, NOT VM-verified */ + !strcmp(n, "fragnesia")) return 87; /* ported page-cache writes; version-pinned detect, exploit NOT VM-verified */ if (!strcmp(n, "ptrace_traceme")) return 85; /* userspace cred race */ if (!strcmp(n, "sudo_samedit")) return 80; /* heap-tuned, may crash sudo */ if (!strcmp(n, "af_unix_gc")) return 25; /* kernel race, low win% */