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:
2026-05-22 23:06:15 -04:00
parent cdb8f5e8f9
commit a26f471ecf
7 changed files with 175 additions and 80 deletions
+17 -13
View File
@@ -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: 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,
+15 -13
View File
@@ -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: 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,