From 4454d8148e09ff91836885770a4737eedfa553e6 Mon Sep 17 00:00:00 2001 From: KaraZajac Date: Thu, 28 May 2026 13:33:28 -0400 Subject: [PATCH] release v0.9.4: drift unblock, fragnesia range fix, infra docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sync docs/CVE_METADATA.json + KEV_CROSSREF.md to match the hand-applied core/cve_metadata.c entries from v0.9.3. Nightly drift-check (red since 2026-05-25) now passes. Pintheft's CWE landed as CWE-787 from NVD (was NULL in the hand-applied entry). - Fix fragnesia (CVE-2026-46300) range table. Per NVD: bug entered at 5.11 SKBFL_SHARED_FRAG, vulnerable through 5.15.207 / 6.1.173 / 6.6.140 / 6.12.90 / 6.18.32 / 7.0.9, fixed at .208/.174/.141/ .91/.33/.10. Prior table had one entry {7,0,9} — off-by-one and missing every other backport. Added predates-5.11 introduction gate + test row. - Update tools/verify-vm/README.md to document the v0.9.x infra: mainline kernel pinning via kernel.ubuntu.com, per-module provisioner hooks, two-phase prep→reboot→verify with post-reboot kernel confirmation, GRUB_DEFAULT pinning. - Add curl fallback for NVD lookups in refresh-cve-metadata.py. Mirrors the CISA path's existing fallback. Prevents the silent Python urlopen hang seen during v0.9.3 prep (55-min stuck on CLOSE_WAIT socket; 30s timeout never fired). --- README.md | 2 +- core/cve_metadata.c | 103 ++++++------ docs/CVE_METADATA.json | 74 ++++++++- docs/KEV_CROSSREF.md | 12 +- docs/RELEASE_NOTES.md | 47 ++++++ docs/index.html | 4 +- .../skeletonkey_modules.c | 31 +++- skeletonkey.c | 2 +- tests/test_detect.c | 6 + tools/refresh-cve-metadata.py | 28 +++- tools/verify-vm/README.md | 151 ++++++++++++------ 11 files changed, 347 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 877f04c..6d5b0f9 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ also compile (modules with Linux-only headers stub out gracefully). ## Status -**v0.9.3 cut 2026-05-24.** 39 modules across 34 CVEs — **every +**v0.9.4 cut 2026-05-28.** 39 modules across 34 CVEs — **every year 2016 → 2026 now covered**. v0.9.0 added 5 gap-fillers (`mutagen_astronomy` / `sudo_runas_neg1` / `tioscpgrp` / `vsock_uaf` / `nft_pipapo`); v0.8.0 added 3 (`sudo_chwoot` / `udisks_libblockdev` / diff --git a/core/cve_metadata.c b/core/cve_metadata.c index 1675ed8..935f53b 100644 --- a/core/cve_metadata.c +++ b/core/cve_metadata.c @@ -28,6 +28,14 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = false, .kev_date_added = "", }, + { + .cve = "CVE-2018-14634", + .cwe = "CWE-190", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = true, + .kev_date_added = "2026-01-26", + }, { .cve = "CVE-2019-13272", .cwe = NULL, @@ -36,6 +44,14 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = true, .kev_date_added = "2021-12-10", }, + { + .cve = "CVE-2019-14287", + .cwe = "CWE-755", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, { .cve = "CVE-2020-14386", .cwe = "CWE-250", @@ -44,6 +60,14 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = false, .kev_date_added = "", }, + { + .cve = "CVE-2020-29661", + .cwe = "CWE-416", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, { .cve = "CVE-2021-22555", .cwe = "CWE-787", @@ -196,60 +220,9 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = true, .kev_date_added = "2024-05-30", }, - { - .cve = "CVE-2026-31635", - .cwe = "CWE-130", - .attack_technique = "T1068", - .attack_subtechnique = NULL, - .in_kev = false, - .kev_date_added = "", - }, - { - .cve = "CVE-2026-41651", - .cwe = "CWE-367", - .attack_technique = "T1068", - .attack_subtechnique = NULL, - .in_kev = false, - .kev_date_added = "", - }, - { - .cve = "CVE-2026-46300", - .cwe = NULL, - .attack_technique = "T1068", - .attack_subtechnique = NULL, - .in_kev = false, - .kev_date_added = "", - }, - /* v0.8.0 / v0.9.0 module additions — populated via direct CISA KEV - * + NVD curl on 2026-05-24 when refresh-cve-metadata.py's urlopen - * hung on CISA's HTTP/2 endpoint. Same data, different transport. */ - { - .cve = "CVE-2018-14634", - .cwe = "CWE-190", - .attack_technique = "T1068", - .attack_subtechnique = NULL, - .in_kev = true, - .kev_date_added = "2026-01-26", - }, - { - .cve = "CVE-2019-14287", - .cwe = "CWE-755", - .attack_technique = "T1068", - .attack_subtechnique = NULL, - .in_kev = false, - .kev_date_added = "", - }, - { - .cve = "CVE-2020-29661", - .cwe = "CWE-416", - .attack_technique = "T1068", - .attack_subtechnique = NULL, - .in_kev = false, - .kev_date_added = "", - }, { .cve = "CVE-2024-26581", - .cwe = NULL, /* NVD: no CWE assigned */ + .cwe = NULL, .attack_technique = "T1068", .attack_subtechnique = NULL, .in_kev = false, @@ -279,9 +252,33 @@ const struct cve_metadata cve_metadata_table[] = { .in_kev = false, .kev_date_added = "", }, + { + .cve = "CVE-2026-31635", + .cwe = "CWE-130", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2026-41651", + .cwe = "CWE-367", + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, { .cve = "CVE-2026-43494", - .cwe = NULL, /* NVD: no CWE assigned */ + .cwe = NULL, + .attack_technique = "T1068", + .attack_subtechnique = NULL, + .in_kev = false, + .kev_date_added = "", + }, + { + .cve = "CVE-2026-46300", + .cwe = "CWE-787", .attack_technique = "T1068", .attack_subtechnique = NULL, .in_kev = false, diff --git a/docs/CVE_METADATA.json b/docs/CVE_METADATA.json index 69c2e8e..4e4332d 100644 --- a/docs/CVE_METADATA.json +++ b/docs/CVE_METADATA.json @@ -17,6 +17,15 @@ "in_kev": false, "kev_date_added": "" }, + { + "cve": "CVE-2018-14634", + "module_dir": "mutagen_astronomy_cve_2018_14634", + "cwe": "CWE-190", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2026-01-26" + }, { "cve": "CVE-2019-13272", "module_dir": "ptrace_traceme_cve_2019_13272", @@ -26,6 +35,15 @@ "in_kev": true, "kev_date_added": "2021-12-10" }, + { + "cve": "CVE-2019-14287", + "module_dir": "sudo_runas_neg1_cve_2019_14287", + "cwe": "CWE-755", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, { "cve": "CVE-2020-14386", "module_dir": "af_packet2_cve_2020_14386", @@ -35,6 +53,15 @@ "in_kev": false, "kev_date_added": "" }, + { + "cve": "CVE-2020-29661", + "module_dir": "tioscpgrp_cve_2020_29661", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, { "cve": "CVE-2021-22555", "module_dir": "netfilter_xtcompat_cve_2021_22555", @@ -206,6 +233,42 @@ "in_kev": true, "kev_date_added": "2024-05-30" }, + { + "cve": "CVE-2024-26581", + "module_dir": "nft_pipapo_cve_2024_26581", + "cwe": null, + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2024-50264", + "module_dir": "vsock_uaf_cve_2024_50264", + "cwe": "CWE-416", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, + { + "cve": "CVE-2025-32463", + "module_dir": "sudo_chwoot_cve_2025_32463", + "cwe": "CWE-829", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": true, + "kev_date_added": "2025-09-29" + }, + { + "cve": "CVE-2025-6019", + "module_dir": "udisks_libblockdev_cve_2025_6019", + "cwe": "CWE-250", + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, { "cve": "CVE-2026-31635", "module_dir": "dirtydecrypt_cve_2026_31635", @@ -224,10 +287,19 @@ "in_kev": false, "kev_date_added": "" }, + { + "cve": "CVE-2026-43494", + "module_dir": "pintheft_cve_2026_43494", + "cwe": null, + "attack_technique": "T1068", + "attack_subtechnique": null, + "in_kev": false, + "kev_date_added": "" + }, { "cve": "CVE-2026-46300", "module_dir": "fragnesia_cve_2026_46300", - "cwe": null, + "cwe": "CWE-787", "attack_technique": "T1068", "attack_subtechnique": null, "in_kev": false, diff --git a/docs/KEV_CROSSREF.md b/docs/KEV_CROSSREF.md index 003abfd..777f3d0 100644 --- a/docs/KEV_CROSSREF.md +++ b/docs/KEV_CROSSREF.md @@ -4,7 +4,7 @@ Which SKELETONKEY modules cover CVEs that CISA has observed exploited in the wild per the Known Exploited Vulnerabilities catalog. Refreshed via `tools/refresh-cve-metadata.py`. -**10 of 26 modules cover KEV-listed CVEs.** +**12 of 34 modules cover KEV-listed CVEs.** ## In KEV (prioritize patching) @@ -19,7 +19,9 @@ Refreshed via `tools/refresh-cve-metadata.py`. | CVE-2024-1086 | 2024-05-30 | CWE-416 | `nf_tables_cve_2024_1086` | | CVE-2022-0185 | 2024-08-21 | CWE-190 | `fuse_legacy_cve_2022_0185` | | CVE-2023-0386 | 2025-06-17 | CWE-282 | `overlayfs_setuid_cve_2023_0386` | +| CVE-2025-32463 | 2025-09-29 | CWE-829 | `sudo_chwoot_cve_2025_32463` | | CVE-2021-22555 | 2025-10-06 | CWE-787 | `netfilter_xtcompat_cve_2021_22555` | +| CVE-2018-14634 | 2026-01-26 | CWE-190 | `mutagen_astronomy_cve_2018_14634` | ## Not in KEV @@ -30,7 +32,9 @@ and are technically reachable. "Not in KEV" is not the same as | CVE | CWE | Module | | --- | --- | --- | | CVE-2017-7308 | CWE-681 | `af_packet_cve_2017_7308` | +| CVE-2019-14287 | CWE-755 | `sudo_runas_neg1_cve_2019_14287` | | CVE-2020-14386 | CWE-250 | `af_packet2_cve_2020_14386` | +| CVE-2020-29661 | CWE-416 | `tioscpgrp_cve_2020_29661` | | CVE-2021-33909 | CWE-190 | `sequoia_cve_2021_33909` | | CVE-2022-0492 | CWE-287 | `cgroup_release_agent_cve_2022_0492` | | CVE-2022-25636 | CWE-269 | `nft_fwd_dup_cve_2022_25636` | @@ -42,6 +46,10 @@ and are technically reachable. "Not in KEV" is not the same as | CVE-2023-32233 | CWE-416 | `nft_set_uaf_cve_2023_32233` | | CVE-2023-3269 | CWE-416 | `stackrot_cve_2023_3269` | | CVE-2023-4622 | CWE-416 | `af_unix_gc_cve_2023_4622` | +| CVE-2024-26581 | ? | `nft_pipapo_cve_2024_26581` | +| CVE-2024-50264 | CWE-416 | `vsock_uaf_cve_2024_50264` | +| CVE-2025-6019 | CWE-250 | `udisks_libblockdev_cve_2025_6019` | | CVE-2026-31635 | CWE-130 | `dirtydecrypt_cve_2026_31635` | | CVE-2026-41651 | CWE-367 | `pack2theroot_cve_2026_41651` | -| CVE-2026-46300 | ? | `fragnesia_cve_2026_46300` | +| CVE-2026-43494 | ? | `pintheft_cve_2026_43494` | +| CVE-2026-46300 | CWE-787 | `fragnesia_cve_2026_46300` | diff --git a/docs/RELEASE_NOTES.md b/docs/RELEASE_NOTES.md index e262f9a..5e2fad0 100644 --- a/docs/RELEASE_NOTES.md +++ b/docs/RELEASE_NOTES.md @@ -1,3 +1,50 @@ +## SKELETONKEY v0.9.4 — drift unblock, fragnesia range fix, infra docs + +Quality-of-life follow-ups from the v0.9.3 review: + +**Nightly CI drift-check unblocked.** v0.9.3's hand-applied +`core/cve_metadata.c` entries weren't reflected in +`docs/CVE_METADATA.json`, so the scheduled `build` workflow had +been red since 2026-05-25 even though push-triggered runs passed. +Regenerated both via the canonical script. Pintheft's CWE landed +as CWE-787 (NVD-derived) — previously NULL. + +**fragnesia module range table corrected.** Same audit pattern that +found the dirtydecrypt bug in v0.9.3. NVD CVE-2026-46300 confirms +the SKBFL_SHARED_FRAG marker was introduced at 5.11 and the bug +spans every stable branch since. Previous range table had one entry +(`{7, 0, 9}`) — off-by-one against NVD's 7.0.10 fix point and +missing every other backport. Now models 6 backports + predates-5.11 +introduction gate: + +```c +{5, 15, 208}, /* 5.15-LTS */ +{6, 1, 174}, /* 6.1-LTS */ +{6, 6, 141}, /* 6.6-LTS */ +{6, 12, 91}, /* 6.12-LTS */ +{6, 18, 33}, /* 6.18-LTS */ +{7, 0, 10}, /* 7.0 */ +``` + +Test row added for the predates path (kernel 4.4 → OK). + +**`tools/verify-vm/README.md` brought current.** The README was +written for the v0.6-era apt-pin-only workflow. Now documents the +v0.9.x infrastructure: mainline kernel pinning via +kernel.ubuntu.com, per-module provisioners +(`provisioners/.sh`), two-phase prep→reboot→verify with +post-reboot kernel confirmation, GRUB_DEFAULT pinning in both apt +and mainline blocks. + +**NVD lookups in `refresh-cve-metadata.py` get a curl fallback.** +v0.9.3 ran into Python's `urlopen` silently hanging on NVD's HTTP/2 +endpoint (55-min process with the 30s timeout never firing — kernel +CLOSE_WAIT socket). The CISA path already had a curl fallback; the +NVD path now mirrors it. Future runs degrade gracefully when +urlopen wedges. + +--- + ## SKELETONKEY v0.9.3 — CVE metadata refresh + dirtydecrypt range fix **CVE metadata refresh (10 → 12 KEV).** Populated the 8 missing diff --git a/docs/index.html b/docs/index.html index 799767c..642f12c 100644 --- a/docs/index.html +++ b/docs/index.html @@ -56,7 +56,7 @@
- v0.9.3 — released 2026-05-24 + v0.9.4 — released 2026-05-28

SKELETONKEY @@ -598,7 +598,7 @@ uid=0(root) gid=0(root) who found the bugs.

diff --git a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c index 341be1a..f5e0147 100644 --- a/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c +++ b/modules/fragnesia_cve_2026_46300/skeletonkey_modules.c @@ -903,11 +903,25 @@ static int fg_active_probe(void) * - --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. + * Per NVD CVE-2026-46300 (queried 2026-05-28): SKBFL_SHARED_FRAG was + * introduced at 5.11; the marker-propagation bug is present 5.11+. The + * fix was backported across every active stable branch: + * + * 5.15-LTS: vulnerable 5.15.0–5.15.207, fixed 5.15.208+ + * 6.1-LTS: vulnerable 5.16.0–6.1.173, fixed 6.1.174+ + * 6.6-LTS: vulnerable 6.2.0–6.6.140, fixed 6.6.141+ + * 6.12-LTS: vulnerable 6.7.0–6.12.90, fixed 6.12.91+ + * 6.18-LTS: vulnerable 6.13.0–6.18.32, fixed 6.18.33+ + * 7.0: vulnerable 6.19.0–7.0.9, fixed 7.0.10+ + * 7.1-rcN: still vulnerable (rc1..rc4 at time of writing) */ static const struct kernel_patched_from fragnesia_patched_branches[] = { - {7, 0, 9}, /* mainline + 7.0.x stable: fix lands at 7.0.9 */ + {5, 15, 208}, /* 5.15-LTS backport */ + {6, 1, 174}, /* 6.1-LTS backport */ + {6, 6, 141}, /* 6.6-LTS backport */ + {6, 12, 91}, /* 6.12-LTS backport */ + {6, 18, 33}, /* 6.18-LTS backport */ + {7, 0, 10}, /* 7.0 stable: fix lands at 7.0.10 */ }; static const struct kernel_range fragnesia_range = { .patched_from = fragnesia_patched_branches, @@ -930,6 +944,17 @@ static skeletonkey_result_t fg_detect(const struct skeletonkey_ctx *ctx) return SKELETONKEY_TEST_ERROR; } + /* Predates the bug: SKBFL_SHARED_FRAG marker only exists from 5.11 + * onwards; older kernels don't have the buggy skb_try_coalesce() + * code path. */ + if (!skeletonkey_host_kernel_at_least(ctx->host, 5, 11, 0)) { + if (!ctx->json) + fprintf(stderr, "[i] fragnesia: kernel %s predates the " + "SKBFL_SHARED_FRAG marker added in 5.11 — not " + "applicable\n", v->release); + return SKELETONKEY_OK; + } + if (!ctx->host->unprivileged_userns_allowed) { if (!ctx->json) fprintf(stderr, "[i] fragnesia: unprivileged user " diff --git a/skeletonkey.c b/skeletonkey.c index 126ea15..a46ccb0 100644 --- a/skeletonkey.c +++ b/skeletonkey.c @@ -35,7 +35,7 @@ #include #include -#define SKELETONKEY_VERSION "0.9.3" +#define SKELETONKEY_VERSION "0.9.4" static const char BANNER[] = "\n" diff --git a/tests/test_detect.c b/tests/test_detect.c index 3b0e32f..9007dd9 100644 --- a/tests/test_detect.c +++ b/tests/test_detect.c @@ -332,6 +332,12 @@ static void run_all(void) &dirtydecrypt_module, &h_ubuntu_24_userns_ok, SKELETONKEY_OK); + /* fragnesia: SKBFL_SHARED_FRAG marker added in 5.11; kernels before + * that predate the buggy skb_try_coalesce() code → OK */ + run_one("fragnesia: kernel 4.4 predates 5.11 SKBFL_SHARED_FRAG → OK", + &fragnesia_module, &h_kernel_4_4, + SKELETONKEY_OK); + /* fragnesia: userns disabled → XFRM gate closed → PRECOND_FAIL */ run_one("fragnesia: userns_allowed=false → PRECOND_FAIL", &fragnesia_module, &h_pre7_no_userns_no_dbus, diff --git a/tools/refresh-cve-metadata.py b/tools/refresh-cve-metadata.py index f5bec9b..bab3c14 100755 --- a/tools/refresh-cve-metadata.py +++ b/tools/refresh-cve-metadata.py @@ -118,18 +118,38 @@ def fetch_kev_catalog() -> dict[str, str]: def fetch_nvd_cwe(cve: str) -> tuple[str | None, str | None]: - """Return (cwe_id, description) from NVD. Returns (None, None) on miss.""" + """Return (cwe_id, description) from NVD. Returns (None, None) on miss. + + Same urlopen-hangs-silently pattern as the CISA fetch: NVD's HTTP/2 + endpoint sometimes leaves Python sockets in CLOSE_WAIT forever even + though the 30s timeout should have fired (observed on macOS 2026-05-24, + process hung 55+ minutes). We try urlopen first, then fall back to + curl --max-time which honors the wall clock reliably.""" url = NVD_URL.format(cve=cve) req = urllib.request.Request(url, headers={"User-Agent": "skeletonkey-cve-metadata/1"}) + blob = None try: with urllib.request.urlopen(req, timeout=30) as r: blob = json.loads(r.read().decode("utf-8")) except urllib.error.HTTPError as e: print(f"[!] NVD HTTP {e.code} for {cve}", file=sys.stderr) return None, None - except (urllib.error.URLError, json.JSONDecodeError) as e: - print(f"[!] NVD parse error for {cve}: {e}", file=sys.stderr) - return None, None + except (urllib.error.URLError, json.JSONDecodeError, TimeoutError) as e: + print(f"[!] NVD urlopen failed for {cve} ({e}); trying curl", file=sys.stderr) + if blob is None: + import subprocess + try: + raw = subprocess.check_output( + ["curl", "-fsSL", "--max-time", "20", + "-H", "User-Agent: skeletonkey-cve-metadata/1", + url], + stderr=subprocess.DEVNULL, + ) + blob = json.loads(raw.decode("utf-8")) + except (subprocess.CalledProcessError, FileNotFoundError, + json.JSONDecodeError) as e: + print(f"[!] NVD curl fallback failed for {cve}: {e}", file=sys.stderr) + return None, None vulns = blob.get("vulnerabilities") or [] if not vulns: return None, None diff --git a/tools/verify-vm/README.md b/tools/verify-vm/README.md index f39a8ec..73ce8e1 100644 --- a/tools/verify-vm/README.md +++ b/tools/verify-vm/README.md @@ -27,18 +27,28 @@ To skip boxes you don't need (save disk): ./tools/verify-vm/verify.sh nf_tables ``` -What that does: +What that does (two-phase model — install kernel, then verify): 1. Reads `tools/verify-vm/targets.yaml`: finds `nf_tables` → box - `generic/ubuntu2204` + kernel pin `linux-image-5.15.0-43-generic`. -2. `vagrant up skk-nf_tables` (provisions on first call, resumes on - subsequent). -3. Installs the pinned vulnerable kernel via `apt`, reboots. -4. Mounts the local repo at `/vagrant`, runs `make`, then runs - `skeletonkey --explain nf_tables --active`. -5. Parses the `VERDICT:` line, compares against `expect_detect` from - targets.yaml, emits a JSON verification record on stdout. -6. Suspends the VM (`vagrant suspend`) — instant resume next run. + `generic/ubuntu2204` + `mainline_version: 5.15.5`. +2. `vagrant up skk-nf_tables` if not already running (each module gets + its own machine for isolation). +3. **Prep phase** — runs every prep provisioner that applies: + - `pin-kernel-` if `kernel_pkg` is set (apt install + GRUB_DEFAULT pin) + - `pin-mainline-` if `mainline_version` is set (download from + kernel.ubuntu.com/mainline, dpkg -i, GRUB_DEFAULT pin) + - `module-provision-` if `provisioners/.sh` exists + (build vulnerable sudo from source, drop polkit allow rule, + install udisks2, etc.) +4. **Conditional reboot** — `vagrant reload` if `uname -r` doesn't + match the target kernel after the prep phase. Confirms post-reboot + kernel actually landed on the target; warns if it didn't. +5. **Verify phase** — `build-and-verify` provisioner: rsync the source, + `make`, run `skeletonkey --explain --active`. +6. Parses the `VERDICT:` line, compares against `expect_detect` from + targets.yaml, appends a JSON verification record to + `docs/VERIFICATIONS.jsonl`. +7. Suspends the VM (`vagrant suspend`) — instant resume next run. Lifecycle flags: @@ -54,73 +64,122 @@ Lifecycle flags: ``` Shows the (module, box, target kernel, expected verdict, notes) matrix -for all 26 modules. Three are flagged `manual: true` because no -public Vagrant box covers them: - -- `vmwgfx` — only reachable on VMware guests; needs a vSphere/Fusion VM - not Parallels. -- `dirtydecrypt`, `fragnesia` — only present in Linux 7.0+ which isn't - shipping as a distro kernel yet. - -For those, verification needs a hand-built or special-distro VM. +for all targets. Modules with `manual: true` are blocked by their +target environment — see the notes field for the reason (VMware-only +guest, EOL kernel needed, t64-transition libs missing, etc.). ## Verification records -`verify.sh` emits JSON on stdout after each run. Example: +`verify.sh` appends one JSON record per run to +`docs/VERIFICATIONS.jsonl`: ```json { "module": "nf_tables", - "verified_at": "2026-05-23T17:42:11Z", - "host_kernel": "5.15.0-43-generic", - "host_distro": "Ubuntu 22.04.5 LTS", + "verified_at": "2026-05-24T03:24:01Z", + "host_kernel": "5.15.5-051505-generic", + "host_distro": "Ubuntu 22.04.3 LTS", "vm_box": "generic/ubuntu2204", "expect_detect": "VULNERABLE", "actual_detect": "VULNERABLE", - "status": "match", - "log": "tools/verify-vm/logs/verify-nf_tables-20260523-174211.log" + "status": "match" } ``` `status: match` means detect() returned what we expected on a known- -vulnerable kernel. Anything else (`MISMATCH`, status code != 0) means +vulnerable kernel. Anything else (`MISMATCH`, exit code != 0) means either: -- The kernel pin didn't take (check `host_kernel` against - `kernel_version` in targets.yaml). +- The kernel pin didn't take — check `host_kernel` against + `kernel_version` in targets.yaml. The "post-reboot kernel" line in + the verify log will say if `vagrant reload` did or didn't land on + the target. - The exploit's preconditions aren't met in the default Vagrant image - (e.g. apparmor blocks unprivileged userns; need to adjust the - Vagrantfile provisioner). -- The detect() logic is wrong for this kernel/distro combo (a real bug - — fix it). + (e.g. apparmor blocks unprivileged userns; provisioner needed). +- The module's detect() logic is wrong for this kernel/distro combo + (a real module bug — fix it, as we did for `dirtydecrypt` after + cross-checking against NVD). -Records are intended to feed a per-module `verified_on[]` table (next -project step) so `--list` can show a `✓ verified ` column. +Run `tools/refresh-verifications.py` after new records land to +regenerate `core/verifications.c` so the binary's `--explain` and +`--list` reflect the latest evidence. ## How it routes module → box Mapping lives in `tools/verify-vm/targets.yaml`. Each entry has: -- `box` — which `boxes/` template (e.g. `ubuntu2204`) -- `kernel_pkg` — apt package name to install if the stock kernel - is patched (omit / empty if stock is already vulnerable) +- `box` — generic/ (e.g. `ubuntu2204`) +- `kernel_pkg` — apt package for a vulnerable stock-archive kernel, + if one still exists in the distro's repos +- `mainline_version` — alternative to `kernel_pkg`: pulls a vanilla + upstream kernel from `kernel.ubuntu.com/mainline/v/`. Use when + the apt-archive version has been garbage-collected (Ubuntu drops + old ABI versions) or when you need a specific point release that + the distro never packaged. - `kernel_version` — what `uname -r` should report after install - `expect_detect` — `VULNERABLE` | `OK` | `PRECOND_FAIL` -- `notes` — short rationale; comments in the file have the full context +- `manual: true` — skip auto verification; explain why in `notes` +- `notes` — full context for why this target was picked -Adding a new module is one block in targets.yaml. The verifier picks -it up automatically. +Adding a new module is one block in targets.yaml. If the module needs +per-target setup beyond installing a kernel — for example building +sudo from source, adding a sudoers grant, or dropping a polkit allow +rule — write a shell script at `tools/verify-vm/provisioners/.sh` +and the Vagrantfile will pick it up automatically. + +## Module-specific provisioners (`provisioners/.sh`) + +When the kernel pin alone doesn't make a host vulnerable — e.g. +the bug is sudo-version-gated, or a polkit "active session" check +blocks the SSH path — drop a shell script at +`tools/verify-vm/provisioners/.sh`. The Vagrantfile +runs it as root in the prep phase, before the `vagrant reload` +check. Scripts should be idempotent (apt is no-op if installed, +file overwrites are safe) since they re-run on every verify. + +Existing examples: + +- `sudo_chwoot.sh` — builds sudo 1.9.16p1 from upstream into + `/usr/local/bin` so the vulnerable `--chroot` code path is reachable + on Ubuntu 22.04 (which ships pre-feature 1.9.9). +- `udisks_libblockdev.sh` — installs `udisks2` + drops a polkit rule + allowing the vagrant user to invoke `loop-setup` / `filesystem-mount` + (without this, the SSH session is not "active" per polkit and the + D-Bus call short-circuits). +- `sudo_runas_neg1.sh` — adds `vagrant ALL=(ALL,!root) NOPASSWD: /bin/vi` + to `/etc/sudoers.d/` so `find_runas_blacklist_grant()` has a grant + to abuse. + +## Pinning kernels: apt vs mainline + +`pin-kernel-` runs `apt-get install -y `. Best when the +target version still lives in the distro's archive (rare for old +point releases — Ubuntu eventually GCs them). Also pins `GRUB_DEFAULT` +to the just-installed kernel so the reboot lands on it instead of +the higher-version stock kernel. + +`pin-mainline-` downloads vanilla mainline debs from +`kernel.ubuntu.com/mainline/v/`. Tries `/amd64/` first, falls +back to bare `/v/` for old kernels (≤ ~4.15) where amd64 wasn't +a separate subdir. Accepts both `linux-image-` (older naming) and +`linux-image-unsigned-` (current). Pins `GRUB_DEFAULT` to the +mainline kernel so grub doesn't keep booting the higher-versioned +stock kernel. ## Files ``` tools/verify-vm/ -├── README.md this file -├── setup.sh one-time bootstrap (Vagrant, plugin, box cache) -├── verify.sh per-module verifier -├── Vagrantfile parameterized VM config (driven by SKK_VM_* env vars) -├── targets.yaml module → box mapping with rationale -└── logs/ per-verification stdout/stderr capture +├── README.md this file +├── setup.sh one-time bootstrap (Vagrant, plugin, box cache) +├── verify.sh per-module verifier +├── Vagrantfile parameterized VM config (driven by SKK_VM_* env vars) +├── targets.yaml module → box mapping with rationale +├── provisioners/ optional per-module shell hooks +│ ├── sudo_chwoot.sh +│ ├── sudo_runas_neg1.sh +│ └── udisks_libblockdev.sh +└── logs/ per-verification stdout/stderr capture ``` ## Why Vagrant + Parallels