dirtydecrypt + fragnesia: pin CVE fix commits, version-based detect()
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
<not-affected, vulnerable code not present>: 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.
This commit is contained in:
@@ -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: `<not-affected, vulnerable code not present>` 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -59,19 +59,23 @@ The exploit rewrites the first 120 bytes of a setuid-root binary
|
||||
|
||||
This module is a **faithful port** of
|
||||
<https://github.com/v12-security/pocs/tree/main/dirtydecrypt>, 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 <not-affected, vulnerable
|
||||
code not present>), 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.
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
@@ -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 <not-affected,
|
||||
* vulnerable code not present>. 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: 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 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: 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,
|
||||
|
||||
@@ -68,18 +68,20 @@ The exploit mechanism itself is reproduced faithfully.
|
||||
|
||||
This module is a **faithful port** of
|
||||
<https://github.com/v12-security/pocs/tree/main/fragnesia>, 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.
|
||||
|
||||
@@ -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 <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -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: 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 is patched "
|
||||
"(7.0.9+; version-only check — use --active to "
|
||||
"confirm)\n", v.release);
|
||||
return SKELETONKEY_OK;
|
||||
}
|
||||
if (!ctx->json)
|
||||
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,
|
||||
|
||||
+1
-1
@@ -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% */
|
||||
|
||||
Reference in New Issue
Block a user