0sec-cli 0.15.0 → 0.17.0

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 (209) hide show
  1. package/0sec.js +322 -0
  2. package/README.md +63 -52
  3. package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
  4. package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
  5. package/attacks/jailbreak/dan-roleplay.yaml +27 -0
  6. package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
  7. package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
  8. package/attacks/output-manipulation/harmful-content.yaml +25 -0
  9. package/attacks/prompt-injection/context-manipulation.yaml +32 -0
  10. package/attacks/prompt-injection/direct-injection.yaml +28 -0
  11. package/attacks/prompt-injection/indirect-injection.yaml +33 -0
  12. package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
  13. package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
  14. package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
  15. package/chunks/adapt-loop-ECQG7454.js +18 -0
  16. package/chunks/adgraph-JLGA6RYI.js +54 -0
  17. package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
  18. package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
  19. package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
  20. package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
  21. package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
  22. package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
  23. package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
  24. package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
  25. package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
  26. package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
  27. package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
  28. package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
  29. package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
  30. package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
  31. package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
  32. package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
  33. package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
  34. package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
  35. package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
  36. package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
  37. package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
  38. package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
  39. package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
  40. package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
  41. package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
  42. package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
  43. package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
  44. package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
  45. package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
  46. package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
  47. package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
  48. package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
  49. package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
  50. package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
  51. package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
  52. package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
  53. package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
  54. package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
  55. package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
  56. package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
  57. package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
  58. package/chunks/artifact-scraper-GJEZMOHF.js +34 -0
  59. package/chunks/assumption-mining-BVMMUZHB.js +79 -0
  60. package/chunks/chunk-2CJ776PV.js +2430 -0
  61. package/chunks/chunk-2RMLOJVB.js +47 -0
  62. package/chunks/chunk-2SANI5RH.js +4728 -0
  63. package/chunks/chunk-3MOLBTLS.js +953 -0
  64. package/chunks/chunk-3QFDYBZQ.js +5108 -0
  65. package/chunks/chunk-47TQJWDH.js +3729 -0
  66. package/chunks/chunk-4Y4KQJXE.js +600 -0
  67. package/chunks/chunk-53G27VPS.js +136 -0
  68. package/chunks/chunk-57ZENEX2.js +169 -0
  69. package/chunks/chunk-5G3ZXFBW.js +182 -0
  70. package/chunks/chunk-6LKRLK2R.js +542 -0
  71. package/chunks/chunk-7DQEV5QI.js +287 -0
  72. package/chunks/chunk-A6CLR72I.js +235 -0
  73. package/chunks/chunk-BKZFDZ23.js +361 -0
  74. package/chunks/chunk-CLHCDHP4.js +471 -0
  75. package/chunks/chunk-D6S3IIPG.js +518 -0
  76. package/chunks/chunk-DN25OQQA.js +1070 -0
  77. package/chunks/chunk-DW5UWPFY.js +275 -0
  78. package/chunks/chunk-F3WBKITT.js +687 -0
  79. package/chunks/chunk-H2FFLZNK.js +3 -0
  80. package/chunks/chunk-H44E2CRN.js +193 -0
  81. package/chunks/chunk-HDWV7PGI.js +596 -0
  82. package/chunks/chunk-HE7LCA7G.js +381 -0
  83. package/chunks/chunk-HT6P7RY3.js +2597 -0
  84. package/chunks/chunk-IOL7D5YV.js +515 -0
  85. package/chunks/chunk-IORC6MQX.js +72828 -0
  86. package/chunks/chunk-IR537GON.js +49 -0
  87. package/chunks/chunk-K26SZ37E.js +5735 -0
  88. package/chunks/chunk-KKGE5RQE.js +1094 -0
  89. package/chunks/chunk-KLTNTE2Z.js +121 -0
  90. package/chunks/chunk-KTQDLNSR.js +1732 -0
  91. package/chunks/chunk-LOHTE223.js +2572 -0
  92. package/chunks/chunk-LP3HHYQU.js +245 -0
  93. package/chunks/chunk-MMLDQR4H.js +852 -0
  94. package/chunks/chunk-MYVT64FN.js +16 -0
  95. package/chunks/chunk-O462Y7P2.js +33019 -0
  96. package/chunks/chunk-OEFNRYI2.js +150 -0
  97. package/chunks/chunk-P6WKNFWX.js +1968 -0
  98. package/chunks/chunk-QI233I24.js +333 -0
  99. package/chunks/chunk-QJQKHO7G.js +917 -0
  100. package/chunks/chunk-QKICO43A.js +808 -0
  101. package/chunks/chunk-QOKTUOU2.js +102 -0
  102. package/chunks/chunk-RDZYQQLW.js +596 -0
  103. package/chunks/chunk-RJWOYEOG.js +271 -0
  104. package/chunks/chunk-RRMJC3ZE.js +105 -0
  105. package/chunks/chunk-RWONANDA.js +973 -0
  106. package/chunks/chunk-SAFFWQW4.js +5921 -0
  107. package/chunks/chunk-SF4KZ4O3.js +502 -0
  108. package/chunks/chunk-SNKTC4BP.js +1422 -0
  109. package/chunks/chunk-SOG2U7B3.js +662 -0
  110. package/chunks/chunk-SZJCPG2I.js +110 -0
  111. package/chunks/chunk-UJ4IK5PO.js +2010 -0
  112. package/chunks/chunk-UM3ZNQIM.js +9329 -0
  113. package/chunks/chunk-VGRDNSHA.js +3249 -0
  114. package/chunks/chunk-VQG4FT5D.js +685 -0
  115. package/chunks/chunk-WKNNZVJS.js +2798 -0
  116. package/chunks/chunk-WU6AFRAZ.js +1167 -0
  117. package/chunks/chunk-WVTBZEQO.js +1531 -0
  118. package/chunks/chunk-YLMN3N25.js +4054 -0
  119. package/chunks/commands-U3AMV5ZU.js +16795 -0
  120. package/chunks/corpus-v1.json +403 -0
  121. package/chunks/cost-ledger-ZCKMBJEN.js +13 -0
  122. package/chunks/data/appsec-archetypes.json +102 -0
  123. package/chunks/data/chromium-archetypes.json +204 -0
  124. package/chunks/data/freebsd-archetypes.json +171 -0
  125. package/chunks/data/kernel-archetypes.json +611 -0
  126. package/chunks/db-26NGQFKO.js +16 -0
  127. package/chunks/disclose-A6AEIEDA.js +132 -0
  128. package/chunks/dist-FDKALV4R.js +2772 -0
  129. package/chunks/dist-GL66KMUX.js +74 -0
  130. package/chunks/dist-JR67XKYO.js +159 -0
  131. package/chunks/eval-runner-DDLE5RQ3.js +27 -0
  132. package/chunks/example-manifest.json +95 -0
  133. package/chunks/exploit-agent-XP23ISN7.js +98 -0
  134. package/chunks/exploit-autoclimb-E6T7RRSC.js +81 -0
  135. package/chunks/exploit-climb-2FAUP6XU.js +358 -0
  136. package/chunks/fix-IG7LYXH3.js +12 -0
  137. package/chunks/github-issues-MJ6OYOOU.js +157 -0
  138. package/chunks/harness-HWOYFKFY.js +22 -0
  139. package/chunks/http-conformance-DU66MZIU.js +11 -0
  140. package/chunks/http-sender-GWH2IYEA.js +10 -0
  141. package/chunks/hunt-scan-XPELSPQQ.js +38 -0
  142. package/chunks/identity-6ZAIIWOR.js +191 -0
  143. package/chunks/kernel-primitive-TENE3R7T.js +44 -0
  144. package/chunks/kernel-vm-runner-4F6QSNFY.js +62 -0
  145. package/chunks/memsafety-scan-J7SECV5W.js +16 -0
  146. package/chunks/native-loop-NTONUDP7.js +54 -0
  147. package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
  148. package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
  149. package/chunks/orchestrate-ZUWAUWBC.js +62 -0
  150. package/chunks/pipeline-FCARFZU3.js +14 -0
  151. package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
  152. package/chunks/prepare-MYY743TK.js +13 -0
  153. package/chunks/process-3Q7QJIOZ.js +13 -0
  154. package/chunks/replay-runner-RRR4E2A7.js +39 -0
  155. package/chunks/run-ESNN4V5W.js +32632 -0
  156. package/chunks/runtime-62ZQAH7H.js +43 -0
  157. package/chunks/runtime-J7PLXZNM.js +12 -0
  158. package/chunks/scan-stream-FHI2FYZE.js +96 -0
  159. package/chunks/scope-BI7BF4ZY.js +18 -0
  160. package/chunks/session-store-BCFQYDCE.js +28 -0
  161. package/chunks/source-files-PWRQR6LY.js +12 -0
  162. package/chunks/specdrift-WCLH6TTQ.js +19 -0
  163. package/chunks/variant-candidates-6KM6F6MD.js +13 -0
  164. package/chunks/web-recon-prepass-DPVCBRZA.js +1148 -0
  165. package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
  166. package/dashboard/assets/bot-yILPnRgD.js +1 -0
  167. package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
  168. package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
  169. package/dashboard/assets/client-DRYsuOGl.js +9 -0
  170. package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
  171. package/dashboard/assets/desktop-CXTQonNm.js +32 -0
  172. package/dashboard/assets/desktop-NOekk4QS.css +1 -0
  173. package/dashboard/assets/dist-CmX-h7a4.js +1 -0
  174. package/dashboard/assets/dist-DAwYQmf_.js +45 -0
  175. package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
  176. package/dashboard/assets/format-PoISqsES.js +1 -0
  177. package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  178. package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  179. package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  180. package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  181. package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  182. package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  183. package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  184. package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  185. package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  186. package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  187. package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  188. package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  189. package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  190. package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  191. package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  192. package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
  193. package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
  194. package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
  195. package/dashboard/assets/operations-BKE8T-vY.css +2 -0
  196. package/dashboard/assets/operations-D8nUP5_m.js +4 -0
  197. package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
  198. package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
  199. package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
  200. package/dashboard/assets/play-Dwo-9xt6.js +1 -0
  201. package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
  202. package/dashboard/assets/search-CiooTLck.js +1 -0
  203. package/dashboard/assets/siren-CCBwTSuk.js +1 -0
  204. package/dashboard/assets/table-BYS-nIVA.js +1 -0
  205. package/dashboard/assets/tabs-DivxbhQy.js +1 -0
  206. package/dashboard/desktop.html +21 -0
  207. package/dashboard/index.html +15 -0
  208. package/package.json +30 -18
  209. package/bin/0sec.cjs +0 -361
@@ -0,0 +1,3729 @@
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";
7
+ import {
8
+ makeSkepticVerifier,
9
+ runHuntScan,
10
+ scoreGeometry
11
+ } from "./chunk-K26SZ37E.js";
12
+ import {
13
+ LlmApiRuntime
14
+ } from "./chunk-2SANI5RH.js";
15
+ import {
16
+ __commonJS,
17
+ __require,
18
+ __toESM
19
+ } from "./chunk-IR537GON.js";
20
+
21
+ // packages/core/dist/stages/tree-sitter-runtime.cjs
22
+ var require_tree_sitter_runtime = __commonJS({
23
+ "packages/core/dist/stages/tree-sitter-runtime.cjs"(exports, module) {
24
+ "use strict";
25
+ var compiled = false;
26
+ if (!compiled) {
27
+ module.exports = {
28
+ Parser: __require("tree-sitter"),
29
+ language: __require("tree-sitter-c")
30
+ };
31
+ } else {
32
+ module.exports = {
33
+ Parser: __require("./tree-sitter-compiled/tree-sitter-runtime.cjs"),
34
+ language: __require("./tree-sitter-compiled/tree-sitter-c.node")
35
+ };
36
+ }
37
+ }
38
+ });
39
+
40
+ // packages/core/dist/stages/assumption-mining.js
41
+ import { readFileSync as readFileSync3 } from "node:fs";
42
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
43
+ import { dirname as dirname3, join } from "node:path";
44
+ import { tmpdir } from "node:os";
45
+
46
+ // packages/core/dist/stages/c-dataflow.js
47
+ var import_tree_sitter_runtime = __toESM(require_tree_sitter_runtime(), 1);
48
+ var { Parser: TreeSitterParser, language: C } = import_tree_sitter_runtime.default;
49
+ var PARSER = null;
50
+ function parser() {
51
+ if (!PARSER) {
52
+ PARSER = new TreeSitterParser();
53
+ PARSER.setLanguage(C);
54
+ }
55
+ return PARSER;
56
+ }
57
+ function parseC(src) {
58
+ try {
59
+ return parser().parse(src, void 0, {
60
+ bufferSize: Buffer.byteLength(src, "utf8") + 8192
61
+ }).rootNode;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ var DEFAULT_ACQUIRE_FNS = [
67
+ "spin_lock",
68
+ "spin_lock_bh",
69
+ "spin_lock_irq",
70
+ "spin_lock_irqsave",
71
+ "spin_trylock",
72
+ "raw_spin_lock",
73
+ "raw_spin_lock_bh",
74
+ "raw_spin_lock_irqsave",
75
+ "mutex_lock",
76
+ "mutex_lock_interruptible",
77
+ "mutex_trylock",
78
+ "read_lock",
79
+ "read_lock_bh",
80
+ "write_lock",
81
+ "write_lock_bh",
82
+ "down",
83
+ "down_read",
84
+ "down_write",
85
+ "down_interruptible",
86
+ "down_trylock"
87
+ ];
88
+ var DEFAULT_RELEASE_FNS = [
89
+ "spin_unlock",
90
+ "spin_unlock_bh",
91
+ "spin_unlock_irq",
92
+ "spin_unlock_irqrestore",
93
+ "raw_spin_unlock",
94
+ "raw_spin_unlock_bh",
95
+ "raw_spin_unlock_irqrestore",
96
+ "mutex_unlock",
97
+ "read_unlock",
98
+ "read_unlock_bh",
99
+ "write_unlock",
100
+ "write_unlock_bh",
101
+ "up",
102
+ "up_read",
103
+ "up_write"
104
+ ];
105
+ function releaseFnsFor(acquireFns) {
106
+ const rel = new Set(DEFAULT_RELEASE_FNS);
107
+ for (const a of acquireFns) {
108
+ if (a === "down" || a === "down_interruptible" || a === "down_trylock")
109
+ rel.add("up");
110
+ else if (a === "down_read")
111
+ rel.add("up_read");
112
+ else if (a === "down_write")
113
+ rel.add("up_write");
114
+ else if (a.includes("irqsave"))
115
+ rel.add(a.replace("irqsave", "irqrestore").replace("_lock", "_unlock"));
116
+ else if (a.includes("lock"))
117
+ rel.add(a.replace("trylock", "unlock").replace("_lock", "_unlock"));
118
+ }
119
+ return [...rel];
120
+ }
121
+ var collapseWs = (s) => s.replace(/\s+/g, "");
122
+ function canon(node, src) {
123
+ switch (node.type) {
124
+ case "identifier":
125
+ case "field_identifier":
126
+ case "type_identifier":
127
+ return src.slice(node.startIndex, node.endIndex);
128
+ case "parenthesized_expression": {
129
+ const inner = node.namedChildren[0];
130
+ return inner ? canon(inner, src) : collapseWs(src.slice(node.startIndex, node.endIndex));
131
+ }
132
+ case "pointer_expression": {
133
+ const op = node.child(0);
134
+ const arg = node.childForFieldName("argument") ?? node.namedChildren[0];
135
+ if (!arg)
136
+ return collapseWs(src.slice(node.startIndex, node.endIndex));
137
+ return op && src.slice(op.startIndex, op.endIndex) === "&" ? canon(arg, src) : "*" + canon(arg, src);
138
+ }
139
+ case "field_expression": {
140
+ const arg = node.childForFieldName("argument");
141
+ const field = node.childForFieldName("field");
142
+ const base = arg ? canon(arg, src) : "";
143
+ const f = field ? src.slice(field.startIndex, field.endIndex) : "";
144
+ return `${base}->${f}`;
145
+ }
146
+ case "subscript_expression": {
147
+ const arg = node.childForFieldName("argument") ?? node.namedChildren[0];
148
+ return (arg ? canon(arg, src) : "") + "[]";
149
+ }
150
+ default:
151
+ return collapseWs(src.slice(node.startIndex, node.endIndex));
152
+ }
153
+ }
154
+ var firstIdent = (s) => /^[A-Za-z_][A-Za-z0-9_]*/.exec(s)?.[0] ?? s;
155
+ function normalizeLockToken(token) {
156
+ const key = collapseWs(token.replace(/^&/, "").replace(/\./g, "->"));
157
+ const idx = key.lastIndexOf("->");
158
+ if (idx === -1)
159
+ return { key, lockField: key, receiver: "", global: true };
160
+ return { key, lockField: key.slice(idx + 2), receiver: key.slice(0, idx), global: false };
161
+ }
162
+ var RECOGNIZED_CALL_SHAPE = /* @__PURE__ */ new Set(["identifier"]);
163
+ function collectEvents(node, src, sink) {
164
+ const line = (n) => n.startPosition.row + 1;
165
+ const visit = (n, assignTarget) => {
166
+ switch (n.type) {
167
+ case "call_expression": {
168
+ const callee = n.childForFieldName("function");
169
+ const args = n.childForFieldName("arguments");
170
+ const calleeName2 = callee && RECOGNIZED_CALL_SHAPE.has(callee.type) ? src.slice(callee.startIndex, callee.endIndex) : "";
171
+ const arg0Node = args?.namedChildren[0];
172
+ const arg0 = arg0Node ? canon(arg0Node, src) : "";
173
+ sink.push({ kind: "call", index: n.endIndex, line: line(n), callee: calleeName2, arg0 });
174
+ if (args) {
175
+ const kids = args.namedChildren;
176
+ for (let i = 1; i < kids.length; i++)
177
+ visit(kids[i], false);
178
+ }
179
+ return;
180
+ }
181
+ case "assignment_expression": {
182
+ const left = n.childForFieldName("left");
183
+ const right = n.childForFieldName("right");
184
+ if (left) {
185
+ sink.push({ kind: "def", index: left.startIndex, line: line(left), canonStr: canon(left, src), base: firstIdent(canon(left, src)) });
186
+ visit(left, true);
187
+ }
188
+ if (right)
189
+ visit(right, false);
190
+ return;
191
+ }
192
+ case "init_declarator": {
193
+ const decl = n.childForFieldName("declarator");
194
+ const value = n.childForFieldName("value");
195
+ if (decl && decl.type === "identifier") {
196
+ const c = canon(decl, src);
197
+ sink.push({ kind: "def", index: decl.startIndex, line: line(decl), canonStr: c, base: firstIdent(c) });
198
+ }
199
+ if (value)
200
+ visit(value, false);
201
+ return;
202
+ }
203
+ case "field_expression": {
204
+ const arg = n.childForFieldName("argument");
205
+ const field = n.childForFieldName("field");
206
+ const receiver = arg ? canon(arg, src) : "";
207
+ const fname = field ? src.slice(field.startIndex, field.endIndex) : "";
208
+ const c = canon(n, src);
209
+ sink.push({
210
+ kind: "field",
211
+ index: n.startIndex,
212
+ line: line(n),
213
+ receiver,
214
+ field: fname,
215
+ canonStr: c,
216
+ isAssignTarget: assignTarget,
217
+ base: firstIdent(c),
218
+ isDeref: true
219
+ });
220
+ if (arg)
221
+ visit(arg, false);
222
+ return;
223
+ }
224
+ case "subscript_expression": {
225
+ const arg = n.childForFieldName("argument") ?? n.namedChildren[0];
226
+ const c = canon(n, src);
227
+ sink.push({ kind: "ref", index: n.startIndex, line: line(n), canonStr: c, base: firstIdent(c), isDeref: true });
228
+ if (arg)
229
+ visit(arg, false);
230
+ const idx = n.childForFieldName("index");
231
+ if (idx)
232
+ visit(idx, false);
233
+ return;
234
+ }
235
+ case "pointer_expression": {
236
+ const op = n.child(0);
237
+ const arg = n.childForFieldName("argument") ?? n.namedChildren[0];
238
+ if (op && src.slice(op.startIndex, op.endIndex) === "*" && arg) {
239
+ const c = canon(arg, src);
240
+ sink.push({ kind: "ref", index: n.startIndex, line: line(n), canonStr: c, base: firstIdent(c), isDeref: true });
241
+ }
242
+ if (arg)
243
+ visit(arg, false);
244
+ return;
245
+ }
246
+ case "identifier": {
247
+ const c = src.slice(n.startIndex, n.endIndex);
248
+ sink.push({ kind: "ref", index: n.startIndex, line: line(n), canonStr: c, base: c, isDeref: false });
249
+ return;
250
+ }
251
+ default: {
252
+ for (const k of n.namedChildren)
253
+ visit(k, false);
254
+ return;
255
+ }
256
+ }
257
+ };
258
+ visit(node, false);
259
+ }
260
+ function buildCfg(body, src) {
261
+ const blocks = [];
262
+ const mk = () => {
263
+ const id = blocks.length;
264
+ blocks.push({ id, events: [], succs: /* @__PURE__ */ new Set() });
265
+ return id;
266
+ };
267
+ const edge = (a, b) => blocks[a].succs.add(b);
268
+ const entry = mk();
269
+ const exit = mk();
270
+ let cur = entry;
271
+ const breakStack = [];
272
+ const continueStack = [];
273
+ const labels = /* @__PURE__ */ new Map();
274
+ const pendingGotos = [];
275
+ const emitInto = (block, node) => collectEvents(node, src, blocks[block].events);
276
+ const emit = (node) => {
277
+ switch (node.type) {
278
+ case "compound_statement": {
279
+ for (const child of node.namedChildren)
280
+ emit(child);
281
+ return;
282
+ }
283
+ case "expression_statement":
284
+ case "declaration": {
285
+ emitInto(cur, node);
286
+ return;
287
+ }
288
+ case "return_statement": {
289
+ emitInto(cur, node);
290
+ edge(cur, exit);
291
+ cur = mk();
292
+ return;
293
+ }
294
+ case "if_statement": {
295
+ const cond = node.childForFieldName("condition");
296
+ if (cond)
297
+ emitInto(cur, cond);
298
+ const condBlock = cur;
299
+ const consequence = node.childForFieldName("consequence");
300
+ const alternative = node.childForFieldName("alternative");
301
+ const thenEntry = mk();
302
+ edge(condBlock, thenEntry);
303
+ cur = thenEntry;
304
+ if (consequence)
305
+ emit(consequence);
306
+ const thenExit = cur;
307
+ const merge = mk();
308
+ edge(thenExit, merge);
309
+ if (alternative) {
310
+ const elseEntry = mk();
311
+ edge(condBlock, elseEntry);
312
+ cur = elseEntry;
313
+ const altStmt = alternative.type === "else_clause" ? alternative.namedChildren[0] ?? alternative : alternative;
314
+ emit(altStmt);
315
+ edge(cur, merge);
316
+ } else {
317
+ edge(condBlock, merge);
318
+ }
319
+ cur = merge;
320
+ return;
321
+ }
322
+ case "while_statement":
323
+ case "for_statement": {
324
+ const cond = node.childForFieldName("condition");
325
+ const init = node.childForFieldName("initializer");
326
+ const update = node.childForFieldName("update");
327
+ if (init)
328
+ emitInto(cur, init);
329
+ const header = mk();
330
+ edge(cur, header);
331
+ if (cond)
332
+ emitInto(header, cond);
333
+ const bodyEntry = mk();
334
+ const exitLoop = mk();
335
+ edge(header, bodyEntry);
336
+ edge(header, exitLoop);
337
+ breakStack.push(exitLoop);
338
+ continueStack.push(header);
339
+ cur = bodyEntry;
340
+ const bodyStmt = node.childForFieldName("body");
341
+ if (bodyStmt)
342
+ emit(bodyStmt);
343
+ if (update)
344
+ emitInto(cur, update);
345
+ edge(cur, header);
346
+ breakStack.pop();
347
+ continueStack.pop();
348
+ cur = exitLoop;
349
+ return;
350
+ }
351
+ case "do_statement": {
352
+ const bodyEntry = mk();
353
+ edge(cur, bodyEntry);
354
+ const exitLoop = mk();
355
+ breakStack.push(exitLoop);
356
+ continueStack.push(bodyEntry);
357
+ cur = bodyEntry;
358
+ const bodyStmt = node.childForFieldName("body");
359
+ if (bodyStmt)
360
+ emit(bodyStmt);
361
+ const cond = node.childForFieldName("condition");
362
+ if (cond)
363
+ emitInto(cur, cond);
364
+ edge(cur, bodyEntry);
365
+ edge(cur, exitLoop);
366
+ breakStack.pop();
367
+ continueStack.pop();
368
+ cur = exitLoop;
369
+ return;
370
+ }
371
+ case "switch_statement": {
372
+ const cond = node.childForFieldName("condition");
373
+ if (cond)
374
+ emitInto(cur, cond);
375
+ const switchBlock = cur;
376
+ const exitSwitch = mk();
377
+ breakStack.push(exitSwitch);
378
+ const bodyStmt = node.childForFieldName("body");
379
+ if (bodyStmt) {
380
+ let prev = null;
381
+ for (const caseNode of bodyStmt.namedChildren) {
382
+ const caseEntry = mk();
383
+ edge(switchBlock, caseEntry);
384
+ if (prev !== null)
385
+ edge(prev, caseEntry);
386
+ cur = caseEntry;
387
+ for (const s of caseNode.namedChildren) {
388
+ if (s.type === "case_statement" || s.type === "default_statement")
389
+ emit(s);
390
+ else
391
+ emit(s);
392
+ }
393
+ prev = cur;
394
+ }
395
+ if (prev !== null)
396
+ edge(prev, exitSwitch);
397
+ }
398
+ edge(switchBlock, exitSwitch);
399
+ breakStack.pop();
400
+ cur = exitSwitch;
401
+ return;
402
+ }
403
+ case "case_statement":
404
+ case "default_statement": {
405
+ for (const child of node.namedChildren) {
406
+ if (child.type === "number_literal" || child.type === "identifier" || child.type === "char_literal")
407
+ continue;
408
+ emit(child);
409
+ }
410
+ return;
411
+ }
412
+ case "goto_statement": {
413
+ const lbl = node.namedChildren.find((c) => c.type === "statement_identifier");
414
+ if (lbl)
415
+ pendingGotos.push({ from: cur, label: src.slice(lbl.startIndex, lbl.endIndex) });
416
+ cur = mk();
417
+ return;
418
+ }
419
+ case "labeled_statement": {
420
+ const lbl = node.namedChildren.find((c) => c.type === "statement_identifier");
421
+ const labelBlock = mk();
422
+ edge(cur, labelBlock);
423
+ if (lbl)
424
+ labels.set(src.slice(lbl.startIndex, lbl.endIndex), labelBlock);
425
+ cur = labelBlock;
426
+ const inner = node.namedChildren.find((c) => c.type !== "statement_identifier");
427
+ if (inner)
428
+ emit(inner);
429
+ return;
430
+ }
431
+ case "break_statement": {
432
+ if (breakStack.length)
433
+ edge(cur, breakStack[breakStack.length - 1]);
434
+ cur = mk();
435
+ return;
436
+ }
437
+ case "continue_statement": {
438
+ if (continueStack.length)
439
+ edge(cur, continueStack[continueStack.length - 1]);
440
+ cur = mk();
441
+ return;
442
+ }
443
+ default: {
444
+ emitInto(cur, node);
445
+ return;
446
+ }
447
+ }
448
+ };
449
+ emit(body);
450
+ edge(cur, exit);
451
+ for (const g of pendingGotos) {
452
+ const target = labels.get(g.label);
453
+ if (target !== void 0)
454
+ edge(g.from, target);
455
+ }
456
+ for (const b of blocks)
457
+ b.events.sort((a, c) => a.index - c.index);
458
+ return { blocks, entry, exit };
459
+ }
460
+ function preds(cfg) {
461
+ const p = cfg.blocks.map(() => []);
462
+ for (const b of cfg.blocks)
463
+ for (const s of b.succs)
464
+ p[s].push(b.id);
465
+ return p;
466
+ }
467
+ function lockGenKill(block, ctx) {
468
+ const held = /* @__PURE__ */ new Set();
469
+ const killed = /* @__PURE__ */ new Set();
470
+ for (const ev of block.events) {
471
+ if (ev.kind !== "call" || !ev.callee || !ev.arg0)
472
+ continue;
473
+ if (ctx.acquireFns.has(ev.callee)) {
474
+ held.add(ev.arg0);
475
+ killed.delete(ev.arg0);
476
+ } else if (ctx.releaseFns.has(ev.callee)) {
477
+ held.delete(ev.arg0);
478
+ killed.add(ev.arg0);
479
+ }
480
+ }
481
+ return { gen: held, kill: killed };
482
+ }
483
+ function lockUniverse(cfg, ctx) {
484
+ const u = /* @__PURE__ */ new Set();
485
+ for (const b of cfg.blocks)
486
+ for (const ev of b.events)
487
+ if (ev.kind === "call" && ev.callee && ctx.acquireFns.has(ev.callee) && ev.arg0)
488
+ u.add(ev.arg0);
489
+ return u;
490
+ }
491
+ function lockSetIn(cfg, ctx) {
492
+ const P = preds(cfg);
493
+ const universe = lockUniverse(cfg, ctx);
494
+ const genKill = cfg.blocks.map((b) => lockGenKill(b, ctx));
495
+ const IN = /* @__PURE__ */ new Map();
496
+ const OUT = /* @__PURE__ */ new Map();
497
+ for (const b of cfg.blocks) {
498
+ IN.set(b.id, new Set(universe));
499
+ OUT.set(b.id, new Set(universe));
500
+ }
501
+ IN.set(cfg.entry, /* @__PURE__ */ new Set());
502
+ const maxIter = cfg.blocks.length * 4 + 10;
503
+ for (let iter = 0; iter < maxIter; iter++) {
504
+ let changed = false;
505
+ for (const b of cfg.blocks) {
506
+ let inSet;
507
+ if (b.id === cfg.entry)
508
+ inSet = /* @__PURE__ */ new Set();
509
+ else if (P[b.id].length === 0)
510
+ inSet = /* @__PURE__ */ new Set();
511
+ else {
512
+ inSet = new Set(OUT.get(P[b.id][0]));
513
+ for (let i = 1; i < P[b.id].length; i++) {
514
+ const po = OUT.get(P[b.id][i]);
515
+ for (const k of [...inSet])
516
+ if (!po.has(k))
517
+ inSet.delete(k);
518
+ }
519
+ }
520
+ const { gen, kill } = genKill[b.id];
521
+ const outSet = /* @__PURE__ */ new Set();
522
+ for (const k of inSet)
523
+ if (!kill.has(k))
524
+ outSet.add(k);
525
+ for (const k of gen)
526
+ outSet.add(k);
527
+ if (!setEq(inSet, IN.get(b.id)) || !setEq(outSet, OUT.get(b.id))) {
528
+ changed = true;
529
+ IN.set(b.id, inSet);
530
+ OUT.set(b.id, outSet);
531
+ }
532
+ }
533
+ if (!changed)
534
+ break;
535
+ }
536
+ return IN;
537
+ }
538
+ function setEq(a, b) {
539
+ if (a.size !== b.size)
540
+ return false;
541
+ for (const k of a)
542
+ if (!b.has(k))
543
+ return false;
544
+ return true;
545
+ }
546
+ function freeGenKill(block, freeFns) {
547
+ const gen = /* @__PURE__ */ new Set();
548
+ const kill = /* @__PURE__ */ new Set();
549
+ for (const ev of block.events) {
550
+ if (ev.kind === "call" && ev.callee && freeFns.has(ev.callee) && ev.arg0) {
551
+ gen.add(ev.arg0);
552
+ kill.delete(ev.arg0);
553
+ } else if (ev.kind === "def" && ev.canonStr) {
554
+ kill.add(ev.canonStr);
555
+ gen.delete(ev.canonStr);
556
+ }
557
+ }
558
+ return { gen, kill };
559
+ }
560
+ function freeSetIn(cfg, freeFns) {
561
+ const P = preds(cfg);
562
+ const genKill = cfg.blocks.map((b) => freeGenKill(b, freeFns));
563
+ const IN = /* @__PURE__ */ new Map();
564
+ const OUT = /* @__PURE__ */ new Map();
565
+ for (const b of cfg.blocks) {
566
+ IN.set(b.id, /* @__PURE__ */ new Set());
567
+ OUT.set(b.id, /* @__PURE__ */ new Set());
568
+ }
569
+ const maxIter = cfg.blocks.length * 4 + 10;
570
+ for (let iter = 0; iter < maxIter; iter++) {
571
+ let changed = false;
572
+ for (const b of cfg.blocks) {
573
+ const inSet = /* @__PURE__ */ new Set();
574
+ for (const p of P[b.id])
575
+ for (const k of OUT.get(p))
576
+ inSet.add(k);
577
+ const { gen, kill } = genKill[b.id];
578
+ const outSet = /* @__PURE__ */ new Set();
579
+ for (const k of inSet)
580
+ if (!kill.has(k))
581
+ outSet.add(k);
582
+ for (const k of gen)
583
+ outSet.add(k);
584
+ if (!setEq(inSet, IN.get(b.id)) || !setEq(outSet, OUT.get(b.id))) {
585
+ changed = true;
586
+ IN.set(b.id, inSet);
587
+ OUT.set(b.id, outSet);
588
+ }
589
+ }
590
+ if (!changed)
591
+ break;
592
+ }
593
+ return IN;
594
+ }
595
+ function collectFunctions(root, src) {
596
+ const out = [];
597
+ const walk = (n) => {
598
+ if (n.type === "function_definition") {
599
+ const body = n.childForFieldName("body");
600
+ if (body && body.type === "compound_statement") {
601
+ out.push({ name: functionName(n, src), body, startLine: n.startPosition.row + 1 });
602
+ }
603
+ return;
604
+ }
605
+ for (const c of n.namedChildren)
606
+ walk(c);
607
+ };
608
+ walk(root);
609
+ return out;
610
+ }
611
+ function functionName(fnDef, src) {
612
+ let decl = fnDef.childForFieldName("declarator");
613
+ const seen = /* @__PURE__ */ new Set();
614
+ while (decl && !seen.has(decl.id)) {
615
+ seen.add(decl.id);
616
+ if (decl.type === "identifier")
617
+ return src.slice(decl.startIndex, decl.endIndex);
618
+ const inner = decl.childForFieldName("declarator");
619
+ if (inner) {
620
+ decl = inner;
621
+ continue;
622
+ }
623
+ const idChild = decl.namedChildren.find((c) => c.type === "identifier");
624
+ if (idChild)
625
+ return src.slice(idChild.startIndex, idChild.endIndex);
626
+ break;
627
+ }
628
+ return "<anonymous>";
629
+ }
630
+ function findViolationsDataflow(model, sources, opts = {}) {
631
+ const log = opts.log ?? (() => {
632
+ });
633
+ const maxViolations = opts.maxViolations ?? 40;
634
+ const refcountCheck = opts.refcountCheck ?? true;
635
+ const violations = [];
636
+ for (const { file, text } of sources) {
637
+ const root = parseC(text);
638
+ if (!root) {
639
+ log(`[dataflow] parse failed for ${file}`);
640
+ continue;
641
+ }
642
+ const fns = collectFunctions(root, text);
643
+ for (const fn of fns) {
644
+ const cfg = buildCfg(fn.body, text);
645
+ for (const obj of model.objects) {
646
+ for (const rule of obj.lockRules) {
647
+ const acquireFns = rule.acquireFns && rule.acquireFns.length > 0 ? [...rule.acquireFns] : [...DEFAULT_ACQUIRE_FNS];
648
+ const norm = normalizeLockToken(rule.lock);
649
+ const ctx = {
650
+ object: obj.object,
651
+ acquireFns: new Set(acquireFns),
652
+ releaseFns: new Set(releaseFnsFor(acquireFns)),
653
+ lockField: norm.lockField,
654
+ receiver: norm.receiver,
655
+ global: norm.global,
656
+ guardedFields: new Set(rule.guardedFields),
657
+ guardKey: rule.lock
658
+ };
659
+ const touches = cfg.blocks.some((b) => b.events.some((e) => e.kind === "field" && e.field && ctx.guardedFields.has(e.field)));
660
+ if (!touches)
661
+ continue;
662
+ const IN = lockSetIn(cfg, ctx);
663
+ const flaggedField = /* @__PURE__ */ new Set();
664
+ for (const b of cfg.blocks) {
665
+ if (P0(cfg, b.id) && b.id !== cfg.entry)
666
+ continue;
667
+ const held = new Set(IN.get(b.id));
668
+ for (const ev of b.events) {
669
+ if (ev.kind === "call" && ev.callee && ev.arg0) {
670
+ if (ctx.acquireFns.has(ev.callee))
671
+ held.add(ev.arg0);
672
+ else if (ctx.releaseFns.has(ev.callee))
673
+ held.delete(ev.arg0);
674
+ continue;
675
+ }
676
+ if (ev.kind !== "field" || !ev.field || !ctx.guardedFields.has(ev.field))
677
+ continue;
678
+ if (flaggedField.has(ev.field))
679
+ continue;
680
+ const required = ctx.global ? norm.key : `${ev.receiver}->${ctx.lockField}`;
681
+ if (held.has(required))
682
+ continue;
683
+ flaggedField.add(ev.field);
684
+ violations.push({
685
+ kind: "unlocked-field-access",
686
+ object: obj.object,
687
+ file,
688
+ line: ev.line,
689
+ functionName: fn.name,
690
+ invariant: `field '${ev.field}' of ${obj.object} must only be accessed while holding ${ctx.guardKey}`,
691
+ detail: `${fn.name}() accesses ${ev.canonStr} at line ${ev.line} while ${ctx.guardKey} is NOT in the held-lock set at that program point (lock-set dataflow over the function CFG). ` + (ctx.global ? `Global lock '${norm.key}' is not held on this path.` : `Required lock '${required}' resolved from the access receiver; not held on this path (e.g. released on an earlier branch or never taken).`)
692
+ });
693
+ }
694
+ }
695
+ }
696
+ for (const rule of obj.lifecycleRules) {
697
+ const freeFns = /* @__PURE__ */ new Set([rule.freeFn]);
698
+ const IN = freeSetIn(cfg, freeFns);
699
+ const flaggedKey = /* @__PURE__ */ new Set();
700
+ for (const b of cfg.blocks) {
701
+ if (P0(cfg, b.id) && b.id !== cfg.entry)
702
+ continue;
703
+ const freed = new Set(IN.get(b.id));
704
+ for (const ev of b.events) {
705
+ if (ev.kind === "call" && ev.callee && freeFns.has(ev.callee) && ev.arg0) {
706
+ freed.add(ev.arg0);
707
+ continue;
708
+ }
709
+ if (ev.kind === "def" && ev.canonStr) {
710
+ freed.delete(ev.canonStr);
711
+ continue;
712
+ }
713
+ if ((ev.kind === "field" || ev.kind === "ref") && ev.base) {
714
+ const baseFreed = freed.has(ev.base);
715
+ const exactFreed = ev.canonStr !== void 0 && freed.has(ev.canonStr);
716
+ let isUAF;
717
+ let freedThing;
718
+ if (ev.isAssignTarget) {
719
+ isUAF = baseFreed && ev.base !== ev.canonStr;
720
+ freedThing = ev.base;
721
+ } else {
722
+ isUAF = baseFreed || exactFreed;
723
+ freedThing = baseFreed ? ev.base : ev.canonStr ?? ev.base;
724
+ }
725
+ if (!isUAF)
726
+ continue;
727
+ const isUse = ev.isDeref || ev.kind === "ref";
728
+ if (!isUse)
729
+ continue;
730
+ if (flaggedKey.has(freedThing))
731
+ continue;
732
+ flaggedKey.add(freedThing);
733
+ violations.push({
734
+ kind: "use-after-free-order",
735
+ object: obj.object,
736
+ file,
737
+ line: ev.line,
738
+ functionName: fn.name,
739
+ invariant: `'${freedThing}' must not be used after ${rule.freeFn}() releases it`,
740
+ detail: `${fn.name}() uses ${ev.canonStr} at line ${ev.line} on a path where ${rule.freeFn}(${freedThing}) reaches it with no intervening re-assignment (reaching-free dataflow over the CFG \u2014 a free on a returning error branch does NOT reach this use).`
741
+ });
742
+ }
743
+ }
744
+ }
745
+ }
746
+ if (refcountCheck) {
747
+ for (const rule of obj.refcountRules) {
748
+ let getCount = 0;
749
+ let putCount = 0;
750
+ let firstPutLine = 0;
751
+ for (const b of cfg.blocks) {
752
+ for (const ev of b.events) {
753
+ if (ev.kind !== "call" || !ev.callee)
754
+ continue;
755
+ if (ev.callee === rule.getFn)
756
+ getCount++;
757
+ else if (ev.callee === rule.putFn) {
758
+ putCount++;
759
+ if (firstPutLine === 0)
760
+ firstPutLine = ev.line;
761
+ }
762
+ }
763
+ }
764
+ if (putCount > 0 && getCount === 0 && firstPutLine > 0) {
765
+ violations.push({
766
+ kind: "refcount-imbalance",
767
+ object: obj.object,
768
+ file,
769
+ line: firstPutLine,
770
+ functionName: fn.name,
771
+ invariant: `${rule.name}: every ${rule.putFn}() must be balanced by a prior ${rule.getFn}()`,
772
+ detail: `${fn.name}() calls ${rule.putFn}() (${putCount}x) but no ${rule.getFn}() \u2014 possible double-put / underflow if the ref wasn't held on entry. WEAK signal (per-function heuristic): the get is often legitimately in a caller (needs a call graph to resolve).`
773
+ });
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+ }
780
+ violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
781
+ const capped = violations.slice(0, maxViolations);
782
+ log(`[dataflow] ${violations.length} violation candidate(s)${capped.length < violations.length ? ` (capped to ${capped.length})` : ""}`);
783
+ return capped;
784
+ }
785
+ function P0(cfg, id) {
786
+ for (const b of cfg.blocks)
787
+ if (b.succs.has(id))
788
+ return false;
789
+ return true;
790
+ }
791
+
792
+ // packages/core/dist/stages/interproc-refcount.js
793
+ var collapseWs2 = (s) => s.replace(/\s+/g, "");
794
+ function canon2(node, src) {
795
+ switch (node.type) {
796
+ case "identifier":
797
+ case "field_identifier":
798
+ case "type_identifier":
799
+ return src.slice(node.startIndex, node.endIndex);
800
+ case "parenthesized_expression": {
801
+ const inner = node.namedChildren[0];
802
+ return inner ? canon2(inner, src) : collapseWs2(src.slice(node.startIndex, node.endIndex));
803
+ }
804
+ case "cast_expression": {
805
+ const value = node.childForFieldName("value");
806
+ return value ? canon2(value, src) : collapseWs2(src.slice(node.startIndex, node.endIndex));
807
+ }
808
+ case "pointer_expression": {
809
+ const op = node.child(0);
810
+ const arg = node.childForFieldName("argument") ?? node.namedChildren[0];
811
+ if (!arg)
812
+ return collapseWs2(src.slice(node.startIndex, node.endIndex));
813
+ return op && src.slice(op.startIndex, op.endIndex) === "&" ? canon2(arg, src) : "*" + canon2(arg, src);
814
+ }
815
+ case "field_expression": {
816
+ const arg = node.childForFieldName("argument");
817
+ const field = node.childForFieldName("field");
818
+ const base = arg ? canon2(arg, src) : "";
819
+ const f = field ? src.slice(field.startIndex, field.endIndex) : "";
820
+ return `${base}->${f}`;
821
+ }
822
+ case "subscript_expression": {
823
+ const arg = node.childForFieldName("argument") ?? node.namedChildren[0];
824
+ return (arg ? canon2(arg, src) : "") + "[]";
825
+ }
826
+ default:
827
+ return collapseWs2(src.slice(node.startIndex, node.endIndex));
828
+ }
829
+ }
830
+ var firstIdent2 = (s) => /^[A-Za-z_][A-Za-z0-9_]*/.exec(s)?.[0] ?? s;
831
+ function fieldOf(key) {
832
+ const idx = key.lastIndexOf("->");
833
+ return idx === -1 ? "" : key.slice(idx + 2).replace(/\[\]$/, "");
834
+ }
835
+ var NON_REFCOUNT_TOKENS = /* @__PURE__ */ new Set([
836
+ // plain atomic counters
837
+ "atomic_inc",
838
+ "atomic_dec",
839
+ "atomic_add",
840
+ "atomic_sub",
841
+ "atomic_inc_return",
842
+ "atomic_dec_return",
843
+ "atomic_dec_and_test",
844
+ "atomic_add_return",
845
+ "atomic64_inc",
846
+ "atomic64_dec",
847
+ "atomic_long_inc",
848
+ "atomic_long_dec",
849
+ "atomic_fetch_inc",
850
+ "atomic_fetch_dec",
851
+ // allocation pairs (ownership-by-alloc, not a get/put refcount)
852
+ "kzalloc",
853
+ "kmalloc",
854
+ "kcalloc",
855
+ "kvzalloc",
856
+ "kvmalloc",
857
+ "vmalloc",
858
+ "vzalloc",
859
+ "kfree",
860
+ "kvfree",
861
+ "vfree",
862
+ "kmem_cache_alloc",
863
+ "kmem_cache_zalloc",
864
+ "kmem_cache_free",
865
+ "alloc_skb",
866
+ "alloc_skb_with_frags",
867
+ "kfree_skb",
868
+ "consume_skb",
869
+ "skb_free",
870
+ "sk_msg_alloc",
871
+ "sk_msg_free",
872
+ "sk_msg_free_nocharge",
873
+ "napi_alloc_skb",
874
+ // page refs (never the modeled object)
875
+ "get_page",
876
+ "put_page",
877
+ "__free_page",
878
+ "__free_pages",
879
+ "free_page",
880
+ "free_pages",
881
+ "get_user_pages",
882
+ "put_user_page",
883
+ // module refs (never the modeled object)
884
+ "try_module_get",
885
+ "__module_get",
886
+ "module_put"
887
+ ]);
888
+ var GET_TOKENS = ["get", "hold", "grab", "_inc", "acquire", "pin", "_ref", "take"];
889
+ var PUT_TOKENS = ["put", "_free", "release", "_dec", "drop", "unref", "unpin", "destroy", "kill"];
890
+ var looksIdentifier = (s) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(s);
891
+ function isRefcountLikeRule(rule) {
892
+ const g = (rule.getFn ?? "").trim();
893
+ const p = (rule.putFn ?? "").trim();
894
+ if (!g || !p || !looksIdentifier(g) || !looksIdentifier(p))
895
+ return false;
896
+ if (g === p)
897
+ return false;
898
+ const gl = g.toLowerCase();
899
+ const pl = p.toLowerCase();
900
+ if (NON_REFCOUNT_TOKENS.has(gl) || NON_REFCOUNT_TOKENS.has(pl))
901
+ return false;
902
+ const gLooksGet = GET_TOKENS.some((t) => gl.includes(t));
903
+ const pLooksPut = PUT_TOKENS.some((t) => pl.includes(t));
904
+ if (gLooksGet && pLooksPut)
905
+ return true;
906
+ const stem = (s) => s.replace(/^(__|_)+/, "").split("_")[0] ?? "";
907
+ const shareStem = stem(gl).length >= 4 && stem(gl) === stem(pl);
908
+ return shareStem && (gLooksGet || pLooksPut);
909
+ }
910
+ function isDestructorFamily(name) {
911
+ return /(?:_|^)(?:destruct(?:or)?|dtor|free|release|evict|reclaim|destroy|put|dealloc|cleanup|teardown|drop|kill)(?:_|$)/i.test(name);
912
+ }
913
+ var line1 = (n) => n.startPosition.row + 1;
914
+ function extractParams(fnDef, src) {
915
+ const out = /* @__PURE__ */ new Set();
916
+ const pl = findDescendant(fnDef.childForFieldName("declarator"), "parameter_list");
917
+ if (!pl)
918
+ return out;
919
+ for (const p of pl.namedChildren) {
920
+ if (p.type !== "parameter_declaration")
921
+ continue;
922
+ const id = findDescendant(p.childForFieldName("declarator") ?? p, "identifier");
923
+ if (id)
924
+ out.add(src.slice(id.startIndex, id.endIndex));
925
+ }
926
+ return out;
927
+ }
928
+ function findDescendant(node, type) {
929
+ if (!node)
930
+ return null;
931
+ const q = [node];
932
+ while (q.length) {
933
+ const n = q.shift();
934
+ if (n.type === type)
935
+ return n;
936
+ for (const c of n.namedChildren)
937
+ q.push(c);
938
+ }
939
+ return null;
940
+ }
941
+ function calleeName(call, src) {
942
+ const fn = call.childForFieldName("function");
943
+ return fn && fn.type === "identifier" ? src.slice(fn.startIndex, fn.endIndex) : "";
944
+ }
945
+ function isReturnedInline(call) {
946
+ let n = call;
947
+ for (; ; ) {
948
+ const parent = n.parent;
949
+ if (!parent)
950
+ return false;
951
+ if (parent.type === "return_statement")
952
+ return true;
953
+ if (parent.type === "parenthesized_expression" || parent.type === "cast_expression") {
954
+ n = parent;
955
+ continue;
956
+ }
957
+ return false;
958
+ }
959
+ }
960
+ function resultKeyOf(call, src) {
961
+ const p = call.parent;
962
+ if (!p)
963
+ return void 0;
964
+ if (p.type === "assignment_expression" && p.childForFieldName("right")?.id === call.id) {
965
+ const left = p.childForFieldName("left");
966
+ return left ? canon2(left, src) : void 0;
967
+ }
968
+ if (p.type === "init_declarator" && p.childForFieldName("value")?.id === call.id) {
969
+ const decl = p.childForFieldName("declarator");
970
+ const id = decl && decl.type === "identifier" ? decl : findDescendant(decl, "identifier");
971
+ return id ? src.slice(id.startIndex, id.endIndex) : void 0;
972
+ }
973
+ return void 0;
974
+ }
975
+ function extractBody(body, src) {
976
+ const calls = [];
977
+ const aliases = [];
978
+ const fieldAliases = [];
979
+ const returnBases = /* @__PURE__ */ new Set();
980
+ const noteFieldAlias = (localName, valueNode) => {
981
+ if (!localName || valueNode.type !== "field_expression")
982
+ return;
983
+ const field = fieldOf(canon2(valueNode, src));
984
+ if (field)
985
+ fieldAliases.push({ local: localName, field });
986
+ };
987
+ const declLocalName = (decl) => {
988
+ let d = decl;
989
+ const seen = /* @__PURE__ */ new Set();
990
+ while (d && !seen.has(d.id)) {
991
+ seen.add(d.id);
992
+ if (d.type === "identifier")
993
+ return src.slice(d.startIndex, d.endIndex);
994
+ if (d.type === "pointer_declarator" || d.type === "parenthesized_declarator") {
995
+ d = d.childForFieldName("declarator") ?? d.namedChildren.find((c) => c.type.endsWith("declarator") || c.type === "identifier") ?? null;
996
+ continue;
997
+ }
998
+ return null;
999
+ }
1000
+ return null;
1001
+ };
1002
+ const walk = (n) => {
1003
+ switch (n.type) {
1004
+ case "call_expression": {
1005
+ const callee = calleeName(n, src);
1006
+ if (callee) {
1007
+ const args = n.childForFieldName("arguments");
1008
+ const a0 = args?.namedChildren[0];
1009
+ calls.push({
1010
+ callee,
1011
+ arg0Key: a0 ? canon2(a0, src) : "",
1012
+ ...resultKeyOf(n, src) ? { resultKey: resultKeyOf(n, src) } : {},
1013
+ returnedInline: isReturnedInline(n),
1014
+ line: line1(n)
1015
+ });
1016
+ }
1017
+ break;
1018
+ }
1019
+ case "assignment_expression": {
1020
+ const left = n.childForFieldName("left");
1021
+ const right = n.childForFieldName("right");
1022
+ if (left && right && right.type === "identifier") {
1023
+ aliases.push({ lhsKey: canon2(left, src), rhsIdent: src.slice(right.startIndex, right.endIndex), line: line1(n) });
1024
+ }
1025
+ if (left && right)
1026
+ noteFieldAlias(left.type === "identifier" ? src.slice(left.startIndex, left.endIndex) : null, right);
1027
+ break;
1028
+ }
1029
+ case "init_declarator": {
1030
+ const decl = n.childForFieldName("declarator");
1031
+ const value = n.childForFieldName("value");
1032
+ if (decl && decl.type === "identifier" && value && value.type === "identifier") {
1033
+ aliases.push({ lhsKey: src.slice(decl.startIndex, decl.endIndex), rhsIdent: src.slice(value.startIndex, value.endIndex), line: line1(n) });
1034
+ }
1035
+ if (decl && value)
1036
+ noteFieldAlias(declLocalName(decl), value);
1037
+ break;
1038
+ }
1039
+ case "return_statement": {
1040
+ const e = n.namedChildren[0];
1041
+ if (e)
1042
+ returnBases.add(firstIdent2(canon2(e, src)));
1043
+ break;
1044
+ }
1045
+ }
1046
+ for (const c of n.namedChildren)
1047
+ walk(c);
1048
+ };
1049
+ walk(body);
1050
+ return { calls, aliases, fieldAliases, returnBases };
1051
+ }
1052
+ function collectFnDefs(root, src, file) {
1053
+ const out = [];
1054
+ const walk = (n) => {
1055
+ if (n.type === "function_definition") {
1056
+ const body = n.childForFieldName("body");
1057
+ if (body && body.type === "compound_statement") {
1058
+ const { calls, aliases, fieldAliases, returnBases } = extractBody(body, src);
1059
+ out.push({
1060
+ name: fnName(n, src),
1061
+ file,
1062
+ startLine: line1(n),
1063
+ calls,
1064
+ aliases,
1065
+ fieldAliases,
1066
+ returnBases,
1067
+ params: extractParams(n, src)
1068
+ });
1069
+ }
1070
+ return;
1071
+ }
1072
+ for (const c of n.namedChildren)
1073
+ walk(c);
1074
+ };
1075
+ walk(root);
1076
+ return out;
1077
+ }
1078
+ function fnName(fnDef, src) {
1079
+ let decl = fnDef.childForFieldName("declarator");
1080
+ const seen = /* @__PURE__ */ new Set();
1081
+ while (decl && !seen.has(decl.id)) {
1082
+ seen.add(decl.id);
1083
+ if (decl.type === "identifier")
1084
+ return src.slice(decl.startIndex, decl.endIndex);
1085
+ const inner = decl.childForFieldName("declarator");
1086
+ if (inner) {
1087
+ decl = inner;
1088
+ continue;
1089
+ }
1090
+ const id = decl.namedChildren.find((c) => c.type === "identifier");
1091
+ if (id)
1092
+ return src.slice(id.startIndex, id.endIndex);
1093
+ break;
1094
+ }
1095
+ return "<anonymous>";
1096
+ }
1097
+ function buildCallGraph(sources) {
1098
+ const fns = [];
1099
+ for (const { file, text } of sources) {
1100
+ const root = parseC(text);
1101
+ if (!root)
1102
+ continue;
1103
+ fns.push(...collectFnDefs(root, text, file));
1104
+ }
1105
+ const byName = /* @__PURE__ */ new Map();
1106
+ for (const f of fns)
1107
+ if (!byName.has(f.name))
1108
+ byName.set(f.name, f);
1109
+ return { fns, byName };
1110
+ }
1111
+ function resolveWrapperOps(cg, rule) {
1112
+ const getOps = /* @__PURE__ */ new Map([
1113
+ [rule.getFn, { via: rule.getFn, resolvedFile: "", resolvedLine: 0, rawOp: rule.getFn }]
1114
+ ]);
1115
+ const putOps = /* @__PURE__ */ new Map([
1116
+ [rule.putFn, { via: rule.putFn, resolvedFile: "", resolvedLine: 0, rawOp: rule.putFn }]
1117
+ ]);
1118
+ for (let iter = 0; iter < cg.fns.length + 2; iter++) {
1119
+ let changed = false;
1120
+ for (const fn of cg.fns) {
1121
+ if (!getOps.has(fn.name)) {
1122
+ for (const call of fn.calls) {
1123
+ const seed = getOps.get(call.callee);
1124
+ if (!seed)
1125
+ continue;
1126
+ const produced = call.resultKey ? firstIdent2(call.resultKey) : firstIdent2(call.arg0Key);
1127
+ const returnsAcquired = call.returnedInline || !!produced && fn.returnBases.has(produced);
1128
+ if (returnsAcquired) {
1129
+ getOps.set(fn.name, {
1130
+ via: fn.name,
1131
+ resolvedFile: seed.resolvedFile || fn.file,
1132
+ resolvedLine: seed.resolvedLine || call.line,
1133
+ rawOp: seed.rawOp
1134
+ });
1135
+ changed = true;
1136
+ break;
1137
+ }
1138
+ }
1139
+ }
1140
+ if (!putOps.has(fn.name)) {
1141
+ for (const call of fn.calls) {
1142
+ const seed = putOps.get(call.callee);
1143
+ if (!seed)
1144
+ continue;
1145
+ if (call.arg0Key && fn.params.has(firstIdent2(call.arg0Key))) {
1146
+ putOps.set(fn.name, {
1147
+ via: fn.name,
1148
+ resolvedFile: seed.resolvedFile || fn.file,
1149
+ resolvedLine: seed.resolvedLine || call.line,
1150
+ rawOp: seed.rawOp
1151
+ });
1152
+ changed = true;
1153
+ break;
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+ if (!changed)
1159
+ break;
1160
+ }
1161
+ return { getOps, putOps };
1162
+ }
1163
+ function findInterprocRefcountCouplings(model, sources, opts = {}) {
1164
+ const log = opts.log ?? (() => {
1165
+ });
1166
+ const maxCouplings = opts.maxCouplings ?? 40;
1167
+ const cg = buildCallGraph(sources);
1168
+ const couplings = [];
1169
+ let rejectedRules = 0;
1170
+ for (const obj of model.objects) {
1171
+ for (const rule of obj.refcountRules) {
1172
+ if (!isRefcountLikeRule(rule)) {
1173
+ rejectedRules++;
1174
+ log(`[interproc-refcount] rejected non-refcount rule "${rule.name}" (${rule.getFn}/${rule.putFn})`);
1175
+ continue;
1176
+ }
1177
+ const { getOps, putOps } = resolveWrapperOps(cg, rule);
1178
+ const allPuts = [];
1179
+ for (const fn of cg.fns) {
1180
+ const inDestructor = isDestructorFamily(fn.name);
1181
+ for (const call of fn.calls) {
1182
+ const seed = putOps.get(call.callee);
1183
+ if (!seed)
1184
+ continue;
1185
+ let argField = fieldOf(call.arg0Key);
1186
+ let fieldViaAlias = false;
1187
+ if (!argField) {
1188
+ const local = firstIdent2(call.arg0Key);
1189
+ const fa = fn.fieldAliases.find((a) => a.local === local);
1190
+ if (fa) {
1191
+ argField = fa.field;
1192
+ fieldViaAlias = true;
1193
+ }
1194
+ }
1195
+ allPuts.push({
1196
+ fn: fn.name,
1197
+ file: fn.file,
1198
+ line: call.line,
1199
+ callee: call.callee,
1200
+ arg0: call.arg0Key,
1201
+ argField,
1202
+ resolvedFile: seed.resolvedFile || fn.file,
1203
+ rawOp: seed.rawOp,
1204
+ matchedBy: "same-var",
1205
+ // refined per get below
1206
+ fromDestructor: inDestructor,
1207
+ fieldViaAlias
1208
+ });
1209
+ }
1210
+ }
1211
+ for (const fn of cg.fns) {
1212
+ for (const call of fn.calls) {
1213
+ const seed = getOps.get(call.callee);
1214
+ if (!seed)
1215
+ continue;
1216
+ const refVar = call.resultKey ? call.resultKey : call.arg0Key;
1217
+ if (!refVar)
1218
+ continue;
1219
+ const refBase = firstIdent2(refVar);
1220
+ const aliasStore = fn.aliases.find((a) => a.rhsIdent === refBase && fieldOf(a.lhsKey));
1221
+ const storedField = aliasStore ? fieldOf(aliasStore.lhsKey) : fieldOf(refVar) || void 0;
1222
+ const getSite = {
1223
+ fn: fn.name,
1224
+ file: fn.file,
1225
+ line: call.line,
1226
+ callee: call.callee,
1227
+ refVar,
1228
+ ...storedField ? { storedField } : {},
1229
+ resolvedFile: seed.resolvedFile || fn.file,
1230
+ resolvedLine: seed.resolvedLine || call.line,
1231
+ rawOp: seed.rawOp
1232
+ };
1233
+ const matched = [];
1234
+ for (const p of allPuts) {
1235
+ const sameVar = p.fn === fn.name && p.file === fn.file && firstIdent2(p.arg0) === refBase && p.line >= call.line;
1236
+ const sameField = storedField !== void 0 && p.argField === storedField;
1237
+ if (sameVar)
1238
+ matched.push({ ...p, matchedBy: "same-var" });
1239
+ else if (sameField)
1240
+ matched.push({ ...p, matchedBy: "stored-field" });
1241
+ }
1242
+ const crossFile = getSite.resolvedFile !== getSite.file || matched.some((p) => p.resolvedFile !== p.file || p.file !== getSite.file);
1243
+ const crossFunction = matched.some((p) => p.fn !== fn.name);
1244
+ const returnedToCaller = fn.returnBases.has(refBase) || call.returnedInline;
1245
+ const sameVarPutCount = matched.filter((p) => p.matchedBy === "same-var").length;
1246
+ let verdict;
1247
+ if (matched.length > 0 || returnedToCaller)
1248
+ verdict = "balanced";
1249
+ else
1250
+ verdict = "leak-suspect";
1251
+ const wrapNote = getSite.callee !== rule.getFn ? `get-wrapper ${getSite.callee}() resolves to ${getSite.rawOp}() @ ${getSite.resolvedFile}:${getSite.resolvedLine}` : `direct ${rule.getFn}()`;
1252
+ const putNote = matched.length === 0 ? "no matching put found in the subsystem" : matched.map((p) => `${p.callee}(${p.arg0}) @ ${p.file}:${p.line} [${p.matchedBy}${p.fromDestructor ? " in destructor/free-path" : ""}${p.fieldViaAlias ? " (field via local copy)" : ""}${p.callee !== rule.putFn ? ` \u2192 ${p.rawOp}() @ ${p.resolvedFile}` : ""}]`).join("; ");
1253
+ couplings.push({
1254
+ object: obj.object,
1255
+ refcount: rule.name,
1256
+ getSite,
1257
+ putSites: matched,
1258
+ crossFile,
1259
+ crossFunction,
1260
+ verdict,
1261
+ sameVarPutCount,
1262
+ detail: `${rule.name}: ${wrapNote}. GET at ${getSite.file}:${getSite.line} in ${getSite.fn}()` + (storedField ? ` stores the ref into ->${storedField}` : "") + `. PUT(s): ${putNote}.` + (crossFile ? " COUPLING SPANS FILES (get/put resolve to a different file than the call site)." : "") + (verdict === "leak-suspect" ? returnedToCaller ? "" : " No release reaches the acquired ref on the analyzed paths \u2014 leak/UAF SUSPECT." : returnedToCaller && matched.length === 0 ? " The ref is RETURNED to the caller (ownership transfer) \u2014 balanced; the caller owns the release." : ` A release is present (${sameVarPutCount} same-var, ${matched.length - sameVarPutCount} via stored field) \u2014 balanced; the candidate is the cross-scope coupling itself.` + (sameVarPutCount >= 2 ? " NOTE: >=2 same-var releases \u2014 likely the exclusive error/success cleanup pair, NOT a double-put; confirming a real double-put needs the path-sensitive CFG (out of scope for this increment)." : ""))
1263
+ });
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+ const rank = (c) => (c.verdict === "balanced" ? 0 : 1) * 0 + // keep all; ordering below
1269
+ (c.crossFile ? 0 : 1);
1270
+ couplings.sort((a, b) => rank(a) - rank(b) || a.getSite.file.localeCompare(b.getSite.file) || a.getSite.line - b.getSite.line);
1271
+ const capped = couplings.slice(0, maxCouplings);
1272
+ log(`[interproc-refcount] ${couplings.length} coupling candidate(s) (${couplings.filter((c) => c.crossFile).length} cross-file, ${couplings.filter((c) => c.verdict !== "balanced").length} imbalance-suspect)${rejectedRules ? `, ${rejectedRules} non-refcount rule(s) rejected` : ""}${capped.length < couplings.length ? ` (capped to ${capped.length})` : ""}`);
1273
+ return capped;
1274
+ }
1275
+ function couplingsToHuntPlan(model, couplings, includeBalanced = false) {
1276
+ const selected = includeBalanced ? couplings : couplings.filter((c) => c.verdict !== "balanced");
1277
+ const bySite = /* @__PURE__ */ new Map();
1278
+ for (const c of selected) {
1279
+ const hint = `INTER-PROCEDURAL REFCOUNT COUPLING candidate (call-graph resolved). ${c.detail} Verify the get/put balance across all reachable paths \u2014 this pairing spans ${c.crossFile ? "files" : "functions"} and was resolved through refcount-op wrappers, so confirm the wrapper resolution and the field-based cross-function match are real (not a name-collision or an unfollowed alias).`;
1280
+ const existing = bySite.get(c.getSite.file);
1281
+ if (existing)
1282
+ existing.hint = `${existing.hint}
1283
+ ---
1284
+ ${hint}`;
1285
+ else
1286
+ bySite.set(c.getSite.file, { path: c.getSite.file, hint });
1287
+ }
1288
+ const brief = {
1289
+ bugClass: `inter-procedural refcount imbalance (seedless, call-graph): ${model.subsystem}`,
1290
+ pattern: `A refcount get/put coupling in ${model.subsystem} is split across functions and/or files: the get and put bottom out in refcount ops (e.g. sock_hold/sock_put) reached only through wrapper functions defined in a different file. A candidate is emitted when the acquired ref has no reaching release (leak/UAF) or a doubled release (double-put). Confirm the call-graph resolution and reject name-collision / unfollowed-alias false positives.`,
1291
+ fixReference: void 0
1292
+ };
1293
+ return { couplings, brief, candidates: [...bySite.values()] };
1294
+ }
1295
+
1296
+ // packages/core/dist/stages/invariant-spec-builder.js
1297
+ var DEFAULT_MAX_CHARS_PER_FILE = 24e3;
1298
+ function clipSource(s, n) {
1299
+ return s.length > n ? s.slice(0, n) + `
1300
+ ...[truncated ${s.length - n} chars]` : s;
1301
+ }
1302
+ async function extractInvariantSpec(input) {
1303
+ const maxCharsPerFile = input.maxCharsPerFile ?? DEFAULT_MAX_CHARS_PER_FILE;
1304
+ const sourceBlocks = input.sources.map((s) => `### FILE: ${s.file}
1305
+ \`\`\`c
1306
+ ${clipSource(s.text, maxCharsPerFile)}
1307
+ \`\`\``).join("\n\n");
1308
+ const messages = [{ role: "user", content: [{ type: "text", text: `## Subsystem source
1309
+
1310
+ ${sourceBlocks}` }] }];
1311
+ const rt = new LlmApiRuntime({ type: "api", ...input.model ? { model: input.model } : {}, timeout: 3e5 });
1312
+ try {
1313
+ const res = await rt.executeNative(input.system, messages, [input.tool], {
1314
+ onThinking() {
1315
+ },
1316
+ onDelta() {
1317
+ },
1318
+ onText() {
1319
+ },
1320
+ onUsage() {
1321
+ }
1322
+ });
1323
+ const call = (res.content ?? []).find((b) => b.type === "tool_use" && b.name === input.tool.name);
1324
+ return call?.input ?? null;
1325
+ } catch (e) {
1326
+ throw new Error(`${input.errorLabel} LLM call failed: ${String(e).slice(0, 200)}`);
1327
+ }
1328
+ }
1329
+
1330
+ // packages/core/dist/stages/subsystem-invariant-model.js
1331
+ import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
1332
+ import { dirname, isAbsolute, posix, relative, resolve, sep, win32 } from "node:path";
1333
+ var INVARIANT_MODEL_VERSION = 1;
1334
+ function pathIsWithin(root, candidate) {
1335
+ const rel = relative(root, candidate);
1336
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
1337
+ }
1338
+ function resolveContainedSourcePath(sourceRoot, relativePath) {
1339
+ const normalized = relativePath.replaceAll("\\", "/");
1340
+ if (!normalized || normalized.includes("\0") || posix.isAbsolute(normalized) || win32.isAbsolute(normalized) || /^[A-Za-z]:/.test(normalized))
1341
+ return null;
1342
+ const parts = normalized.split("/");
1343
+ if (parts.some((part) => !part || part === "." || part === ".."))
1344
+ return null;
1345
+ try {
1346
+ const rootAbs = resolve(sourceRoot);
1347
+ const candidateAbs = resolve(rootAbs, ...parts);
1348
+ if (!pathIsWithin(rootAbs, candidateAbs))
1349
+ return null;
1350
+ const rootReal = realpathSync(rootAbs);
1351
+ const candidateReal = realpathSync(candidateAbs);
1352
+ return pathIsWithin(rootReal, candidateReal) ? candidateReal : null;
1353
+ } catch {
1354
+ return null;
1355
+ }
1356
+ }
1357
+ function readSource(sourceRoot, file) {
1358
+ const path = resolveContainedSourcePath(sourceRoot, file);
1359
+ if (!path)
1360
+ return null;
1361
+ try {
1362
+ return readFileSync(path, "utf8");
1363
+ } catch {
1364
+ return null;
1365
+ }
1366
+ }
1367
+ var MODEL_TOOL = {
1368
+ name: "emit_invariant_model",
1369
+ description: "Emit the EXPLICIT, STRUCTURED invariant model for each key object in the subsystem: lock discipline, refcount balance, lifetime rules, init-order. This is a MODEL, not a bug report \u2014 do not hunt bugs here; describe the rules the code is SUPPOSED to uphold so a separate deterministic checker can find violations.",
1370
+ input_schema: {
1371
+ type: "object",
1372
+ properties: {
1373
+ objects: {
1374
+ type: "array",
1375
+ description: "One model per key heap object / state machine in the subsystem.",
1376
+ items: {
1377
+ type: "object",
1378
+ properties: {
1379
+ object: { type: "string", description: "The struct/object, e.g. 'struct nfc_llcp_local'." },
1380
+ allocSite: { type: "string", description: "Function where it's allocated (provenance)." },
1381
+ freeSite: { type: "string", description: "Function where it's freed (provenance)." },
1382
+ lockRules: {
1383
+ type: "array",
1384
+ description: "Which lock guards which fields.",
1385
+ items: {
1386
+ type: "object",
1387
+ properties: {
1388
+ lock: { type: "string", description: "The lock token as written at the acquire site, e.g. 'local->sockets.lock' or 'llcp_devices_lock'." },
1389
+ guardedFields: { type: "array", items: { type: "string" }, description: "Field names (as written after -> or .) this lock protects from concurrent mutation." },
1390
+ acquireFns: { type: "array", items: { type: "string" }, description: "Acquire-call tokens, e.g. ['spin_lock','spin_lock_bh','mutex_lock']. Omit for the common defaults." }
1391
+ },
1392
+ required: ["lock", "guardedFields"]
1393
+ }
1394
+ },
1395
+ refcountRules: {
1396
+ type: "array",
1397
+ description: "get/put pairings that must balance.",
1398
+ items: {
1399
+ type: "object",
1400
+ properties: {
1401
+ name: { type: "string", description: "Human name, e.g. 'nfc_llcp_local ref'." },
1402
+ getFn: { type: "string", description: "The acquire/get call token, e.g. 'nfc_llcp_local_get' or 'sock_hold'." },
1403
+ putFn: { type: "string", description: "The release/put call token, e.g. 'nfc_llcp_local_put' or 'sock_put'." }
1404
+ },
1405
+ required: ["name", "getFn", "putFn"]
1406
+ }
1407
+ },
1408
+ lifecycleRules: {
1409
+ type: "array",
1410
+ description: "Free/release calls after which the freed variable must not be used.",
1411
+ items: {
1412
+ type: "object",
1413
+ properties: {
1414
+ freeFn: { type: "string", description: "The free/release call token, e.g. 'kfree', 'nfc_llcp_local_put', 'sock_put'." },
1415
+ note: { type: "string", description: "What it frees (provenance)." }
1416
+ },
1417
+ required: ["freeFn"]
1418
+ }
1419
+ },
1420
+ initOrder: { type: "array", items: { type: "string" }, description: "Fields that must be initialized before first use (notes)." }
1421
+ },
1422
+ required: ["object", "lockRules", "refcountRules", "lifecycleRules"]
1423
+ }
1424
+ },
1425
+ notes: { type: "string", description: "Free-form provenance notes about the model." }
1426
+ },
1427
+ required: ["objects"]
1428
+ }
1429
+ };
1430
+ var MODEL_SYSTEM = "You are a kernel subsystem analyst building a REUSABLE INVARIANT MODEL of a subsystem's key objects. You are given the source. Your job is NOT to find bugs \u2014 it is to describe, per key heap object / state machine, the RULES the code is supposed to uphold, so a separate deterministic checker can later find code that violates them.\n\nFor each key object emit:\n - LOCK DISCIPLINE: which lock (spinlock/mutex/rwlock/seqlock) guards which FIELDS. Give the lock token EXACTLY as it is written at the acquire site (e.g. 'local->sockets.lock', 'llcp_devices_lock', 'sk->sk_lock.slock'), and the field names exactly as written after '->' or '.'. If a nonstandard acquire wrapper is used, list it in acquireFns.\n - REFCOUNT BALANCE: the get/put (or hold/put, kref_get/kref_put) call-token PAIRS that must balance.\n - LIFETIME: the free/release call tokens (kfree, *_put, *_free) after which the object must NOT be used.\n - INIT-ORDER: fields that must be initialized before first use.\n\nBe precise about TOKENS \u2014 the downstream checker matches your tokens literally against the source, so a lock token or field name that doesn't appear verbatim in the code is useless. Prefer the objects reachable from UNPRIVILEGED syscalls. Emit the model via emit_invariant_model.";
1431
+ async function buildInvariantModel(input) {
1432
+ const log = input.log ?? (() => {
1433
+ });
1434
+ if (!input.subsystemFiles || input.subsystemFiles.length === 0) {
1435
+ throw new Error("invariant model build needs at least one subsystemFile");
1436
+ }
1437
+ const sources = [];
1438
+ for (const file of input.subsystemFiles) {
1439
+ const text = readSource(input.sourceRoot, file);
1440
+ if (text == null) {
1441
+ log(`[invariant-model] could not read ${file}`);
1442
+ continue;
1443
+ }
1444
+ sources.push({ file, text });
1445
+ }
1446
+ if (sources.length === 0) {
1447
+ throw new Error("invariant model build could not read any subsystemFile under sourceRoot");
1448
+ }
1449
+ const modelOut = await extractInvariantSpec({
1450
+ sources,
1451
+ system: MODEL_SYSTEM,
1452
+ tool: MODEL_TOOL,
1453
+ ...input.model ? { model: input.model } : {},
1454
+ ...input.maxCharsPerFile !== void 0 ? { maxCharsPerFile: input.maxCharsPerFile } : {},
1455
+ errorLabel: "invariant-model"
1456
+ });
1457
+ const objects = Array.isArray(modelOut?.objects) ? modelOut.objects : [];
1458
+ if (objects.length === 0)
1459
+ throw new Error("model build emitted no objects");
1460
+ const normalized = objects.filter((o) => o && typeof o.object === "string").map((o) => ({
1461
+ object: o.object,
1462
+ ...o.allocSite ? { allocSite: o.allocSite } : {},
1463
+ ...o.freeSite ? { freeSite: o.freeSite } : {},
1464
+ lockRules: Array.isArray(o.lockRules) ? o.lockRules.filter((r) => r && typeof r.lock === "string" && Array.isArray(r.guardedFields)) : [],
1465
+ refcountRules: Array.isArray(o.refcountRules) ? o.refcountRules.filter((r) => r && typeof r.getFn === "string" && typeof r.putFn === "string") : [],
1466
+ lifecycleRules: Array.isArray(o.lifecycleRules) ? o.lifecycleRules.filter((r) => r && typeof r.freeFn === "string") : [],
1467
+ ...Array.isArray(o.initOrder) ? { initOrder: o.initOrder } : {}
1468
+ }));
1469
+ if (normalized.length === 0)
1470
+ throw new Error("model build emitted no well-formed objects");
1471
+ log(`[invariant-model] built model for ${normalized.length} object(s) in ${input.subsystem}`);
1472
+ return {
1473
+ modelVersion: INVARIANT_MODEL_VERSION,
1474
+ subsystem: input.subsystem,
1475
+ subsystemFiles: sources.map((s) => s.file),
1476
+ objects: normalized,
1477
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
1478
+ ...modelOut?.notes ? { notes: modelOut.notes } : {}
1479
+ };
1480
+ }
1481
+ function storeInvariantModel(model, path) {
1482
+ mkdirSync(dirname(path), { recursive: true });
1483
+ writeFileSync(path, JSON.stringify(model, null, 2) + "\n", "utf8");
1484
+ return path;
1485
+ }
1486
+ function loadInvariantModel(path) {
1487
+ const model = JSON.parse(readFileSync(path, "utf8"));
1488
+ if (model.modelVersion !== INVARIANT_MODEL_VERSION) {
1489
+ throw new Error(`stored model is schema v${model.modelVersion}, checker is v${INVARIANT_MODEL_VERSION} \u2014 rebuild the model`);
1490
+ }
1491
+ if (!Array.isArray(model.objects))
1492
+ throw new Error("stored model has no objects array");
1493
+ return model;
1494
+ }
1495
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set([
1496
+ "if",
1497
+ "for",
1498
+ "while",
1499
+ "switch",
1500
+ "do",
1501
+ "else",
1502
+ "return",
1503
+ "sizeof",
1504
+ "typeof",
1505
+ "__attribute__",
1506
+ "case",
1507
+ "default",
1508
+ "goto",
1509
+ "asm",
1510
+ "__asm__",
1511
+ "static_assert"
1512
+ ]);
1513
+ function blankCommentsAndStrings(src) {
1514
+ const out = src.split("");
1515
+ let i = 0;
1516
+ const n = src.length;
1517
+ const blank = (a, b) => {
1518
+ for (let k = a; k < b && k < n; k++)
1519
+ if (out[k] !== "\n")
1520
+ out[k] = " ";
1521
+ };
1522
+ while (i < n) {
1523
+ const c = src[i];
1524
+ const c2 = src[i + 1];
1525
+ if (c === "/" && c2 === "/") {
1526
+ let j = i + 2;
1527
+ while (j < n && src[j] !== "\n")
1528
+ j++;
1529
+ blank(i, j);
1530
+ i = j;
1531
+ } else if (c === "/" && c2 === "*") {
1532
+ let j = i + 2;
1533
+ while (j < n && !(src[j] === "*" && src[j + 1] === "/"))
1534
+ j++;
1535
+ j = Math.min(n, j + 2);
1536
+ blank(i, j);
1537
+ i = j;
1538
+ } else if (c === '"' || c === "'") {
1539
+ const quote = c;
1540
+ let j = i + 1;
1541
+ while (j < n) {
1542
+ if (src[j] === "\\") {
1543
+ j += 2;
1544
+ continue;
1545
+ }
1546
+ if (src[j] === quote) {
1547
+ j++;
1548
+ break;
1549
+ }
1550
+ if (src[j] === "\n")
1551
+ break;
1552
+ j++;
1553
+ }
1554
+ blank(i, j);
1555
+ i = j;
1556
+ } else {
1557
+ i++;
1558
+ }
1559
+ }
1560
+ return out.join("");
1561
+ }
1562
+ function lineAt(src, offset) {
1563
+ let line = 1;
1564
+ for (let k = 0; k < offset && k < src.length; k++)
1565
+ if (src[k] === "\n")
1566
+ line++;
1567
+ return line;
1568
+ }
1569
+ var IDENT_RE = /[A-Za-z_][A-Za-z0-9_]*/;
1570
+ function splitCFunctions(src) {
1571
+ const blanked = blankCommentsAndStrings(src);
1572
+ const fns = [];
1573
+ let depth = 0;
1574
+ const n = blanked.length;
1575
+ for (let i = 0; i < n; i++) {
1576
+ const ch = blanked[i];
1577
+ if (ch === "{") {
1578
+ if (depth === 0) {
1579
+ let j = i - 1;
1580
+ while (j >= 0 && /\s/.test(blanked[j]))
1581
+ j--;
1582
+ if (j >= 0 && blanked[j] === ")") {
1583
+ let pd = 0;
1584
+ let k = j;
1585
+ for (; k >= 0; k--) {
1586
+ if (blanked[k] === ")")
1587
+ pd++;
1588
+ else if (blanked[k] === "(") {
1589
+ pd--;
1590
+ if (pd === 0)
1591
+ break;
1592
+ }
1593
+ }
1594
+ if (k >= 0) {
1595
+ let m = k - 1;
1596
+ while (m >= 0 && /\s/.test(blanked[m]))
1597
+ m--;
1598
+ let e = m;
1599
+ while (e >= 0 && /[A-Za-z0-9_]/.test(blanked[e]))
1600
+ e--;
1601
+ const name = blanked.slice(e + 1, m + 1).trim();
1602
+ if (name && IDENT_RE.test(name) && !CONTROL_KEYWORDS.has(name)) {
1603
+ let bd = 0;
1604
+ let end = i;
1605
+ for (; end < n; end++) {
1606
+ if (blanked[end] === "{")
1607
+ bd++;
1608
+ else if (blanked[end] === "}") {
1609
+ bd--;
1610
+ if (bd === 0)
1611
+ break;
1612
+ }
1613
+ }
1614
+ let sigStart = e + 1;
1615
+ while (sigStart > 0 && blanked[sigStart - 1] !== "\n")
1616
+ sigStart--;
1617
+ fns.push({
1618
+ name,
1619
+ startLine: lineAt(src, sigStart),
1620
+ endLine: lineAt(src, end),
1621
+ text: src.slice(sigStart, Math.min(n, end + 1)),
1622
+ startOffset: sigStart
1623
+ });
1624
+ depth = 0;
1625
+ i = end;
1626
+ continue;
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+ depth++;
1632
+ } else if (ch === "}") {
1633
+ if (depth > 0)
1634
+ depth--;
1635
+ }
1636
+ }
1637
+ return fns;
1638
+ }
1639
+ var DEFAULT_ACQUIRE_FNS2 = [
1640
+ "spin_lock",
1641
+ "spin_lock_bh",
1642
+ "spin_lock_irq",
1643
+ "spin_lock_irqsave",
1644
+ "spin_trylock",
1645
+ "raw_spin_lock",
1646
+ "raw_spin_lock_bh",
1647
+ "raw_spin_lock_irqsave",
1648
+ "mutex_lock",
1649
+ "mutex_lock_interruptible",
1650
+ "mutex_trylock",
1651
+ "read_lock",
1652
+ "read_lock_bh",
1653
+ "write_lock",
1654
+ "write_lock_bh",
1655
+ "down",
1656
+ "down_read",
1657
+ "down_write",
1658
+ "down_interruptible"
1659
+ ];
1660
+ var escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1661
+ function bodyHoldsLock(blankedBody, lock, acquireFns) {
1662
+ const lockRe = escapeRe(lock);
1663
+ for (const fn of acquireFns) {
1664
+ const re = new RegExp(`\\b${escapeRe(fn)}\\s*\\(\\s*&?\\s*${lockRe}\\b`);
1665
+ if (re.test(blankedBody))
1666
+ return true;
1667
+ }
1668
+ return false;
1669
+ }
1670
+ function fieldAccessLines(blankedBody, bodyStartLine, field) {
1671
+ const re = new RegExp(`(->|\\.)\\s*${escapeRe(field)}\\b`, "g");
1672
+ const lines = [];
1673
+ let m;
1674
+ while ((m = re.exec(blankedBody)) !== null) {
1675
+ lines.push(bodyStartLine + countNewlines(blankedBody, m.index));
1676
+ }
1677
+ return lines;
1678
+ }
1679
+ function countNewlines(s, upto) {
1680
+ let c = 0;
1681
+ for (let k = 0; k < upto && k < s.length; k++)
1682
+ if (s[k] === "\n")
1683
+ c++;
1684
+ return c;
1685
+ }
1686
+ function parseFreeCall(blankedBody, callIndex, fn) {
1687
+ const open = blankedBody.indexOf("(", callIndex + fn.length);
1688
+ if (open === -1)
1689
+ return null;
1690
+ let depth = 0;
1691
+ let firstArgEnd = -1;
1692
+ let callEnd = -1;
1693
+ for (let end = open + 1; end < blankedBody.length; end++) {
1694
+ const ch = blankedBody[end];
1695
+ if (ch === "(")
1696
+ depth++;
1697
+ else if (ch === ")") {
1698
+ if (depth === 0) {
1699
+ callEnd = end + 1;
1700
+ if (firstArgEnd === -1)
1701
+ firstArgEnd = end;
1702
+ break;
1703
+ }
1704
+ depth--;
1705
+ } else if (ch === "," && depth === 0 && firstArgEnd === -1) {
1706
+ firstArgEnd = end;
1707
+ }
1708
+ }
1709
+ if (firstArgEnd === -1 || callEnd === -1)
1710
+ return null;
1711
+ const arg = blankedBody.slice(open + 1, firstArgEnd).trim();
1712
+ const m = /^&?\s*([A-Za-z_][A-Za-z0-9_]*)/.exec(arg);
1713
+ return m ? { freedVar: m[1], callEnd } : null;
1714
+ }
1715
+ function findInvariantViolations(model, sources, opts = {}) {
1716
+ return findViolationsDataflow(model, sources, {
1717
+ ...opts.maxViolations !== void 0 ? { maxViolations: opts.maxViolations } : {},
1718
+ ...opts.refcountCheck !== void 0 ? { refcountCheck: opts.refcountCheck } : {},
1719
+ ...opts.log ? { log: opts.log } : {}
1720
+ });
1721
+ }
1722
+ function findInvariantViolationsTokenLevel(model, sources, opts = {}) {
1723
+ const log = opts.log ?? (() => {
1724
+ });
1725
+ const maxViolations = opts.maxViolations ?? 40;
1726
+ const refcountCheck = opts.refcountCheck ?? true;
1727
+ const violations = [];
1728
+ for (const { file, text } of sources) {
1729
+ const blanked = blankCommentsAndStrings(text);
1730
+ const fns = splitCFunctions(text);
1731
+ for (const fn of fns) {
1732
+ const bStart = fn.startOffset;
1733
+ const bEnd = bStart + fn.text.length;
1734
+ const blankedBody = blanked.slice(bStart, bEnd);
1735
+ const bodyStartLine = fn.startLine;
1736
+ for (const obj of model.objects) {
1737
+ for (const rule of obj.lockRules) {
1738
+ const acquireFns = rule.acquireFns && rule.acquireFns.length > 0 ? rule.acquireFns : DEFAULT_ACQUIRE_FNS2;
1739
+ const holds = bodyHoldsLock(blankedBody, rule.lock, acquireFns);
1740
+ if (holds)
1741
+ continue;
1742
+ for (const field of rule.guardedFields) {
1743
+ const lines = fieldAccessLines(blankedBody, bodyStartLine, field);
1744
+ if (lines.length === 0)
1745
+ continue;
1746
+ violations.push({
1747
+ kind: "unlocked-field-access",
1748
+ object: obj.object,
1749
+ file,
1750
+ line: lines[0],
1751
+ functionName: fn.name,
1752
+ invariant: `field '${field}' of ${obj.object} must only be accessed while holding ${rule.lock}`,
1753
+ detail: `${fn.name}() accesses ->${field} (line ${lines[0]}${lines.length > 1 ? `, +${lines.length - 1} more` : ""}) but never acquires ${rule.lock} in this function body. Confirm the lock isn't held by a caller (intra-procedural check \u2014 caller-held locks are a false positive).`
1754
+ });
1755
+ }
1756
+ }
1757
+ for (const rule of obj.lifecycleRules) {
1758
+ const callRe = new RegExp(`\\b${escapeRe(rule.freeFn)}\\s*\\(`, "g");
1759
+ let cm;
1760
+ while ((cm = callRe.exec(blankedBody)) !== null) {
1761
+ const parsed = parseFreeCall(blankedBody, cm.index, rule.freeFn);
1762
+ if (!parsed)
1763
+ continue;
1764
+ const { freedVar, callEnd } = parsed;
1765
+ const freeLine = bodyStartLine + countNewlines(blankedBody, cm.index);
1766
+ const useRe = new RegExp(`\\b${escapeRe(freedVar)}\\b`, "g");
1767
+ useRe.lastIndex = callEnd;
1768
+ let um;
1769
+ while ((um = useRe.exec(blankedBody)) !== null) {
1770
+ const after = blankedBody.slice(um.index + freedVar.length);
1771
+ if (/^\s*=[^=]/.test(after))
1772
+ break;
1773
+ const isDeref = /^\s*(->|\[)/.test(after);
1774
+ const isCallArg = /^\s*[,)]/.test(after);
1775
+ if (isDeref || isCallArg) {
1776
+ const useLine = bodyStartLine + countNewlines(blankedBody, um.index);
1777
+ violations.push({
1778
+ kind: "use-after-free-order",
1779
+ object: obj.object,
1780
+ file,
1781
+ line: useLine,
1782
+ functionName: fn.name,
1783
+ invariant: `'${freedVar}' must not be used after ${rule.freeFn}() releases it`,
1784
+ detail: `${fn.name}() calls ${rule.freeFn}(${freedVar}) at line ${freeLine}, then uses ${freedVar} again at line ${useLine}. Confirm this isn't a different-object reassignment or a path where the free didn't run (intra-procedural, path-insensitive check).`
1785
+ });
1786
+ break;
1787
+ }
1788
+ }
1789
+ }
1790
+ }
1791
+ if (refcountCheck) {
1792
+ for (const rule of obj.refcountRules) {
1793
+ const getCount = (blankedBody.match(new RegExp(`\\b${escapeRe(rule.getFn)}\\s*\\(`, "g")) ?? []).length;
1794
+ const putM = new RegExp(`\\b${escapeRe(rule.putFn)}\\s*\\(`, "g").exec(blankedBody);
1795
+ const putCount = (blankedBody.match(new RegExp(`\\b${escapeRe(rule.putFn)}\\s*\\(`, "g")) ?? []).length;
1796
+ if (putCount > 0 && getCount === 0 && putM) {
1797
+ const putLine = bodyStartLine + countNewlines(blankedBody, putM.index);
1798
+ violations.push({
1799
+ kind: "refcount-imbalance",
1800
+ object: obj.object,
1801
+ file,
1802
+ line: putLine,
1803
+ functionName: fn.name,
1804
+ invariant: `${rule.name}: every ${rule.putFn}() must be balanced by a prior ${rule.getFn}()`,
1805
+ detail: `${fn.name}() calls ${rule.putFn}() (${putCount}x) but no ${rule.getFn}() \u2014 possible double-put / underflow if the ref wasn't held on entry. WEAK signal: the get is often legitimately in a caller.`
1806
+ });
1807
+ }
1808
+ }
1809
+ }
1810
+ }
1811
+ }
1812
+ }
1813
+ violations.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
1814
+ const capped = violations.slice(0, maxViolations);
1815
+ log(`[invariant-check] ${violations.length} violation candidate(s)${capped.length < violations.length ? ` (capped to ${capped.length})` : ""}`);
1816
+ return capped;
1817
+ }
1818
+ var KIND_LABEL = {
1819
+ "unlocked-field-access": "UNLOCKED FIELD ACCESS",
1820
+ "use-after-free-order": "USE-AFTER-FREE ORDER",
1821
+ "refcount-imbalance": "REFCOUNT IMBALANCE"
1822
+ };
1823
+ function violationsToHuntPlan(model, violations) {
1824
+ const bySite = /* @__PURE__ */ new Map();
1825
+ for (const v of violations) {
1826
+ const hint = `${KIND_LABEL[v.kind]} candidate (deterministic, from stored invariant model). Invariant: ${v.invariant}. Site: ${v.file}:${v.line} in ${v.functionName}(). ${v.detail}`;
1827
+ const existing = bySite.get(v.file);
1828
+ if (existing)
1829
+ existing.hint = `${existing.hint}
1830
+ ---
1831
+ ${hint}`;
1832
+ else
1833
+ bySite.set(v.file, { path: v.file, hint });
1834
+ }
1835
+ const kinds = [...new Set(violations.map((v) => v.kind))].map((k) => KIND_LABEL[k]).join(", ");
1836
+ const brief = {
1837
+ bugClass: `invariant-model violation (seedless): ${kinds || "lock/refcount/lifetime discipline"}`,
1838
+ pattern: `A key object in ${model.subsystem} violates its modeled invariant \u2014 a guarded field touched without its lock, a variable used after free, or an unbalanced refcount put. The candidate sites below were found by a deterministic checker against the stored invariant model; verify each is a real, reachable violation and not a caller-held-lock / path-insensitive false positive.`,
1839
+ fixReference: void 0
1840
+ };
1841
+ return { model, violations, brief, candidates: [...bySite.values()] };
1842
+ }
1843
+ async function runSubsystemInvariantHunt(input) {
1844
+ const log = input.log ?? (() => {
1845
+ });
1846
+ let model;
1847
+ let modelLoaded = false;
1848
+ if (!input.rebuildModel) {
1849
+ try {
1850
+ model = loadInvariantModel(input.modelPath);
1851
+ modelLoaded = true;
1852
+ log(`[invariant] loaded stored model ${input.modelPath} (${model.objects.length} object(s)) \u2014 no LLM call`);
1853
+ } catch {
1854
+ model = await buildInvariantModel(input);
1855
+ storeInvariantModel(model, input.modelPath);
1856
+ log(`[invariant] built + stored model ${input.modelPath}`);
1857
+ }
1858
+ } else {
1859
+ model = await buildInvariantModel(input);
1860
+ storeInvariantModel(model, input.modelPath);
1861
+ log(`[invariant] rebuilt + stored model ${input.modelPath}`);
1862
+ }
1863
+ const sources = [];
1864
+ for (const file of input.subsystemFiles) {
1865
+ const text = readSource(input.sourceRoot, file);
1866
+ if (text != null)
1867
+ sources.push({ file, text });
1868
+ }
1869
+ if (sources.length === 0)
1870
+ throw new Error("invariant hunt could not read any subsystemFile under sourceRoot");
1871
+ const violations = findInvariantViolations(model, sources, { ...input.findOptions, log });
1872
+ const plan = violationsToHuntPlan(model, violations);
1873
+ if (input.skipHunt || plan.candidates.length === 0) {
1874
+ return { model, modelPath: input.modelPath, modelLoaded, violations, plan };
1875
+ }
1876
+ const verify = input.verify ?? makeSkepticVerifier({ sourceRoot: input.sourceRoot, runtime: input.runtime });
1877
+ const hunt = await runHuntScan({
1878
+ sourceRoot: input.sourceRoot,
1879
+ candidates: plan.candidates,
1880
+ brief: plan.brief,
1881
+ runtime: input.runtime,
1882
+ ...input.finderModels && input.finderModels.length > 0 ? { models: input.finderModels } : {},
1883
+ verify,
1884
+ ...input.exploitability ? { exploitability: input.exploitability } : {},
1885
+ log
1886
+ });
1887
+ return { model, modelPath: input.modelPath, modelLoaded, violations, plan, hunt };
1888
+ }
1889
+
1890
+ // packages/core/dist/stages/dynamic-witness.js
1891
+ import { createHash } from "node:crypto";
1892
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1893
+ import { dirname as dirname2 } from "node:path";
1894
+ function dualViewCandidateFromContext(ctx, assumption, bodies, subsystem) {
1895
+ if (!ctx.dualView || !ctx.object || !ctx.pairedEntry)
1896
+ return null;
1897
+ const entryA = ctx.pairedEntry;
1898
+ const entryB = ctx.caller;
1899
+ const sources = [];
1900
+ const push = (label, fn) => {
1901
+ const code = bodies.get(fn);
1902
+ if (code)
1903
+ sources.push({ label, fn, code });
1904
+ };
1905
+ push("entryA (establishing view)", entryA);
1906
+ push("entryB (skipping view)", entryB);
1907
+ if (assumption.subject !== entryA && assumption.subject !== entryB) {
1908
+ push("relied-on subject", assumption.subject);
1909
+ }
1910
+ return {
1911
+ assumptionId: ctx.assumptionId,
1912
+ subsystem,
1913
+ object: ctx.object,
1914
+ subject: ctx.subject,
1915
+ entryA,
1916
+ entryB,
1917
+ establisherToken: ctx.establisherToken,
1918
+ kind: assumption.kind,
1919
+ securityRelevance: assumption.securityRelevance,
1920
+ predicate: assumption.predicate,
1921
+ unprivEntry: ctx.unprivEntry,
1922
+ sources,
1923
+ detail: ctx.detail
1924
+ };
1925
+ }
1926
+ var DEFAULT_RACE_THREADS = 4;
1927
+ var DEFAULT_RACE_ITERS = 2e5;
1928
+ var DEFAULT_RACE_CONFIG = { threads: DEFAULT_RACE_THREADS, iters: DEFAULT_RACE_ITERS };
1929
+ var RACE_SHAPED_KINDS = /* @__PURE__ */ new Set([
1930
+ "lock-held",
1931
+ "ownership-exclusive",
1932
+ "refcount-positive",
1933
+ "called-once",
1934
+ "revalidated"
1935
+ ]);
1936
+ function isRaceShapedCandidate(c) {
1937
+ if (RACE_SHAPED_KINDS.has(c.kind))
1938
+ return true;
1939
+ if (c.unprivEntry && (c.securityRelevance === "lifetime" || c.securityRelevance === "type"))
1940
+ return true;
1941
+ return false;
1942
+ }
1943
+ function resolveWitnessMode(mode, c) {
1944
+ if (mode === "single")
1945
+ return "single";
1946
+ if (mode === "race")
1947
+ return "race";
1948
+ return isRaceShapedCandidate(c) ? "race" : "single";
1949
+ }
1950
+ var SYNTH_SYSTEM = "You are a world-class Linux kernel exploit engineer writing a PROOF-OF-CONCEPT trigger, not a description. You are given a DUAL-VIEW / CROSS-PHASE assumption candidate: a long-lived kernel object reached by TWO distinct entries/phases, where entry A establishes a guarantee (a held lock / a validated ref / an exclusive owner / a once-only init) and entry B reaches the SAME object WITHOUT re-establishing it. Your job: produce a SELF-CONTAINED, UNPRIVILEGED C program that (a) sets up the object via entry A's path to establish the assumed state, (b) drives entry B's path in the way that VIOLATES entry A's assumption on the SAME object instance, and (c) triggers the dereference/use that would fault \u2014 aiming for a KASAN use-after-free / out-of-bounds / double-free on the candidate's object.\n\nHARD RULES:\n 1. Output ONE complete C file in a single ```c fenced block. It must compile with `gcc -O0 -static -o poc poc.c -lpthread` on a modern glibc. No external headers beyond libc/uapi; if a syscall wrapper is missing, use syscall(2) with the __NR_ number directly.\n 2. Drop privileges early: call setuid(65534)/setgid(65534) after any setup that is itself unprivileged, so the trigger runs as an UNPRIVILEGED user (the whole point \u2014 the object must be reachable without root). If the whole flow is unprivileged (SCM_RIGHTS, AF_UNIX, io_uring, fuse mounts in a userns), drop immediately.\n 3. To hit a cross-phase RACE, spawn threads that hammer entry A and entry B concurrently on the SAME object (the same fd / same socket / same registered buffer), pinned to CPUs if it helps, looping thousands of times.\n 4. Do NOT print fabricated kernel logs. NEVER printf a 'BUG: KASAN' / 'use-after-free' / 'general protection' string \u2014 the witness is the KERNEL's splat on the serial console, and a PoC that prints one is rejected as fabrication. Print only your own progress markers.\n 5. Make it deterministic-ish and bounded: exit within ~20s even if it does not trigger (the harness caps the run). Emit via the fenced C block only; a one-paragraph plan before it is fine.";
1951
+ var RACE_SYNTH_SYSTEM = "You are a world-class Linux kernel exploit engineer writing a genuinely CONCURRENT, MULTI-THREAD PROOF-OF-CONCEPT race trigger, not a description and not a single-threaded stub. You are given a DUAL-VIEW / CROSS-PHASE assumption candidate whose violation is a RACE: a long-lived kernel object reached by TWO distinct entries/phases, where entry A holds a guarantee (a held lock / a validated ref / an exclusive owner / a live refcount) and entry B, run CONCURRENTLY on the SAME object instance, tears it down or mutates it out from under A \u2014 the DirtyCred/DirtyPipe class (a put racing a use, a same-type object swapped mid-use, a double-fetch, a concurrent teardown). Your job: produce a SELF-CONTAINED, UNPRIVILEGED C program that races entry A against entry B on the SAME object to trip a KASAN use-after-free / out-of-bounds / double-free.\n\nHARD RULES:\n 1. Output ONE complete C file in a single ```c fenced block. It MUST compile with `gcc -O0 -static -o poc poc.c -lpthread` on a modern glibc. Include <pthread.h>, <sched.h>, <unistd.h>. No external headers beyond libc/uapi; if a syscall wrapper is missing, use syscall(2) with __NR_ directly.\n 2. GENUINELY CONCURRENT STRUCTURE \u2014 all of these, not a sketch:\n \u2022 A `pthread_barrier_t` start barrier so every worker begins the contended window at the SAME instant (initialize it for exactly the number of threads that wait on it, and have each worker `pthread_barrier_wait` before its hammer loop).\n \u2022 N worker threads via `pthread_create` \u2014 roughly half driving entry A's path, half driving entry B's path, all on the SAME shared object (same fd / same socket / same registered buffer/id).\n \u2022 CPU PINNING: each worker calls `sched_setaffinity` onto a DISTINCT core (cpu = worker_index % get_nprocs()) so A and B run on different physical CPUs and actually overlap.\n \u2022 A tight HAMMER LOOP of a HIGH iteration count (100000+ per thread) around the contended syscall pair, and an OUTER loop that RE-ARMS the object (re-create/re-register) each attempt so the window is retried many times \u2014 a race almost never fires on the first pass.\n 3. Drop privileges early (setuid/setgid to 65534) once setup is unprivileged, OR run fully unprivileged from the start (SCM_RIGHTS, AF_UNIX, io_uring, userns) \u2014 the object must be reachable without root.\n 4. Do NOT print fabricated kernel logs. NEVER printf a 'BUG: KASAN' / 'use-after-free' / 'general protection' string \u2014 the witness is the KERNEL's splat on serial; a PoC that prints one is rejected as fabrication. Print only your own progress markers (e.g. iteration counts).\n 5. Bounded: cap total wall time (~20s) with an overall attempt ceiling and join all threads before exit, so it terminates even when the race does not fire (the harness also caps the run). Emit the fenced C block; a one-paragraph plan before it is fine.";
1952
+ function raceScaffoldSpec(c, race) {
1953
+ return `## RACE SYNTHESIS \u2014 emit a CONCURRENT multi-thread PoC
1954
+ This seam is race-shaped: the violation of '${c.predicate}' on struct ${c.object} is a concurrent interleave, NOT a sequential A-then-B ordering. A single-threaded PoC CANNOT witness it. Race entry A (${c.entryA}, which establishes ${c.establisherToken}) against entry B (${c.entryB}, which skips it) on the SAME struct ${c.object} instance.
1955
+
1956
+ REQUIRED structure (all must appear in the emitted C):
1957
+ - pthread_barrier_t start barrier; every worker pthread_barrier_wait()s before the hammer loop.
1958
+ - ${race.threads} worker threads via pthread_create: ~half drive ${c.entryA}'s syscall path, ~half drive ${c.entryB}'s, all on ONE shared object.
1959
+ - sched_setaffinity pinning each worker to a distinct CPU (cpu = idx % get_nprocs()).
1960
+ - a per-thread hammer loop of ${race.iters}+ iterations around the contended syscalls, inside an outer loop that RE-ARMS the object each attempt.
1961
+ - join all threads; bounded ~20s wall clock; no fabricated splat strings.
1962
+ `;
1963
+ }
1964
+ function buildRaceSynthesisPrompt(input) {
1965
+ const c = input.candidate;
1966
+ const race = input.race ?? DEFAULT_RACE_CONFIG;
1967
+ const excerpts = c.sources.map((s) => `### ${s.label} \u2014 ${s.fn}()
1968
+ \`\`\`c
1969
+ ${clip(s.code, 8e3)}
1970
+ \`\`\``).join("\n\n");
1971
+ const header = `## Dual-view RACE candidate (subsystem: ${c.subsystem})
1972
+ - object (type reached + contended by both views): struct ${c.object}
1973
+ - entry A (ESTABLISHES ${c.establisherToken}): ${c.entryA}()
1974
+ - entry B (SKIPS ${c.establisherToken}, race it): ${c.entryB}()
1975
+ - relied-on subject: ${c.subject}()
1976
+ - assumption kind: ${c.kind} (${c.securityRelevance})
1977
+ - relied-on precondition (entry B races to violate): ${c.predicate}
1978
+ - unprivileged-reachable entry B: ${c.unprivEntry ? "yes" : "unknown"}
1979
+
1980
+ ${c.detail}
1981
+ `;
1982
+ if (input.round > 1) {
1983
+ return `${header}
1984
+ ${raceScaffoldSpec(c, race)}
1985
+ ## Prior race PoC (round ${input.round - 1}) did NOT witness the bug \u2014 FIX it.
1986
+ \`\`\`c
1987
+ ${clip(input.priorCSource ?? "", 9e3)}
1988
+ \`\`\`
1989
+
1990
+ ## Boot feedback (compile error, or serial/dmesg with NO object-bound KASAN splat)
1991
+ \`\`\`
1992
+ ${clip(input.priorFeedback ?? "(none captured)", 6e3)}
1993
+ \`\`\`
1994
+
1995
+ Diagnose why the race did not fire (window too narrow? threads not on the same instance? barrier/affinity missing? too few iterations? compile error?) and emit a corrected complete concurrent C file that keeps the required race structure.
1996
+
1997
+ ## Source excerpts
1998
+
1999
+ ${excerpts}`;
2000
+ }
2001
+ return `${header}
2002
+ ${raceScaffoldSpec(c, race)}
2003
+ ## Source excerpts
2004
+
2005
+ ${excerpts}
2006
+
2007
+ Synthesize the concurrent race PoC now.`;
2008
+ }
2009
+ function buildSynthesisPrompt(input) {
2010
+ if (input.mode === "race")
2011
+ return buildRaceSynthesisPrompt(input);
2012
+ const c = input.candidate;
2013
+ const excerpts = c.sources.map((s) => `### ${s.label} \u2014 ${s.fn}()
2014
+ \`\`\`c
2015
+ ${clip(s.code, 8e3)}
2016
+ \`\`\``).join("\n\n");
2017
+ const header = `## Dual-view assumption candidate (subsystem: ${c.subsystem})
2018
+ - object (type reached by both views): struct ${c.object}
2019
+ - entry A (ESTABLISHES ${c.establisherToken}): ${c.entryA}()
2020
+ - entry B (SKIPS ${c.establisherToken}): ${c.entryB}()
2021
+ - relied-on subject: ${c.subject}()
2022
+ - assumption kind: ${c.kind} (${c.securityRelevance})
2023
+ - relied-on precondition (entry B may violate): ${c.predicate}
2024
+ - unprivileged-reachable entry B: ${c.unprivEntry ? "yes" : "unknown"}
2025
+
2026
+ ${c.detail}
2027
+ `;
2028
+ if (input.round > 1) {
2029
+ return `${header}
2030
+ ## Prior PoC (round ${input.round - 1}) did NOT witness the bug \u2014 FIX it.
2031
+ \`\`\`c
2032
+ ${clip(input.priorCSource ?? "", 9e3)}
2033
+ \`\`\`
2034
+
2035
+ ## Boot feedback (compile error, or serial/dmesg with NO object-bound KASAN splat)
2036
+ \`\`\`
2037
+ ${clip(input.priorFeedback ?? "(none captured)", 6e3)}
2038
+ \`\`\`
2039
+
2040
+ Diagnose why it did not trigger (wrong syscall sequence? never reached entry B on the same instance? window too narrow? compile error?) and emit a corrected complete C file.
2041
+
2042
+ ## Source excerpts
2043
+
2044
+ ${excerpts}`;
2045
+ }
2046
+ return `${header}
2047
+ ## Source excerpts
2048
+
2049
+ ${excerpts}
2050
+
2051
+ Synthesize the PoC now.`;
2052
+ }
2053
+ function clip(s, n) {
2054
+ return s.length > n ? s.slice(0, n) + `
2055
+ /* ...[truncated ${s.length - n} chars] */` : s;
2056
+ }
2057
+ function extractCFromLlmOutput(text) {
2058
+ if (!text)
2059
+ return null;
2060
+ const fenced = text.match(/```(?:c|cpp|c\+\+)?\s*\n([\s\S]*?)```/i);
2061
+ if (fenced && fenced[1].trim())
2062
+ return fenced[1].replace(/\s+$/, "") + "\n";
2063
+ if (/#include\b|\bint\s+main\s*\(/.test(text))
2064
+ return text.replace(/\s+$/, "") + "\n";
2065
+ return null;
2066
+ }
2067
+ function makeDefaultSynthesizePoc(_runtime, model, timeoutMs = 3e5) {
2068
+ return async (input) => {
2069
+ const rt = new LlmApiRuntime({ type: "api", timeout: timeoutMs, ...model ? { model } : {} });
2070
+ const system = input.mode === "race" ? RACE_SYNTH_SYSTEM : SYNTH_SYSTEM;
2071
+ const res = await rt.executeNative(system, [{ role: "user", content: [{ type: "text", text: buildSynthesisPrompt(input) }] }], []);
2072
+ const text = (res.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("\n");
2073
+ const cSource = extractCFromLlmOutput(text);
2074
+ if (!cSource)
2075
+ return null;
2076
+ return { cSource };
2077
+ };
2078
+ }
2079
+ var defaultBootPoc = (cSource) => {
2080
+ const report = {
2081
+ raw: "",
2082
+ crashType: "unknown",
2083
+ faultingFunction: "unknown",
2084
+ stackFrames: [],
2085
+ reproducer: cSource,
2086
+ reproducerLanguage: "c"
2087
+ };
2088
+ return runReproducerInKernelVm(report);
2089
+ };
2090
+ var WITNESS_SIGNATURES = [
2091
+ { pattern: /KASAN:\s+slab-use-after-free|KASAN:.*use-after-free|use-after-free in/i, signature: "kasan-uaf" },
2092
+ { pattern: /KASAN:\s+slab-out-of-bounds|KASAN:.*out-of-bounds|out-of-bounds in/i, signature: "kasan-oob" },
2093
+ { pattern: /KASAN:.*double-free|double-free or invalid-free/i, signature: "kasan-double-free" },
2094
+ { pattern: /KASAN:.*invalid-free/i, signature: "kasan-invalid-free" },
2095
+ { pattern: /KASAN:.*stack-out-of-bounds/i, signature: "kasan-stack-oob" }
2096
+ ];
2097
+ var ANY_CRASH = /BUG:\s*KASAN|KASAN:\s*(?:slab-|global-|stack-|vmalloc-|use-after-free|out-of-bounds|invalid-free|double-free)|UBSAN|general protection fault|NULL pointer dereference|kernel NULL pointer|BUG:\s*KCSAN/i;
2098
+ function candidateReferenceTokens(c) {
2099
+ const toks = /* @__PURE__ */ new Set();
2100
+ for (const t of [c.object, c.subject, c.entryA, c.entryB, ...c.sources.map((s) => s.fn)]) {
2101
+ const s = (t ?? "").trim();
2102
+ if (s && s.length >= 4)
2103
+ toks.add(s);
2104
+ }
2105
+ return [...toks];
2106
+ }
2107
+ function extractSplatRegion(dmesg) {
2108
+ if (!dmesg)
2109
+ return null;
2110
+ const lines = dmesg.split(/\r?\n/);
2111
+ let start = -1;
2112
+ for (let i = 0; i < lines.length; i++) {
2113
+ if (/BUG:\s*KASAN|KASAN:\s*(slab|global|stack|use-after|out-of|double-free|invalid-free)|general protection fault|NULL pointer dereference/i.test(lines[i])) {
2114
+ start = i;
2115
+ break;
2116
+ }
2117
+ }
2118
+ if (start < 0)
2119
+ return null;
2120
+ if (start > 0 && /^={5,}\s*$/.test(lines[start - 1].replace(/^\[\s*\d+\.\d+\]\s*/, "").trim()))
2121
+ start -= 1;
2122
+ let end = Math.min(lines.length, start + 80);
2123
+ for (let i = start + 1; i < Math.min(lines.length, start + 120); i++) {
2124
+ if (/^={5,}\s*$/.test(lines[i].replace(/^\[\s*\d+\.\d+\]\s*/, "").trim())) {
2125
+ end = i + 1;
2126
+ break;
2127
+ }
2128
+ }
2129
+ return lines.slice(start, end).join("\n");
2130
+ }
2131
+ function pocFabricatesSplat(cSource, dmesg) {
2132
+ if (!cSource || !dmesg)
2133
+ return false;
2134
+ const src = cSource;
2135
+ for (const line of dmesg.split(/\r?\n/)) {
2136
+ const t = line.trim();
2137
+ if (t.length < 12)
2138
+ continue;
2139
+ if (!ANY_CRASH.test(t))
2140
+ continue;
2141
+ const bare = t.replace(/^\[\s*\d+\.\d+\]\s*/, "");
2142
+ if (bare.length >= 12 && src.includes(bare))
2143
+ return true;
2144
+ }
2145
+ return false;
2146
+ }
2147
+ function checkWitness(candidate, cSource, result) {
2148
+ const dmesg = `${result.dmesg ?? ""}
2149
+ ${result.output ?? ""}`;
2150
+ const splat = extractSplatRegion(dmesg);
2151
+ if (!result.executed) {
2152
+ return { witnessed: false, objectBound: false, reason: result.compiled ? "PoC compiled but did not execute in the VM" : "PoC did not compile", ...splat ? { splat } : {} };
2153
+ }
2154
+ const sigHit = WITNESS_SIGNATURES.find((s) => s.pattern.test(dmesg));
2155
+ if (!sigHit) {
2156
+ return { witnessed: false, objectBound: false, reason: ANY_CRASH.test(dmesg) ? "a kernel crash fired but not a promote-class KASAN UAF/OOB/double-free splat" : "no KASAN splat in the boot output", ...splat ? { splat } : {} };
2157
+ }
2158
+ if (pocFabricatesSplat(cSource, dmesg)) {
2159
+ return { witnessed: false, objectBound: false, signature: sigHit.signature, reason: "REJECTED: a splat line is a verbatim substring of the PoC source \u2014 fabricated, not a kernel-emitted splat", ...splat ? { splat } : {} };
2160
+ }
2161
+ const region = splat ?? dmesg;
2162
+ const tokens = candidateReferenceTokens(candidate);
2163
+ const boundTo = tokens.find((t) => new RegExp(`\\b${escapeRe2(t)}\\b`).test(region));
2164
+ if (!boundTo) {
2165
+ return { witnessed: false, objectBound: false, signature: sigHit.signature, reason: `KASAN ${sigHit.signature} splat fired but is NOT bound to the candidate \u2014 no candidate object/function [${tokens.join(", ")}] appears in the splat (incidental splat)`, ...splat ? { splat } : {} };
2166
+ }
2167
+ return {
2168
+ witnessed: true,
2169
+ objectBound: true,
2170
+ signature: sigHit.signature,
2171
+ boundTo,
2172
+ ...splat ? { splat } : {},
2173
+ reason: `object-bound ${sigHit.signature}: the splat references '${boundTo}' (the candidate's ${boundTo === candidate.object ? "object type" : "entry/subject"}) \u2014 a real dynamic witness of the assumption violation`
2174
+ };
2175
+ }
2176
+ var escapeRe2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2177
+ async function witnessAssumptionViolation(candidate, deps = {}) {
2178
+ const log = deps.log ?? (() => {
2179
+ });
2180
+ const maxRounds = Math.max(1, deps.maxRounds ?? 3);
2181
+ const synthesize = deps.synthesizePoc ?? makeDefaultSynthesizePoc(deps.runtime ?? "api", deps.model);
2182
+ const boot = deps.bootPoc ?? defaultBootPoc;
2183
+ const resolvedMode = resolveWitnessMode(deps.witnessMode ?? "auto", candidate);
2184
+ const raceConfig = {
2185
+ threads: deps.raceConfig?.threads ?? DEFAULT_RACE_THREADS,
2186
+ iters: deps.raceConfig?.iters ?? DEFAULT_RACE_ITERS
2187
+ };
2188
+ if (resolvedMode === "race") {
2189
+ log(`[witness] race-shaped seam \u2192 CONCURRENT PoC synthesis (${raceConfig.threads} threads \xD7 ${raceConfig.iters} iters)`);
2190
+ }
2191
+ const attempts = [];
2192
+ let priorCSource;
2193
+ let priorFeedback;
2194
+ let lastCSource;
2195
+ for (let round = 1; round <= maxRounds; round++) {
2196
+ log(`[witness] ${candidate.assumptionId} ${candidate.entryA}\u21C4${candidate.entryB} on struct ${candidate.object} \u2014 round ${round}/${maxRounds}: synthesizing PoC`);
2197
+ let synth = null;
2198
+ try {
2199
+ synth = await synthesize({
2200
+ candidate,
2201
+ round,
2202
+ // Only the race path adds mode/race to the input — a single-resolved seam keeps
2203
+ // the original input shape (mode undefined), so its prompt is byte-for-byte unchanged.
2204
+ ...resolvedMode === "race" ? { mode: "race", race: raceConfig } : {},
2205
+ ...priorCSource ? { priorCSource } : {},
2206
+ ...priorFeedback ? { priorFeedback } : {}
2207
+ });
2208
+ } catch (err) {
2209
+ log(`[witness] synthesis error: ${err instanceof Error ? err.message : String(err)}`);
2210
+ }
2211
+ if (!synth || !synth.cSource.trim()) {
2212
+ attempts.push({ round, synthesized: false, compiled: false, executed: false, timedOut: false, feedbackTail: "synthesizer produced no PoC" });
2213
+ priorFeedback = "the previous round produced no usable C \u2014 emit a complete compilable file in a ```c block";
2214
+ continue;
2215
+ }
2216
+ lastCSource = synth.cSource;
2217
+ log(`[witness] booting PoC (${synth.cSource.length} bytes) in KASAN VM`);
2218
+ let result;
2219
+ try {
2220
+ result = await boot(synth.cSource, candidate);
2221
+ } catch (err) {
2222
+ const msg = err instanceof Error ? err.message : String(err);
2223
+ attempts.push({ round, synthesized: true, compiled: false, executed: false, timedOut: false, feedbackTail: `boot error: ${msg}`.slice(0, 4e3) });
2224
+ priorCSource = synth.cSource;
2225
+ priorFeedback = `the KASAN boot errored: ${msg}`.slice(0, 4e3);
2226
+ continue;
2227
+ }
2228
+ const check = checkWitness(candidate, synth.cSource, result);
2229
+ const feedbackTail = (result.compiled ? result.dmesg || result.output : result.output).slice(-4e3);
2230
+ const attempt = {
2231
+ round,
2232
+ synthesized: true,
2233
+ compiled: result.compiled,
2234
+ executed: result.executed,
2235
+ timedOut: result.timedOut,
2236
+ check,
2237
+ feedbackTail
2238
+ };
2239
+ attempts.push(attempt);
2240
+ log(`[witness] round ${round}: compiled=${result.compiled} executed=${result.executed} \u21D2 ${check.witnessed ? "WITNESSED" : "no witness"} (${check.reason})`);
2241
+ if (check.witnessed) {
2242
+ return {
2243
+ candidate,
2244
+ verdict: "confirmed",
2245
+ attempts,
2246
+ witnessedAttempt: attempt,
2247
+ finalCSource: synth.cSource,
2248
+ ...check.splat ? { splat: check.splat } : {},
2249
+ summary: `CONFIRMED: object-bound ${check.signature} witnessed on struct ${candidate.object} at round ${round} \u2014 ${check.reason}`
2250
+ };
2251
+ }
2252
+ priorCSource = synth.cSource;
2253
+ priorFeedback = `${check.reason}
2254
+ --- boot output tail ---
2255
+ ${feedbackTail}`;
2256
+ }
2257
+ const everRan = attempts.some((a) => a.executed);
2258
+ const verdict = everRan ? "refuted" : "inconclusive";
2259
+ return {
2260
+ candidate,
2261
+ verdict,
2262
+ attempts,
2263
+ ...lastCSource ? { finalCSource: lastCSource } : {},
2264
+ summary: verdict === "refuted" ? `refuted: ${attempts.length} PoC round(s) compiled + ran but produced no object-bound KASAN splat on struct ${candidate.object} (assume-FP holds)` : `inconclusive: no PoC compiled+executed within ${maxRounds} round(s) (AEG/synthesis limit \u2014 NOT evidence the assumption holds)`
2265
+ };
2266
+ }
2267
+ var WITNESSABLE_RELEVANCE = /* @__PURE__ */ new Set(["lifetime", "type", "bounds"]);
2268
+ function preFilterDualViewContexts(contexts, byId, bodies) {
2269
+ const kept = [];
2270
+ const dropped = [];
2271
+ for (const ctx of contexts) {
2272
+ if (!ctx.dualView) {
2273
+ kept.push(ctx);
2274
+ continue;
2275
+ }
2276
+ const a = byId.get(ctx.assumptionId);
2277
+ if (!a) {
2278
+ kept.push(ctx);
2279
+ continue;
2280
+ }
2281
+ if (!WITNESSABLE_RELEVANCE.has(a.securityRelevance)) {
2282
+ dropped.push({
2283
+ ctx,
2284
+ reason: `securityRelevance '${a.securityRelevance}' is not memory-safety \u2014 the KASAN oracle can only witness lifetime/type/bounds (UAF/OOB/double-free), so this can never be confirmed dynamically`
2285
+ });
2286
+ continue;
2287
+ }
2288
+ const entryBBody = bodies.get(ctx.caller);
2289
+ if (entryBBody) {
2290
+ const selfReason = subjectSelfEnforces(entryBBody, a);
2291
+ if (selfReason) {
2292
+ dropped.push({ ctx, reason: `entryB ${ctx.caller}() ${selfReason}` });
2293
+ continue;
2294
+ }
2295
+ const toks = [a.oracle.establisherToken, ...a.oracle.establisherAliases ?? []].filter(isMechanizableEstablisher);
2296
+ const present = toks.find((t) => new RegExp(`\\b${escapeRe2(t)}\\s*\\(`).test(entryBBody));
2297
+ if (present) {
2298
+ dropped.push({
2299
+ ctx,
2300
+ reason: `entryB ${ctx.caller}() directly calls establisher '${present}' \u2014 both phases take the guard (illusory dual-view skip)`
2301
+ });
2302
+ continue;
2303
+ }
2304
+ }
2305
+ kept.push(ctx);
2306
+ }
2307
+ return { kept, dropped };
2308
+ }
2309
+ var ROTATION_STATE_VERSION = 1;
2310
+ var ROTATION_TTL_MS = 14 * 24 * 60 * 60 * 1e3;
2311
+ var ROTATION_RETENTION_MS = 2 * ROTATION_TTL_MS;
2312
+ var ROTATION_MAX_ENTRIES = 5e3;
2313
+ function candidateStableId(parts) {
2314
+ const key = [parts.object, parts.entryA, parts.entryB, parts.entryBFile ?? "", parts.assumptionId, parts.predicate ?? ""].join("\0");
2315
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
2316
+ }
2317
+ function candidateStableIdFromContext(ctx, predicate) {
2318
+ return candidateStableId({
2319
+ object: ctx.object ?? "",
2320
+ entryA: ctx.pairedEntry ?? "",
2321
+ entryB: ctx.caller,
2322
+ entryBFile: ctx.callerFile,
2323
+ assumptionId: ctx.assumptionId,
2324
+ predicate
2325
+ });
2326
+ }
2327
+ function loadRotationState(path) {
2328
+ try {
2329
+ const raw = JSON.parse(readFileSync2(path, "utf8"));
2330
+ if (raw && raw.version === ROTATION_STATE_VERSION && raw.candidates && typeof raw.candidates === "object") {
2331
+ return raw;
2332
+ }
2333
+ } catch {
2334
+ }
2335
+ return { version: ROTATION_STATE_VERSION, candidates: {} };
2336
+ }
2337
+ function pruneRotationState(state, now = Date.now()) {
2338
+ const entries = Object.entries(state.candidates).filter(([, e]) => {
2339
+ const t = Date.parse(e.witnessedAt);
2340
+ return Number.isFinite(t) && now - t < ROTATION_RETENTION_MS;
2341
+ }).sort((a, b) => Date.parse(b[1].witnessedAt) - Date.parse(a[1].witnessedAt)).slice(0, ROTATION_MAX_ENTRIES);
2342
+ return { version: ROTATION_STATE_VERSION, candidates: Object.fromEntries(entries) };
2343
+ }
2344
+ function saveRotationState(path, state, now = Date.now()) {
2345
+ const pruned = pruneRotationState(state, now);
2346
+ mkdirSync2(dirname2(path), { recursive: true });
2347
+ writeFileSync2(path, JSON.stringify(pruned, null, 2) + "\n", "utf8");
2348
+ }
2349
+ function isWitnessedWithinTtl(state, id, now = Date.now(), ttlMs = ROTATION_TTL_MS) {
2350
+ const e = state.candidates[id];
2351
+ if (!e)
2352
+ return false;
2353
+ const t = Date.parse(e.witnessedAt);
2354
+ return Number.isFinite(t) && now - t < ttlMs;
2355
+ }
2356
+ async function witnessDualViewContexts(input) {
2357
+ const log = input.log ?? input.deps?.log ?? (() => {
2358
+ });
2359
+ const byId = new Map(input.kept.map((a) => [a.id, a]));
2360
+ const cap = input.maxCandidates ?? 10;
2361
+ const now = input.now ?? Date.now();
2362
+ const dualCtxs = input.contexts.filter((c) => c.dualView);
2363
+ const pf = preFilterDualViewContexts(dualCtxs, byId, input.bodies);
2364
+ for (const d of pf.dropped) {
2365
+ log(`[witness] pre-filter dropped ${d.ctx.caller}\u21C4${d.ctx.pairedEntry ?? "?"} on struct ${d.ctx.object ?? "?"}: ${d.reason}`);
2366
+ }
2367
+ if (pf.dropped.length) {
2368
+ log(`[witness] pre-filter kept ${pf.kept.length}/${dualCtxs.length} dual-view candidate(s) (${pf.dropped.length} benign dropped)`);
2369
+ }
2370
+ const rotationState = input.rotationStatePath ? loadRotationState(input.rotationStatePath) : null;
2371
+ const idOf = (ctx) => candidateStableIdFromContext(ctx, byId.get(ctx.assumptionId)?.predicate ?? "");
2372
+ const selected = [];
2373
+ let skippedByRotation = 0;
2374
+ for (const ctx of pf.kept) {
2375
+ if (rotationState && isWitnessedWithinTtl(rotationState, idOf(ctx), now)) {
2376
+ skippedByRotation++;
2377
+ continue;
2378
+ }
2379
+ selected.push(ctx);
2380
+ if (selected.length >= cap)
2381
+ break;
2382
+ }
2383
+ if (skippedByRotation) {
2384
+ log(`[witness] rotation skipped ${skippedByRotation} candidate(s) already witnessed within the ${ROTATION_TTL_MS / 864e5}d TTL \u2014 testing fresh ones`);
2385
+ }
2386
+ const candidates = [];
2387
+ const idByCandidate = /* @__PURE__ */ new Map();
2388
+ for (const ctx of selected) {
2389
+ const a = byId.get(ctx.assumptionId);
2390
+ if (!a)
2391
+ continue;
2392
+ const cand = dualViewCandidateFromContext(ctx, a, input.bodies, input.subsystem);
2393
+ if (cand) {
2394
+ candidates.push(cand);
2395
+ idByCandidate.set(cand, idOf(ctx));
2396
+ }
2397
+ }
2398
+ log(`[witness] running the dynamic oracle on ${candidates.length} dual-view candidate(s) (bypassing the static skeptic)`);
2399
+ const results = [];
2400
+ for (const cand of candidates) {
2401
+ results.push(await witnessAssumptionViolation(cand, { ...input.deps, log }));
2402
+ }
2403
+ if (rotationState && input.rotationStatePath) {
2404
+ const at = new Date(now).toISOString();
2405
+ for (const r of results) {
2406
+ const id = idByCandidate.get(r.candidate);
2407
+ if (!id)
2408
+ continue;
2409
+ rotationState.candidates[id] = {
2410
+ verdict: r.verdict,
2411
+ witnessedAt: at,
2412
+ object: r.candidate.object,
2413
+ entryA: r.candidate.entryA,
2414
+ entryB: r.candidate.entryB
2415
+ };
2416
+ }
2417
+ saveRotationState(input.rotationStatePath, rotationState, now);
2418
+ }
2419
+ return {
2420
+ results,
2421
+ confirmed: results.filter((r) => r.verdict === "confirmed"),
2422
+ refuted: results.filter((r) => r.verdict === "refuted"),
2423
+ inconclusive: results.filter((r) => r.verdict === "inconclusive")
2424
+ };
2425
+ }
2426
+
2427
+ // packages/core/dist/stages/assumption-mining.js
2428
+ var ASSUMPTION_MODEL_VERSION = 1;
2429
+ var ASSUMPTION_ARCHETYPE_FEWSHOTS = [
2430
+ // A9 dual-view / DirtyCred — a consumer relies on the object being the one it validated.
2431
+ `dual-view / DirtyCred (ownership-exclusive): a function uses a file/cred/sk it looked up EARLIER, relying that no concurrent path swapped a same-type object underneath it. subject=the consumer; predicate="'file' is still the same object validated at lookup"; establisherToken=the lookup/get that pins it (fget/fdget/get_file/sock_hold); kind=ownership-exclusive; securityRelevance=lifetime.`,
2432
+ // A6/err-path double-put — a cleanup relies on the ref not already having been released.
2433
+ `err-path double-put (refcount-positive): a release relies on the ref being HELD at that point, but an error path already released it. subject=the releasing fn; predicate="'sk' still holds a ref here"; establisherToken=the get that must dominate the put (sock_hold/get_pid/kref_get); kind=refcount-positive; securityRelevance=lifetime.`,
2434
+ // A1/double-fetch — a use relies on a value not having changed since it was validated.
2435
+ `double-fetch / revalidate (revalidated): a function validates a length/index, then RE-READS the same user/shared value at use, relying it did not change. subject=the using fn; predicate="'len' at use equals the validated 'len'"; establisherToken=the bounds check/copy that must re-run (copy_from_user/ check_add_overflow/a *_ok validator); kind=revalidated; securityRelevance=bounds.`,
2436
+ // A2/called-once — an init/free relies on running exactly once per object.
2437
+ `called-once (called-once): an init/free/attach relies on running EXACTLY ONCE for an object, but a reachable re-entry runs it twice. subject=the once-fn; predicate="this runs once per object"; establisherToken=the guard/state flag that must gate re-entry (test_and_set_bit/a state== check/ a WAS_INIT flag); kind=called-once; securityRelevance=lifetime.`,
2438
+ // A11/A12 authz sibling / incomplete-fix — a handler relies on a cap check a sibling path skips.
2439
+ `authz sibling / incomplete-fix (state-precondition): a handler relies on a capability/permission having been checked by the dispatcher, but a sibling entry reaches it without the check. subject=the handler; predicate="caller verified CAP_NET_ADMIN in the target netns"; establisherToken=the check a caller must perform (ns_capable/capable/netlink_capable); kind=state-precondition; securityRelevance=authz.`,
2440
+ // A9' cross-api dual-view (THE high-value shape) — request/submit phase vs reply/completion/release phase.
2441
+ `cross-api dual-view / cross-phase (relied-on-cross-api): a REQUEST/SUBMIT/CREATE phase sets up a long-lived object (a fuse_req, an io_uring registered fd/creds, a dma_buf, a reused pipe_buffer) and a SECOND, userspace-/IRQ-/completion-driven phase (the REPLY/COMPLETION/RELEASE view) reaches the SAME object via a DISTINCT entry. subject=the phase that RELIES on stability; object='struct fuse_req' (name the STRUCT TYPE, it is the dual-view join); predicate="req fields validated at request time are not mutated by the reply path"; establisherToken=the flag/lock/state the setup phase sets that the second phase must honor (a *_INIT/*_LOCKED flag, req->state==, a held lock); kind=ownership-exclusive or state-precondition; provenance=relied-on-cross-api; securityRelevance=lifetime or type.`,
2442
+ // fd-install / SCM_RIGHTS dual-view — installer establishes a type/validity the consumer trusts.
2443
+ `fd-install dual-view (relied-on-cross-api): one API INSTALLS an fd/file into a receiver (SCM_RIGHTS, fd passing) whose type/validity the CONSUMER assumes was checked at install. subject=the receiver/consumer; object='struct file'; predicate="the installed file was type-checked to the expected f_op/proto_ops"; establisherToken=the validating check the install path must perform (an f_op== compare, a proto check, a security_ hook); kind=ownership-exclusive; provenance=relied-on-cross-api; securityRelevance=type.`
2444
+ ];
2445
+ var MINE_TOOL = {
2446
+ name: "emit_assumption_set",
2447
+ description: "Emit the set of implicit RELIED-ON preconditions in the subsystem. Do NOT hunt bugs. For each key function, state a precondition it DEPENDS ON but does NOT itself establish, and name the exact TOKEN (a call / lock / validator identifier) that a caller, a lock, or another API uses to establish it \u2014 that token's ABSENCE on a path is what a separate deterministic checker will hunt.",
2448
+ input_schema: {
2449
+ type: "object",
2450
+ properties: {
2451
+ assumptions: {
2452
+ type: "array",
2453
+ description: "One entry per relied-on precondition. Prefer preconditions on objects reachable from unprivileged syscalls.",
2454
+ items: {
2455
+ type: "object",
2456
+ properties: {
2457
+ kind: {
2458
+ type: "string",
2459
+ enum: [
2460
+ "non-null",
2461
+ "validated-range",
2462
+ "revalidated",
2463
+ "state-precondition",
2464
+ "called-once",
2465
+ "refcount-positive",
2466
+ "lock-held",
2467
+ "field-initialized",
2468
+ "ownership-exclusive",
2469
+ "size-consistent"
2470
+ ],
2471
+ description: "The precondition shape (see the archetypes in the system prompt)."
2472
+ },
2473
+ object: { type: "string", description: "The object/struct the precondition constrains, e.g. 'struct sock'." },
2474
+ subject: { type: "string", description: "The function that RELIES on the precondition (exact name as defined)." },
2475
+ predicate: { type: "string", description: `The precondition, e.g. "'other' still holds an sk ref here".` },
2476
+ location: { type: "string", description: "Where the reliance is, as file:line or function name." },
2477
+ provenance: {
2478
+ type: "string",
2479
+ enum: ["enforced-local", "relied-on-caller", "relied-on-subsystem", "relied-on-cross-api", "relied-on-concurrency"],
2480
+ description: "Where the precondition is supposed to be established. Use enforced-local ONLY if the subject itself establishes it."
2481
+ },
2482
+ establishedBy: { type: "string", description: "Who normally establishes it (a caller / lock / API name)." },
2483
+ oracle: {
2484
+ type: "object",
2485
+ properties: {
2486
+ mechanism: {
2487
+ type: "string",
2488
+ enum: ["establisher-absent-on-path", "state-not-established", "revalidation-absent", "establisher-absent-cross-api"]
2489
+ },
2490
+ target: { type: "string", description: "The object/expression checked, e.g. 'other' or 'skb->len'." },
2491
+ establisherToken: {
2492
+ type: "string",
2493
+ description: "The SINGLE call/lock/validator identifier that establishes the precondition, verbatim as it appears in code (e.g. 'sock_hold', 'mutex_lock', 'ns_capable'). The checker greps this literally \u2014 a prose phrase is useless."
2494
+ },
2495
+ establisherAliases: { type: "array", items: { type: "string" }, description: "Other tokens that also establish it." }
2496
+ },
2497
+ required: ["mechanism", "target", "establisherToken"]
2498
+ },
2499
+ securityRelevance: {
2500
+ type: "string",
2501
+ enum: ["lifetime", "bounds", "authz", "type", "other"],
2502
+ description: "Which security dimension the precondition protects."
2503
+ }
2504
+ },
2505
+ required: ["kind", "subject", "predicate", "provenance", "oracle", "securityRelevance"]
2506
+ }
2507
+ },
2508
+ notes: { type: "string", description: "Free-form provenance notes." }
2509
+ },
2510
+ required: ["assumptions"]
2511
+ }
2512
+ };
2513
+ var MINE_SYSTEM = "You are a world-class kernel auditor performing ASSUMPTION MINING. You are given a subsystem's source. Your job is NOT to find bugs \u2014 it is to make the code's IMPLICIT contract EXPLICIT: for each key function, state a precondition it RELIES ON but does NOT itself establish, and name the exact TOKEN a caller, a lock, or another API uses to establish that precondition. A separate deterministic checker will then hunt reachable contexts where that token is ABSENT.\n\nThink about the deepest bug classes and state the assumption whose violation WOULD be one \u2014 these archetypes are the shape to emit (not patterns to grep):\n - " + ASSUMPTION_ARCHETYPE_FEWSHOTS.join("\n - ") + "\n\nRULES:\n 1. establisherToken MUST be a real identifier that appears verbatim in the source (a function/macro call, a lock acquire, a validator). NEVER a prose phrase \u2014 the checker matches it literally.\n 2. Use provenance 'enforced-local' ONLY when the subject establishes the precondition itself; otherwise pick the relied-on-* class that says WHO establishes it (caller / subsystem / cross-api / concurrency).\n 3. Prefer preconditions on objects reachable from UNPRIVILEGED syscalls, and prefer lifetime / bounds / authz / type relevance (a race on a lifetime pointer, a skipped bounds/authz check) over cosmetic ones.\n 4. 'subject' must be an exact function name as defined in the source.\n 5. establisherAliases \u2014 CRITICAL for precision. When the precondition can be established through a FAMILY of tokens (sibling macros like CMSG_LEN/CMSG_SPACE/CMSG_ALIGN, a helper that WRAPS the raw primitive such as unix_table_double_lock() wrapping spin_lock(), or several equivalent validators), put the establisherToken as the one you consider primary and list EVERY other token that also establishes it in establisherAliases. The deterministic checker treats a caller that uses the token OR any alias (directly or through a wrapper) as having established the precondition \u2014 a missing alias/wrapper is the #1 false-positive source, so be generous here.\n 6. Prefer the WRAPPER identifier the surrounding code actually calls (e.g. unix_state_lock, unix_table_double_lock) as the establisherToken when one exists, and list the raw primitive it bottoms out in as an alias.\n 7. HIGHEST PRIORITY \u2014 DUAL-VIEW / CROSS-PHASE seams. The deepest LPEs (DirtyPipe, DirtyCred, fuse request-vs-reply, io_uring submit-vs-completion, SCM_RIGHTS install-vs-consume) are NOT single-caller preconditions \u2014 they are one LONG-LIVED OBJECT reached by TWO DISTINCT APIs/phases where the second phase does not re-establish what the first set up. When you see an object that OUTLIVES a single call and is reached from MULTIPLE entry points or lifecycle phases (create/open vs release; request/submit vs reply/completion; install vs consume), EMIT a relied-on-cross-api assumption for it: set provenance='relied-on-cross-api' (or 'relied-on-concurrency' for a race), name the STRUCT TYPE in 'object' verbatim (e.g. 'struct fuse_req' \u2014 the type name is the mechanical join the dual-view checker greps), and pick kind ownership-exclusive / state-precondition / called-once / field-initialized / revalidated. Prefer these cross-phase assumptions over plain caller-precondition ones. Emit via emit_assumption_set.";
2514
+ function readSource2(sourceRoot, file) {
2515
+ const path = resolveContainedSourcePath(sourceRoot, file);
2516
+ if (!path)
2517
+ return null;
2518
+ try {
2519
+ return readFileSync3(path, "utf8");
2520
+ } catch {
2521
+ return null;
2522
+ }
2523
+ }
2524
+ async function mineAssumptions(input) {
2525
+ const log = input.log ?? (() => {
2526
+ });
2527
+ if (!input.subsystemFiles || input.subsystemFiles.length === 0) {
2528
+ throw new Error("assumption mining needs at least one subsystemFile");
2529
+ }
2530
+ const sources = [];
2531
+ for (const file of input.subsystemFiles) {
2532
+ const text = readSource2(input.sourceRoot, file);
2533
+ if (text == null) {
2534
+ log(`[assumption-mine] could not read ${file}`);
2535
+ continue;
2536
+ }
2537
+ sources.push({ file, text });
2538
+ }
2539
+ if (sources.length === 0)
2540
+ throw new Error("assumption mining could not read any subsystemFile under sourceRoot");
2541
+ const out = await extractInvariantSpec({
2542
+ sources,
2543
+ system: MINE_SYSTEM,
2544
+ tool: MINE_TOOL,
2545
+ ...input.model ? { model: input.model } : {},
2546
+ ...input.maxCharsPerFile !== void 0 ? { maxCharsPerFile: input.maxCharsPerFile } : {},
2547
+ errorLabel: "assumption-mine"
2548
+ });
2549
+ const raw = Array.isArray(out?.assumptions) ? out.assumptions : [];
2550
+ const assumptions = normalizeAssumptions(raw);
2551
+ if (assumptions.length === 0)
2552
+ throw new Error("assumption mine emitted no well-formed assumptions");
2553
+ log(`[assumption-mine] mined ${assumptions.length} relied-on assumption(s) in ${input.subsystem}`);
2554
+ return {
2555
+ modelVersion: ASSUMPTION_MODEL_VERSION,
2556
+ subsystem: input.subsystem,
2557
+ subsystemFiles: sources.map((s) => s.file),
2558
+ assumptions,
2559
+ builtAt: (/* @__PURE__ */ new Date()).toISOString(),
2560
+ ...out?.notes ? { notes: out.notes } : {}
2561
+ };
2562
+ }
2563
+ function normalizeAssumptions(raw) {
2564
+ const perSubject = /* @__PURE__ */ new Map();
2565
+ const out = [];
2566
+ for (const a of raw) {
2567
+ if (!a || typeof a.subject !== "string" || !a.subject)
2568
+ continue;
2569
+ if (!a.oracle || typeof a.oracle.establisherToken !== "string" || !a.oracle.establisherToken)
2570
+ continue;
2571
+ if (typeof a.predicate !== "string" || !a.predicate)
2572
+ continue;
2573
+ const n = (perSubject.get(a.subject) ?? 0) + 1;
2574
+ perSubject.set(a.subject, n);
2575
+ out.push({
2576
+ id: `${a.subject}#${n}`,
2577
+ kind: a.kind,
2578
+ ...a.object ? { object: a.object } : {},
2579
+ subject: a.subject,
2580
+ predicate: a.predicate,
2581
+ location: typeof a.location === "string" ? a.location : a.subject,
2582
+ provenance: a.provenance,
2583
+ ...a.establishedBy ? { establishedBy: a.establishedBy } : {},
2584
+ oracle: {
2585
+ mechanism: a.oracle.mechanism,
2586
+ target: typeof a.oracle.target === "string" && a.oracle.target ? a.oracle.target : a.subject,
2587
+ establisherToken: a.oracle.establisherToken,
2588
+ ...Array.isArray(a.oracle.establisherAliases) ? { establisherAliases: a.oracle.establisherAliases } : {}
2589
+ },
2590
+ securityRelevance: a.securityRelevance
2591
+ });
2592
+ }
2593
+ return out;
2594
+ }
2595
+ function storeAssumptionModel(model, path) {
2596
+ mkdirSync3(dirname3(path), { recursive: true });
2597
+ writeFileSync3(path, JSON.stringify(model, null, 2) + "\n", "utf8");
2598
+ return path;
2599
+ }
2600
+ function loadAssumptionModel(path) {
2601
+ const model = JSON.parse(readFileSync3(path, "utf8"));
2602
+ if (model.modelVersion !== ASSUMPTION_MODEL_VERSION) {
2603
+ throw new Error(`stored assumption model is v${model.modelVersion}, checker is v${ASSUMPTION_MODEL_VERSION} \u2014 re-mine`);
2604
+ }
2605
+ if (!Array.isArray(model.assumptions))
2606
+ throw new Error("stored assumption model has no assumptions array");
2607
+ return model;
2608
+ }
2609
+ var KEEP_RELEVANCE = /* @__PURE__ */ new Set(["lifetime", "bounds", "authz", "type"]);
2610
+ var NON_MECHANIZABLE = /* @__PURE__ */ new Set([
2611
+ "n/a",
2612
+ "na",
2613
+ "none",
2614
+ "the",
2615
+ "a",
2616
+ "caller",
2617
+ "user",
2618
+ "userspace",
2619
+ "unknown",
2620
+ "somewhere",
2621
+ "before",
2622
+ "after",
2623
+ "valid",
2624
+ "check",
2625
+ "checked",
2626
+ "the_caller",
2627
+ "todo",
2628
+ "tbd"
2629
+ ]);
2630
+ function isMechanizableEstablisher(token) {
2631
+ const t = (token ?? "").trim();
2632
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(t))
2633
+ return false;
2634
+ if (t.length < 3)
2635
+ return false;
2636
+ if (NON_MECHANIZABLE.has(t.toLowerCase()))
2637
+ return false;
2638
+ return true;
2639
+ }
2640
+ function bodyCallsToken(fnBody, token) {
2641
+ const esc = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2642
+ return new RegExp(`\\b${esc}\\s*\\(`).test(fnBody);
2643
+ }
2644
+ function leadingIdent(expr) {
2645
+ const m = (expr ?? "").trim().match(/^([A-Za-z_]\w*)/);
2646
+ return m ? m[1] : "";
2647
+ }
2648
+ function trailingIdent(expr) {
2649
+ const m = (expr ?? "").trim().match(/([A-Za-z_]\w*)\s*$/);
2650
+ return m ? m[1] : "";
2651
+ }
2652
+ function guardConditions(body) {
2653
+ const out = [];
2654
+ const re = /\b(?:if|while)\s*\(/g;
2655
+ let m;
2656
+ while (m = re.exec(body)) {
2657
+ let i = m.index + m[0].length - 1;
2658
+ let depth = 0;
2659
+ const start = i + 1;
2660
+ for (; i < body.length; i++) {
2661
+ if (body[i] === "(")
2662
+ depth++;
2663
+ else if (body[i] === ")") {
2664
+ depth--;
2665
+ if (depth === 0)
2666
+ break;
2667
+ }
2668
+ }
2669
+ out.push(body.slice(start, i));
2670
+ re.lastIndex = i + 1;
2671
+ }
2672
+ return out;
2673
+ }
2674
+ function callArgsOf(body, name) {
2675
+ const re = new RegExp(`\\b${escapeReStr(name)}\\s*\\(`);
2676
+ const m = re.exec(body);
2677
+ if (!m)
2678
+ return null;
2679
+ let i = m.index + m[0].length - 1;
2680
+ let depth = 0;
2681
+ const start = i + 1;
2682
+ for (; i < body.length; i++) {
2683
+ if (body[i] === "(")
2684
+ depth++;
2685
+ else if (body[i] === ")") {
2686
+ depth--;
2687
+ if (depth === 0)
2688
+ break;
2689
+ }
2690
+ }
2691
+ return body.slice(start, i);
2692
+ }
2693
+ function escapeReStr(s) {
2694
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2695
+ }
2696
+ var SELF_ASSERT_FAMILY = ["lockdep_assert_held", "assert_spin_locked", "WARN_ON_ONCE", "WARN_ON", "BUG_ON", "VM_BUG_ON_PAGE", "VM_BUG_ON"];
2697
+ function subjectSelfEnforces(body, a) {
2698
+ const tokens = [a.oracle.establisherToken, ...a.oracle.establisherAliases ?? []].filter(isMechanizableEstablisher);
2699
+ const target = a.oracle.target || a.object || "";
2700
+ const tvar = leadingIdent(target);
2701
+ const tfield = trailingIdent(target);
2702
+ const refs = [tvar, tfield, ...tokens].filter((r) => r && r.length >= 2);
2703
+ const wordIn = (hay, w) => new RegExp(`\\b${escapeReStr(w)}\\b`).test(hay);
2704
+ if (a.kind === "lock-held" && /\b(?:lockdep_assert_held|\w*_assert_held|assert_spin_locked)\s*\(/.test(body)) {
2705
+ return "subject asserts its lock is caller-held (lockdep_assert_held/*_assert_held) \u2014 self-enforcing (external contract asserted, not a silent violator)";
2706
+ }
2707
+ for (const fam of SELF_ASSERT_FAMILY) {
2708
+ const arg = callArgsOf(body, fam);
2709
+ if (arg && refs.some((r) => wordIn(arg, r))) {
2710
+ return `subject guards its contract with ${fam}(\u2026${refs.find((r) => wordIn(arg, r))}\u2026) \u2014 self-enforcing`;
2711
+ }
2712
+ }
2713
+ for (const cond of guardConditions(body)) {
2714
+ const tokHit = tokens.find((t) => wordIn(cond, t));
2715
+ if (tokHit)
2716
+ return `subject tests establisher '${tokHit}' in an if/while guard \u2014 self-enforcing`;
2717
+ for (const v of [tvar, tfield].filter((x) => x && x.length >= 2)) {
2718
+ if (new RegExp(`\\b${escapeReStr(v)}\\b[^;{}]*&\\s*[A-Z_][A-Z0-9_]{2,}`).test(cond)) {
2719
+ return `subject flag-tests '${v}' against a FLAG in a guard \u2014 self-enforcing`;
2720
+ }
2721
+ }
2722
+ }
2723
+ return null;
2724
+ }
2725
+ function crossCheckAssumptions(model, bodies) {
2726
+ const kept = [];
2727
+ const dropped = [];
2728
+ const reclassified = [];
2729
+ for (const a of model.assumptions) {
2730
+ if (!isMechanizableEstablisher(a.oracle.establisherToken)) {
2731
+ dropped.push({ assumption: a, reason: `non-mechanizable establisher token "${a.oracle.establisherToken}" (prose-only)` });
2732
+ continue;
2733
+ }
2734
+ const sbody = bodies.get(a.subject);
2735
+ if (sbody != null) {
2736
+ const selfReason = subjectSelfEnforces(sbody, a);
2737
+ if (selfReason) {
2738
+ dropped.push({ assumption: a, reason: selfReason });
2739
+ continue;
2740
+ }
2741
+ }
2742
+ let effective = a;
2743
+ if (a.provenance === "enforced-local") {
2744
+ const body = bodies.get(a.subject);
2745
+ if (body == null) {
2746
+ dropped.push({ assumption: a, reason: `enforced-local but ${a.subject}() body unreadable \u2014 cannot confirm a violator (safe drop)` });
2747
+ continue;
2748
+ }
2749
+ const establishes = bodyCallsToken(body, a.oracle.establisherToken) || (a.oracle.establisherAliases ?? []).some((alias) => isMechanizableEstablisher(alias) && bodyCallsToken(body, alias));
2750
+ if (establishes) {
2751
+ dropped.push({ assumption: a, reason: `enforced-local verified \u2014 ${a.subject}() establishes ${a.oracle.establisherToken} itself (no external violator)` });
2752
+ continue;
2753
+ }
2754
+ effective = { ...a, provenance: "relied-on-caller" };
2755
+ reclassified.push(effective);
2756
+ }
2757
+ if (!KEEP_RELEVANCE.has(effective.securityRelevance)) {
2758
+ dropped.push({ assumption: a, reason: `securityRelevance '${effective.securityRelevance}' not in {lifetime,bounds,authz,type}` });
2759
+ continue;
2760
+ }
2761
+ kept.push(effective);
2762
+ }
2763
+ return { kept, dropped, reclassified };
2764
+ }
2765
+ function buildFunctionBodyIndex(sources) {
2766
+ const out = /* @__PURE__ */ new Map();
2767
+ for (const { text } of sources) {
2768
+ for (const fn of splitTopLevelFunctions(text)) {
2769
+ if (!out.has(fn.name))
2770
+ out.set(fn.name, fn.text);
2771
+ }
2772
+ }
2773
+ return out;
2774
+ }
2775
+ function splitTopLevelFunctions(src) {
2776
+ const blanked = blankCommentsAndStrings2(src);
2777
+ const out = [];
2778
+ const n = blanked.length;
2779
+ let depth = 0;
2780
+ const CONTROL = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "do", "else", "return", "sizeof", "typeof", "__attribute__", "case", "default", "goto", "asm", "__asm__", "static_assert"]);
2781
+ for (let i = 0; i < n; i++) {
2782
+ const ch = blanked[i];
2783
+ if (ch === "{") {
2784
+ if (depth === 0) {
2785
+ let j = i - 1;
2786
+ while (j >= 0 && /\s/.test(blanked[j]))
2787
+ j--;
2788
+ if (j >= 0 && blanked[j] === ")") {
2789
+ let pd = 0;
2790
+ let k = j;
2791
+ for (; k >= 0; k--) {
2792
+ if (blanked[k] === ")")
2793
+ pd++;
2794
+ else if (blanked[k] === "(") {
2795
+ pd--;
2796
+ if (pd === 0)
2797
+ break;
2798
+ }
2799
+ }
2800
+ if (k >= 0) {
2801
+ let m = k - 1;
2802
+ while (m >= 0 && /\s/.test(blanked[m]))
2803
+ m--;
2804
+ let e = m;
2805
+ while (e >= 0 && /[A-Za-z0-9_]/.test(blanked[e]))
2806
+ e--;
2807
+ const name = blanked.slice(e + 1, m + 1).trim();
2808
+ if (name && /^[A-Za-z_][A-Za-z0-9_]*$/.test(name) && !CONTROL.has(name)) {
2809
+ let bd = 0;
2810
+ let end = i;
2811
+ for (; end < n; end++) {
2812
+ if (blanked[end] === "{")
2813
+ bd++;
2814
+ else if (blanked[end] === "}") {
2815
+ bd--;
2816
+ if (bd === 0)
2817
+ break;
2818
+ }
2819
+ }
2820
+ let sigStart = e + 1;
2821
+ while (sigStart > 0 && blanked[sigStart - 1] !== "\n")
2822
+ sigStart--;
2823
+ out.push({ name, text: src.slice(sigStart, Math.min(n, end + 1)) });
2824
+ i = end;
2825
+ depth = 0;
2826
+ continue;
2827
+ }
2828
+ }
2829
+ }
2830
+ }
2831
+ depth++;
2832
+ } else if (ch === "}") {
2833
+ if (depth > 0)
2834
+ depth--;
2835
+ }
2836
+ }
2837
+ return out;
2838
+ }
2839
+ function blankCommentsAndStrings2(src) {
2840
+ const out = src.split("");
2841
+ let i = 0;
2842
+ const n = src.length;
2843
+ const blank = (a, b) => {
2844
+ for (let k = a; k < b && k < n; k++)
2845
+ if (out[k] !== "\n")
2846
+ out[k] = " ";
2847
+ };
2848
+ while (i < n) {
2849
+ const c = src[i];
2850
+ const c2 = src[i + 1];
2851
+ if (c === "/" && c2 === "/") {
2852
+ let j = i + 2;
2853
+ while (j < n && src[j] !== "\n")
2854
+ j++;
2855
+ blank(i, j);
2856
+ i = j;
2857
+ } else if (c === "/" && c2 === "*") {
2858
+ let j = i + 2;
2859
+ while (j < n && !(src[j] === "*" && src[j + 1] === "/"))
2860
+ j++;
2861
+ j = Math.min(n, j + 2);
2862
+ blank(i, j);
2863
+ i = j;
2864
+ } else if (c === '"' || c === "'") {
2865
+ const q = c;
2866
+ let j = i + 1;
2867
+ while (j < n) {
2868
+ if (src[j] === "\\") {
2869
+ j += 2;
2870
+ continue;
2871
+ }
2872
+ if (src[j] === q) {
2873
+ j++;
2874
+ break;
2875
+ }
2876
+ if (src[j] === "\n")
2877
+ break;
2878
+ j++;
2879
+ }
2880
+ blank(i, j);
2881
+ i = j;
2882
+ } else
2883
+ i++;
2884
+ }
2885
+ return out.join("");
2886
+ }
2887
+ var UNPRIV_ENTRY_RE = /(^__?sys_|^__do_sys_|_ioctl$|_recvmsg$|_sendmsg$|_recvmmsg$|_sendmmsg$|_setsockopt$|_getsockopt$|_read$|_write$|_poll$|_mmap$|_release$|_connect$|_bind$|_listen$|_accept)/;
2888
+ var escapeRe3 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2889
+ function computeEstablisherWrappers(cg, establisherTokens, maxRounds = 4) {
2890
+ const defined = new Set(cg.fns.map((f) => f.name));
2891
+ const establishes = /* @__PURE__ */ new Map();
2892
+ for (const fn of cg.fns) {
2893
+ const s = /* @__PURE__ */ new Set();
2894
+ for (const call of fn.calls)
2895
+ if (establisherTokens.has(call.callee))
2896
+ s.add(call.callee);
2897
+ establishes.set(fn.name, s);
2898
+ }
2899
+ for (let round = 0; round < Math.max(1, maxRounds); round++) {
2900
+ let changed = false;
2901
+ for (const fn of cg.fns) {
2902
+ const cur = establishes.get(fn.name);
2903
+ for (const call of fn.calls) {
2904
+ if (!defined.has(call.callee))
2905
+ continue;
2906
+ const via = establishes.get(call.callee);
2907
+ if (!via)
2908
+ continue;
2909
+ for (const t of via)
2910
+ if (!cur.has(t)) {
2911
+ cur.add(t);
2912
+ changed = true;
2913
+ }
2914
+ }
2915
+ }
2916
+ if (!changed)
2917
+ break;
2918
+ }
2919
+ return establishes;
2920
+ }
2921
+ function computeEstablisherSets(cg, establisherTokens, exported, maxRounds = 8, wrappers) {
2922
+ const local = /* @__PURE__ */ new Map();
2923
+ for (const fn of cg.fns) {
2924
+ const s = wrappers?.get(fn.name) ? new Set(wrappers.get(fn.name)) : /* @__PURE__ */ new Set();
2925
+ for (const call of fn.calls)
2926
+ if (establisherTokens.has(call.callee))
2927
+ s.add(call.callee);
2928
+ local.set(fn.name, s);
2929
+ }
2930
+ const callersOf = /* @__PURE__ */ new Map();
2931
+ const defined = new Set(cg.fns.map((f) => f.name));
2932
+ for (const fn of cg.fns) {
2933
+ for (const call of fn.calls) {
2934
+ if (!defined.has(call.callee))
2935
+ continue;
2936
+ const set = callersOf.get(call.callee) ?? /* @__PURE__ */ new Set();
2937
+ set.add(fn.name);
2938
+ callersOf.set(call.callee, set);
2939
+ }
2940
+ }
2941
+ const est = /* @__PURE__ */ new Map();
2942
+ for (const fn of cg.fns)
2943
+ est.set(fn.name, new Set(local.get(fn.name)));
2944
+ for (let round = 0; round < Math.max(1, maxRounds); round++) {
2945
+ let changed = false;
2946
+ for (const fn of cg.fns) {
2947
+ const callers = callersOf.get(fn.name);
2948
+ if (!callers || callers.size === 0 || exported.has(fn.name))
2949
+ continue;
2950
+ let inter = null;
2951
+ for (const c of callers) {
2952
+ const cs = est.get(c) ?? /* @__PURE__ */ new Set();
2953
+ if (inter === null)
2954
+ inter = new Set(cs);
2955
+ else
2956
+ for (const t of [...inter])
2957
+ if (!cs.has(t))
2958
+ inter.delete(t);
2959
+ if (inter.size === 0)
2960
+ break;
2961
+ }
2962
+ if (!inter || inter.size === 0)
2963
+ continue;
2964
+ const cur = est.get(fn.name);
2965
+ for (const t of inter) {
2966
+ if (!cur.has(t)) {
2967
+ cur.add(t);
2968
+ changed = true;
2969
+ }
2970
+ }
2971
+ }
2972
+ if (!changed)
2973
+ break;
2974
+ }
2975
+ return est;
2976
+ }
2977
+ function collectExportedSymbols(sources) {
2978
+ const out = /* @__PURE__ */ new Set();
2979
+ const re = /EXPORT_SYMBOL(?:_GPL|_NS|_NS_GPL)?\s*\(\s*([A-Za-z_]\w*)/g;
2980
+ for (const { text } of sources) {
2981
+ let m;
2982
+ while (m = re.exec(text))
2983
+ out.add(m[1]);
2984
+ }
2985
+ return out;
2986
+ }
2987
+ function callLineInBody(callerBody, callerStartLine, callee) {
2988
+ const re = new RegExp(`\\b${escapeRe3(callee)}\\s*\\(`);
2989
+ const m = re.exec(callerBody);
2990
+ if (!m)
2991
+ return callerStartLine;
2992
+ let nl = 0;
2993
+ for (let k = 0; k < m.index; k++)
2994
+ if (callerBody[k] === "\n")
2995
+ nl++;
2996
+ return callerStartLine + nl;
2997
+ }
2998
+ function scanViolatingContexts(kept, cg, sources, bodies, opts = {}) {
2999
+ const log = opts.log ?? (() => {
3000
+ });
3001
+ const maxContexts = opts.maxContexts ?? 60;
3002
+ const establisherTokens = /* @__PURE__ */ new Set();
3003
+ for (const a of kept) {
3004
+ establisherTokens.add(a.oracle.establisherToken);
3005
+ for (const alias of a.oracle.establisherAliases ?? [])
3006
+ if (isMechanizableEstablisher(alias))
3007
+ establisherTokens.add(alias);
3008
+ }
3009
+ const exported = collectExportedSymbols(sources);
3010
+ const wrappers = opts.resolveWrappers === false ? void 0 : computeEstablisherWrappers(cg, establisherTokens, opts.wrapperRounds ?? 4);
3011
+ const est = computeEstablisherSets(cg, establisherTokens, exported, opts.maxPropagationRounds ?? 8, wrappers);
3012
+ const callersOf = /* @__PURE__ */ new Map();
3013
+ const defined = new Set(cg.fns.map((f) => f.name));
3014
+ for (const fn of cg.fns) {
3015
+ const seen = /* @__PURE__ */ new Set();
3016
+ for (const call of fn.calls) {
3017
+ if (!defined.has(call.callee) || seen.has(call.callee))
3018
+ continue;
3019
+ seen.add(call.callee);
3020
+ const arr = callersOf.get(call.callee) ?? [];
3021
+ arr.push(fn);
3022
+ callersOf.set(call.callee, arr);
3023
+ }
3024
+ }
3025
+ const contexts = [];
3026
+ for (const a of kept) {
3027
+ const subject = a.subject;
3028
+ const token = a.oracle.establisherToken;
3029
+ const aliases = (a.oracle.establisherAliases ?? []).filter(isMechanizableEstablisher);
3030
+ const establishesToken = (fnName2) => {
3031
+ const set = est.get(fnName2);
3032
+ if (!set)
3033
+ return false;
3034
+ if (set.has(token))
3035
+ return true;
3036
+ return aliases.some((al) => set.has(al));
3037
+ };
3038
+ const callers = callersOf.get(subject);
3039
+ if (!callers || callers.length === 0)
3040
+ continue;
3041
+ for (const c of callers) {
3042
+ if (c.name === subject)
3043
+ continue;
3044
+ if (establishesToken(c.name))
3045
+ continue;
3046
+ const body = bodies.get(c.name) ?? "";
3047
+ const line = callLineInBody(body, c.startLine, subject);
3048
+ const unpriv = UNPRIV_ENTRY_RE.test(c.name) || !callersOf.get(c.name)?.length;
3049
+ contexts.push({
3050
+ assumptionId: a.id,
3051
+ subject,
3052
+ caller: c.name,
3053
+ callerFile: c.file,
3054
+ callLine: line,
3055
+ establisherToken: token,
3056
+ unprivEntry: unpriv,
3057
+ detail: `${c.name}() calls ${subject}() at ${c.file}:${line} but never establishes ${token}${aliases.length ? ` (nor ${aliases.join("/")})` : ""}, and does not inherit it from its callers. ${subject}() RELIES ON: ${a.predicate} (${a.kind}, ${a.securityRelevance}). If this path is attacker-reachable and the precondition is genuinely required, the assumption is VIOLATED here. CANDIDATE to DISPROVE \u2014 coarse establisher-anywhere + name-based call graph; confirm ${token} is truly absent on THIS path.`
3058
+ });
3059
+ }
3060
+ }
3061
+ contexts.sort((x, y) => Number(y.unprivEntry) - Number(x.unprivEntry) || x.subject.localeCompare(y.subject) || x.caller.localeCompare(y.caller));
3062
+ const capped = contexts.slice(0, maxContexts);
3063
+ log(`[assumption-scan] ${contexts.length} violating context(s)${capped.length < contexts.length ? ` (capped to ${capped.length})` : ""}`);
3064
+ return capped;
3065
+ }
3066
+ var DEFAULT_CROSS_API_KINDS = /* @__PURE__ */ new Set([
3067
+ "ownership-exclusive",
3068
+ "called-once",
3069
+ "field-initialized",
3070
+ "revalidated",
3071
+ "state-precondition"
3072
+ ]);
3073
+ var UBIQUITOUS_OBJECT_TYPES = /* @__PURE__ */ new Set([
3074
+ "file",
3075
+ "inode",
3076
+ "dentry",
3077
+ "page",
3078
+ "folio",
3079
+ "sk_buff",
3080
+ "sock",
3081
+ "task_struct",
3082
+ "mm_struct",
3083
+ "vm_area_struct",
3084
+ "kiocb",
3085
+ "address_space",
3086
+ "super_block",
3087
+ "vfsmount",
3088
+ "path",
3089
+ "kobject",
3090
+ "device",
3091
+ "list_head",
3092
+ "work_struct",
3093
+ "timer_list",
3094
+ "pid",
3095
+ "cred",
3096
+ "seq_file",
3097
+ "kmem_cache",
3098
+ "nameidata",
3099
+ "kstat",
3100
+ "iov_iter",
3101
+ "bio",
3102
+ "request"
3103
+ ]);
3104
+ function isCrossApiAssumption(a, kinds = DEFAULT_CROSS_API_KINDS) {
3105
+ return a.provenance === "relied-on-cross-api" || a.provenance === "relied-on-concurrency" || kinds.has(a.kind);
3106
+ }
3107
+ function objectTypeToken(a) {
3108
+ const raw = (a.object ?? a.oracle.target ?? "").trim();
3109
+ const sm = raw.match(/struct\s+([A-Za-z_]\w*)/);
3110
+ if (sm)
3111
+ return sm[1];
3112
+ const bm = raw.match(/^([A-Za-z_]\w*)$/);
3113
+ if (bm && bm[1].length >= 4)
3114
+ return bm[1];
3115
+ return null;
3116
+ }
3117
+ function buildAdjacency(cg) {
3118
+ const defined = new Set(cg.fns.map((f) => f.name));
3119
+ const adj = /* @__PURE__ */ new Map();
3120
+ for (const fn of cg.fns) {
3121
+ const s = adj.get(fn.name) ?? /* @__PURE__ */ new Set();
3122
+ for (const call of fn.calls)
3123
+ if (defined.has(call.callee))
3124
+ s.add(call.callee);
3125
+ adj.set(fn.name, s);
3126
+ }
3127
+ return adj;
3128
+ }
3129
+ function reachesFn(adj, from, to, maxDepth = 12) {
3130
+ if (from === to)
3131
+ return true;
3132
+ const seen = /* @__PURE__ */ new Set([from]);
3133
+ let frontier = [from];
3134
+ let depth = 0;
3135
+ while (frontier.length && depth++ < maxDepth) {
3136
+ const next = [];
3137
+ for (const f of frontier) {
3138
+ for (const c of adj.get(f) ?? []) {
3139
+ if (c === to)
3140
+ return true;
3141
+ if (!seen.has(c)) {
3142
+ seen.add(c);
3143
+ next.push(c);
3144
+ }
3145
+ }
3146
+ }
3147
+ frontier = next;
3148
+ }
3149
+ return false;
3150
+ }
3151
+ function lockCallArgRanges(blanked) {
3152
+ const ranges = [];
3153
+ const push = (openParen) => {
3154
+ let depth = 0;
3155
+ for (let i = openParen; i < blanked.length; i++) {
3156
+ if (blanked[i] === "(")
3157
+ depth++;
3158
+ else if (blanked[i] === ")") {
3159
+ depth--;
3160
+ if (depth === 0) {
3161
+ ranges.push([openParen, i]);
3162
+ return;
3163
+ }
3164
+ }
3165
+ }
3166
+ };
3167
+ LOCK_CALL_RE.lastIndex = 0;
3168
+ let m;
3169
+ while (m = LOCK_CALL_RE.exec(blanked))
3170
+ push(m.index + m[0].length - 1);
3171
+ const guardRe = /\b(?:guard|scoped_guard)\s*\(/g;
3172
+ let g;
3173
+ while (g = guardRe.exec(blanked))
3174
+ push(g.index + g[0].length - 1);
3175
+ return ranges;
3176
+ }
3177
+ function objectFieldAccesses(body, typeToken) {
3178
+ const out = /* @__PURE__ */ new Map();
3179
+ if (!body || !typeToken)
3180
+ return out;
3181
+ const blanked = blankCommentsAndStrings2(body);
3182
+ const lockRanges = lockCallArgRanges(blanked);
3183
+ const insideLockArg = (idx) => lockRanges.some(([s, e]) => idx > s && idx < e);
3184
+ const vars = /* @__PURE__ */ new Set();
3185
+ const declRe = new RegExp(`(?:struct\\s+)?\\b${escapeRe3(typeToken)}\\b\\s*\\**\\s*([A-Za-z_]\\w*)`, "g");
3186
+ let dm;
3187
+ while (dm = declRe.exec(blanked)) {
3188
+ const name = dm[1];
3189
+ if (name && name !== "struct")
3190
+ vars.add(name);
3191
+ }
3192
+ if (vars.size === 0)
3193
+ return out;
3194
+ for (const v of vars) {
3195
+ const accRe = new RegExp(`\\b${escapeRe3(v)}\\s*(?:->|\\.)\\s*([A-Za-z_]\\w*)`, "g");
3196
+ let am;
3197
+ while (am = accRe.exec(blanked)) {
3198
+ if (insideLockArg(am.index))
3199
+ continue;
3200
+ const field = am[1];
3201
+ const arr = out.get(field) ?? [];
3202
+ arr.push(am.index);
3203
+ out.set(field, arr);
3204
+ }
3205
+ }
3206
+ return out;
3207
+ }
3208
+ var LOCK_CALL_RE = /\b(lock_sock_nested|lock_sock|bh_lock_sock|bh_unlock_sock|release_sock|mutex_lock_interruptible|mutex_lock_nested|mutex_lock_killable|mutex_lock|mutex_unlock|spin_lock_irqsave|spin_lock_irq|spin_lock_bh|spin_lock|spin_unlock_irqrestore|spin_unlock_irq|spin_unlock_bh|spin_unlock|read_lock_bh|read_lock|read_unlock_bh|read_unlock|write_lock_bh|write_lock|write_unlock_bh|write_unlock|rcu_read_lock|rcu_read_unlock)\s*\(/g;
3209
+ function firstCallArgNorm(body, openParen) {
3210
+ let depth = 0;
3211
+ let i = openParen;
3212
+ const start = openParen + 1;
3213
+ let end = start;
3214
+ for (; i < body.length; i++) {
3215
+ const c = body[i];
3216
+ if (c === "(")
3217
+ depth++;
3218
+ else if (c === ")") {
3219
+ depth--;
3220
+ if (depth === 0) {
3221
+ end = i;
3222
+ break;
3223
+ }
3224
+ } else if (c === "," && depth === 1) {
3225
+ end = i;
3226
+ break;
3227
+ }
3228
+ }
3229
+ return body.slice(start, end).replace(/\s+/g, "").replace(/^&/, "");
3230
+ }
3231
+ function lockEvents(body) {
3232
+ const blanked = blankCommentsAndStrings2(body);
3233
+ const events = [];
3234
+ LOCK_CALL_RE.lastIndex = 0;
3235
+ let m;
3236
+ while (m = LOCK_CALL_RE.exec(blanked)) {
3237
+ const name = m[1];
3238
+ const openParen = m.index + m[0].length - 1;
3239
+ const arg = firstCallArgNorm(blanked, openParen);
3240
+ let id;
3241
+ let acquire;
3242
+ if (name === "lock_sock" || name === "lock_sock_nested" || name === "bh_lock_sock") {
3243
+ id = "sock";
3244
+ acquire = true;
3245
+ } else if (name === "release_sock" || name === "bh_unlock_sock") {
3246
+ id = "sock";
3247
+ acquire = false;
3248
+ } else if (name === "rcu_read_lock") {
3249
+ id = "rcu";
3250
+ acquire = true;
3251
+ } else if (name === "rcu_read_unlock") {
3252
+ id = "rcu";
3253
+ acquire = false;
3254
+ } else if (name.startsWith("mutex_lock")) {
3255
+ id = `mutex:${arg}`;
3256
+ acquire = true;
3257
+ } else if (name === "mutex_unlock") {
3258
+ id = `mutex:${arg}`;
3259
+ acquire = false;
3260
+ } else if (name.startsWith("spin_lock")) {
3261
+ id = `spin:${arg}`;
3262
+ acquire = true;
3263
+ } else if (name.startsWith("spin_unlock")) {
3264
+ id = `spin:${arg}`;
3265
+ acquire = false;
3266
+ } else if (name.startsWith("read_lock") || name.startsWith("write_lock")) {
3267
+ id = `rw:${arg}`;
3268
+ acquire = true;
3269
+ } else if (name.startsWith("read_unlock") || name.startsWith("write_unlock")) {
3270
+ id = `rw:${arg}`;
3271
+ acquire = false;
3272
+ } else
3273
+ continue;
3274
+ events.push({ index: m.index, acquire, id });
3275
+ }
3276
+ const guardRe = /\b(?:guard|scoped_guard)\s*\(\s*\w+\s*(?:\)\s*\(|,)\s*([^),]+)/g;
3277
+ let g;
3278
+ while (g = guardRe.exec(blanked)) {
3279
+ const arg = g[1].replace(/\s+/g, "").replace(/^&/, "");
3280
+ events.push({ index: g.index, acquire: true, id: `guard:${arg}` });
3281
+ }
3282
+ events.sort((a, b) => a.index - b.index);
3283
+ return events;
3284
+ }
3285
+ function heldLockIdsAt(events, at) {
3286
+ const counts = /* @__PURE__ */ new Map();
3287
+ for (const e of events) {
3288
+ if (e.index >= at)
3289
+ break;
3290
+ const c = counts.get(e.id) ?? 0;
3291
+ counts.set(e.id, e.acquire ? c + 1 : Math.max(0, c - 1));
3292
+ }
3293
+ const held = /* @__PURE__ */ new Set();
3294
+ for (const [id, c] of counts)
3295
+ if (c > 0)
3296
+ held.add(id);
3297
+ return held;
3298
+ }
3299
+ function fieldCoveringLocks(events, offsets) {
3300
+ let inter = null;
3301
+ for (const off of offsets) {
3302
+ const held = heldLockIdsAt(events, off);
3303
+ if (inter === null)
3304
+ inter = new Set(held);
3305
+ else
3306
+ for (const id of [...inter])
3307
+ if (!held.has(id))
3308
+ inter.delete(id);
3309
+ if (inter.size === 0)
3310
+ break;
3311
+ }
3312
+ return inter ?? /* @__PURE__ */ new Set();
3313
+ }
3314
+ function dualViewSeamDecision(bodyA, bodyB, typeToken) {
3315
+ const fieldsA = objectFieldAccesses(bodyA, typeToken);
3316
+ const fieldsB = objectFieldAccesses(bodyB, typeToken);
3317
+ if (fieldsA.size === 0 || fieldsB.size === 0) {
3318
+ return { keep: true, reason: "field info unavailable on a phase \u2014 degraded to object-granular (kept)" };
3319
+ }
3320
+ const shared = [...fieldsA.keys()].filter((f) => fieldsB.has(f));
3321
+ if (shared.length === 0) {
3322
+ return { keep: false, reason: `disjoint fields (A:{${[...fieldsA.keys()].join(",")}} vs B:{${[...fieldsB.keys()].join(",")}}) \u2014 not a same-field seam` };
3323
+ }
3324
+ const eventsA = lockEvents(bodyA);
3325
+ const eventsB = lockEvents(bodyB);
3326
+ for (const f of shared) {
3327
+ const covA = fieldCoveringLocks(eventsA, fieldsA.get(f));
3328
+ const covB = fieldCoveringLocks(eventsB, fieldsB.get(f));
3329
+ const common = [...covA].filter((id) => covB.has(id));
3330
+ if (common.length === 0) {
3331
+ const lockNote = covA.size === 0 || covB.size === 0 ? `field '${f}' is accessed LOCKLESS in at least one phase` : `field '${f}' is held under DIFFERENT locks ({${[...covA].join(",")}} vs {${[...covB].join(",")}})`;
3332
+ return { keep: true, sharedField: f, reason: `same-field seam on '${f}' \u2014 ${lockNote}` };
3333
+ }
3334
+ }
3335
+ return {
3336
+ keep: false,
3337
+ reason: `all shared field(s) {${shared.join(",")}} are covered by a common lock in BOTH phases \u2014 serialized, not a seam`
3338
+ };
3339
+ }
3340
+ var RELEVANCE_RANK = {
3341
+ lifetime: 5,
3342
+ type: 4,
3343
+ bounds: 3,
3344
+ authz: 2,
3345
+ other: 1
3346
+ };
3347
+ var KIND_RANK = {
3348
+ "ownership-exclusive": 3,
3349
+ "refcount-positive": 3,
3350
+ "called-once": 3,
3351
+ "state-precondition": 3,
3352
+ revalidated: 2,
3353
+ "field-initialized": 2,
3354
+ "size-consistent": 2,
3355
+ "lock-held": 2,
3356
+ "validated-range": 1,
3357
+ "non-null": 1
3358
+ };
3359
+ var GEOMETRY_BONUS_CAP = 90;
3360
+ function assumptionGeometryBonus(a) {
3361
+ const object = (a.object ?? a.oracle.target ?? "").trim();
3362
+ if (!object)
3363
+ return 0;
3364
+ const synthetic = {
3365
+ title: object,
3366
+ description: `${a.predicate} ${a.kind} ${a.securityRelevance}`,
3367
+ evidence: { analysis: `${object} ${a.oracle.target ?? ""}` },
3368
+ category: ""
3369
+ };
3370
+ const g = scoreGeometry(synthetic);
3371
+ return g.geometryScore > 0 ? Math.min(g.geometryScore, GEOMETRY_BONUS_CAP) : 0;
3372
+ }
3373
+ function scoreDualViewWeaponizability(a) {
3374
+ const rel = RELEVANCE_RANK[a.securityRelevance] ?? 0;
3375
+ const kind = KIND_RANK[a.kind] ?? 0;
3376
+ const geom = assumptionGeometryBonus(a);
3377
+ const score = rel * 1e3 + kind * 100 + geom;
3378
+ const rationale = [
3379
+ `securityRelevance=${a.securityRelevance} (rank ${rel}/5)`,
3380
+ `kind=${a.kind} (rank ${kind}/3)`,
3381
+ ...geom > 0 ? [`+${geom} object-on-weaponizable-slab geometry ('${a.object ?? a.oracle.target}')`] : []
3382
+ ];
3383
+ return { score, rationale };
3384
+ }
3385
+ function scanDualViewContexts(kept, cg, sources, bodies, opts = {}) {
3386
+ const log = opts.log ?? (() => {
3387
+ });
3388
+ const maxContexts = opts.maxContexts ?? 60;
3389
+ const kinds = opts.crossApiKinds ?? DEFAULT_CROSS_API_KINDS;
3390
+ const reachDepth = opts.reachDepth ?? 12;
3391
+ const maxTouchers = opts.maxTouchers ?? Infinity;
3392
+ const maxPairsPerObject = opts.maxPairsPerObject ?? 6;
3393
+ const denylist = opts.objectDenylist ?? UBIQUITOUS_OBJECT_TYPES;
3394
+ const fieldGranular = opts.fieldGranular !== false;
3395
+ const crossApi = kept.filter((a) => isCrossApiAssumption(a, kinds) && objectTypeToken(a));
3396
+ if (crossApi.length === 0) {
3397
+ log("[dual-view] no cross-api/cross-phase assumptions with a typed object to enumerate");
3398
+ return [];
3399
+ }
3400
+ const establisherTokens = /* @__PURE__ */ new Set();
3401
+ for (const a of crossApi) {
3402
+ establisherTokens.add(a.oracle.establisherToken);
3403
+ for (const alias of a.oracle.establisherAliases ?? [])
3404
+ if (isMechanizableEstablisher(alias))
3405
+ establisherTokens.add(alias);
3406
+ }
3407
+ const exported = collectExportedSymbols(sources);
3408
+ const wrappers = opts.resolveWrappers === false ? void 0 : computeEstablisherWrappers(cg, establisherTokens, opts.wrapperRounds ?? 4);
3409
+ const est = computeEstablisherSets(cg, establisherTokens, exported, opts.maxPropagationRounds ?? 8, wrappers);
3410
+ const adj = buildAdjacency(cg);
3411
+ const defined = new Set(cg.fns.map((f) => f.name));
3412
+ const hasInternalCaller = /* @__PURE__ */ new Set();
3413
+ for (const fn of cg.fns)
3414
+ for (const call of fn.calls)
3415
+ if (defined.has(call.callee))
3416
+ hasInternalCaller.add(call.callee);
3417
+ const isEntry = (n) => UNPRIV_ENTRY_RE.test(n) || exported.has(n) || !hasInternalCaller.has(n);
3418
+ const contexts = [];
3419
+ const seenPair = /* @__PURE__ */ new Set();
3420
+ const suppressed = /* @__PURE__ */ new Map();
3421
+ for (const a of crossApi) {
3422
+ const type = objectTypeToken(a);
3423
+ if (denylist.has(type)) {
3424
+ suppressed.set(type, "ubiquitous type (needs per-instance aliasing, not a static type grep)");
3425
+ continue;
3426
+ }
3427
+ const token = a.oracle.establisherToken;
3428
+ const aliases = (a.oracle.establisherAliases ?? []).filter(isMechanizableEstablisher);
3429
+ const establishesE = (fn) => {
3430
+ const s = est.get(fn);
3431
+ return !!s && (s.has(token) || aliases.some((al) => s.has(al)));
3432
+ };
3433
+ const typeRe = new RegExp(`\\b${escapeRe3(type)}\\b`);
3434
+ const touchers = cg.fns.filter((f) => {
3435
+ const b = bodies.get(f.name);
3436
+ return b != null && typeRe.test(b);
3437
+ }).map((f) => f.name);
3438
+ if (touchers.length < 2)
3439
+ continue;
3440
+ if (touchers.length > maxTouchers) {
3441
+ suppressed.set(type, `${touchers.length} touchers > ${maxTouchers} (pervasive in-subsystem type, not a focused seam)`);
3442
+ continue;
3443
+ }
3444
+ const establishing = touchers.filter(establishesE);
3445
+ const skipping = touchers.filter((t) => !establishesE(t));
3446
+ if (establishing.length === 0 || skipping.length === 0)
3447
+ continue;
3448
+ let emittedForObject = 0;
3449
+ for (const tB of skipping) {
3450
+ if (emittedForObject >= maxPairsPerObject)
3451
+ break;
3452
+ let tA;
3453
+ let seam;
3454
+ for (const x of establishing) {
3455
+ if (x === tB || reachesFn(adj, x, tB, reachDepth) || reachesFn(adj, tB, x, reachDepth))
3456
+ continue;
3457
+ if (fieldGranular) {
3458
+ const d = dualViewSeamDecision(bodies.get(x) ?? "", bodies.get(tB) ?? "", type);
3459
+ if (!d.keep) {
3460
+ seam = seam ?? d;
3461
+ continue;
3462
+ }
3463
+ tA = x;
3464
+ seam = d;
3465
+ break;
3466
+ }
3467
+ tA = x;
3468
+ break;
3469
+ }
3470
+ if (!tA) {
3471
+ if (fieldGranular && seam)
3472
+ log(`[dual-view] dropped ${tB}() on 'struct ${type}': ${seam.reason}`);
3473
+ continue;
3474
+ }
3475
+ const key = `${a.id}|${tA}|${tB}`;
3476
+ if (seenPair.has(key))
3477
+ continue;
3478
+ seenPair.add(key);
3479
+ const sharedField = seam?.sharedField;
3480
+ const fnB = cg.byName.get(tB);
3481
+ const b = bodies.get(tB) ?? "";
3482
+ const mm = typeRe.exec(b);
3483
+ let line = fnB.startLine;
3484
+ if (mm) {
3485
+ let nl = 0;
3486
+ for (let k = 0; k < mm.index; k++)
3487
+ if (b[k] === "\n")
3488
+ nl++;
3489
+ line = fnB.startLine + nl;
3490
+ }
3491
+ contexts.push({
3492
+ assumptionId: a.id,
3493
+ subject: a.subject,
3494
+ caller: tB,
3495
+ callerFile: fnB.file,
3496
+ callLine: line,
3497
+ establisherToken: token,
3498
+ unprivEntry: isEntry(tB),
3499
+ dualView: true,
3500
+ pairedEntry: tA,
3501
+ object: type,
3502
+ ...sharedField ? { field: sharedField } : {},
3503
+ detail: `DUAL-VIEW (cross-api/cross-phase): ${tA}() and ${tB}() both operate on 'struct ${type}'${sharedField ? `.${sharedField}` : ""}, reached via DISTINCT call-trees (neither calls the other). ${tA}() ESTABLISHES ${token}${aliases.length ? `/${aliases.join("/")}` : ""} on the object; ${tB}() reaches the SAME object type WITHOUT it. ${sharedField ? `Both contend on field '${sharedField}' (${seam?.reason}). ` : ""}${a.subject}() RELIES ON: ${a.predicate} (${a.kind}, ${a.securityRelevance}). If ${tB}() can run on the SAME ${type} INSTANCE concurrently with, or after, ${tA}()'s guarantee, the cross-api assumption is VIOLATED. CANDIDATE to DISPROVE \u2014 type-name toucher grep + field-match + name-based establisher set: confirm (1) both paths reach the SAME instance (aliasing / a shared table / an fd), and (2) ${token} is genuinely absent on ${tB}()'s path.`
3504
+ });
3505
+ emittedForObject++;
3506
+ }
3507
+ }
3508
+ if (suppressed.size > 0) {
3509
+ log(`[dual-view] suppressed ${suppressed.size} pervasive object type(s): ${[...suppressed.entries()].map(([t, r]) => `${t} (${r})`).join("; ")}`);
3510
+ }
3511
+ const scoreById = /* @__PURE__ */ new Map();
3512
+ for (const a of kept)
3513
+ scoreById.set(a.id, scoreDualViewWeaponizability(a).score);
3514
+ contexts.sort((x, y) => Number(y.unprivEntry) - Number(x.unprivEntry) || (scoreById.get(y.assumptionId) ?? 0) - (scoreById.get(x.assumptionId) ?? 0) || (x.object ?? "").localeCompare(y.object ?? "") || x.caller.localeCompare(y.caller));
3515
+ const capped = contexts.slice(0, maxContexts);
3516
+ log(`[dual-view] ${contexts.length} dual-view context(s)${capped.length < contexts.length ? ` (capped to ${capped.length})` : ""}`);
3517
+ return capped;
3518
+ }
3519
+ function assumptionsToHuntPlan(model, kept, contexts) {
3520
+ const bySite = /* @__PURE__ */ new Map();
3521
+ for (const ctx of contexts) {
3522
+ const hint = `ASSUMPTION-VIOLATION candidate (seedless, from mined assumption model). Subject ${ctx.subject}() relies on an establisher (${ctx.establisherToken}) that caller ${ctx.caller}() reaches it without. ${ctx.detail}`;
3523
+ const existing = bySite.get(ctx.callerFile);
3524
+ if (existing)
3525
+ existing.hint = `${existing.hint}
3526
+ ---
3527
+ ${hint}`;
3528
+ else
3529
+ bySite.set(ctx.callerFile, { path: ctx.callerFile, hint });
3530
+ }
3531
+ const kinds = [...new Set(kept.map((a) => a.kind))].join(", ");
3532
+ const brief = {
3533
+ bugClass: `relied-on-assumption violation (seedless, assumption-mining): ${kinds || "precondition not established on a reachable path"}`,
3534
+ pattern: `A function in ${model.subsystem} RELIES ON a precondition it does not itself establish (a validated ref, a held lock, a re-checked value, a once-only init, an exclusive owner, a caller-side capability check), and a reachable context reaches the same object/function WITHOUT establishing it. The candidate sites below are callers found (deterministically) to reach the relied-on subject without the establisher token and without inheriting it from their own callers. Verify each is a real, attacker-reachable violation \u2014 confirm the establisher is genuinely absent on THIS path (not present under a different name, not established by an unfollowed indirect caller) before trusting it.`,
3535
+ fixReference: void 0
3536
+ };
3537
+ return { model, kept, contexts, brief, candidates: [...bySite.values()] };
3538
+ }
3539
+ function buildFocusedCandidates(kept, contexts, bodies, excerptDir) {
3540
+ const predById = new Map(kept.map((a) => [a.id, a.predicate]));
3541
+ const byCaller = /* @__PURE__ */ new Map();
3542
+ for (const ctx of contexts) {
3543
+ const arr = byCaller.get(ctx.caller) ?? [];
3544
+ arr.push(ctx);
3545
+ byCaller.set(ctx.caller, arr);
3546
+ }
3547
+ const candidates = [];
3548
+ const missedCallers = [];
3549
+ let wrote = false;
3550
+ for (const [caller, ctxs] of byCaller) {
3551
+ const callerBody = bodies.get(caller);
3552
+ if (!callerBody) {
3553
+ missedCallers.push(caller);
3554
+ continue;
3555
+ }
3556
+ if (!wrote) {
3557
+ mkdirSync3(excerptDir, { recursive: true });
3558
+ wrote = true;
3559
+ }
3560
+ const subjects = [...new Set(ctxs.map((c) => c.subject))];
3561
+ const header = `/* FOCUSED EXCERPT \u2014 seedless assumption-violation candidate(s) to DISPROVE.
3562
+ * Caller ${caller}() at ${ctxs[0].callerFile}:${ctxs[0].callLine} reaches, WITHOUT establishing the precondition:
3563
+ ` + ctxs.map((c) => ` * ${c.subject}() \u2014 relies on establisher '${c.establisherToken}': ${predById.get(c.assumptionId) ?? ""}`).join("\n") + `
3564
+ * Confirm the establisher is GENUINELY ABSENT on the reachable path: not present under a
3565
+ * wrapper/alias, not established by an unfollowed indirect (fn-pointer/macro) caller. If it is
3566
+ * present under any name, this is a FALSE POSITIVE. Original tree lines are in the headers below. */
3567
+ `;
3568
+ const label = ctxs.some((c) => c.dualView) ? "skipping view (entryB)" : "violating caller";
3569
+ const parts = [header, `/* --- ${label}: ${caller}() (${ctxs[0].callerFile}) --- */
3570
+ ${callerBody}
3571
+ `];
3572
+ const pairedEntries = [...new Set(ctxs.map((c) => c.pairedEntry).filter((p) => !!p))];
3573
+ for (const p of pairedEntries) {
3574
+ const b = bodies.get(p);
3575
+ if (b)
3576
+ parts.push(`/* --- paired establishing view (entryA): ${p}() --- */
3577
+ ${b}
3578
+ `);
3579
+ }
3580
+ for (const s of subjects) {
3581
+ if (pairedEntries.includes(s))
3582
+ continue;
3583
+ const b = bodies.get(s);
3584
+ if (b)
3585
+ parts.push(`/* --- relied-on subject: ${s}() --- */
3586
+ ${b}
3587
+ `);
3588
+ }
3589
+ const safe = caller.replace(/[^A-Za-z0-9_]/g, "_") || "caller";
3590
+ const path = join(excerptDir, `${safe}.c`);
3591
+ writeFileSync3(path, parts.join("\n"), "utf8");
3592
+ const hint = `ASSUMPTION-VIOLATION candidate (focused excerpt: ${caller}() + its relied-on subject(s)). ` + ctxs.map((c) => c.detail).join("\n---\n");
3593
+ candidates.push({ path, hint });
3594
+ }
3595
+ return { candidates, missedCallers };
3596
+ }
3597
+ async function runAssumptionHunt(input) {
3598
+ const log = input.log ?? (() => {
3599
+ });
3600
+ let model;
3601
+ let modelLoaded = false;
3602
+ if (!input.remine) {
3603
+ try {
3604
+ model = loadAssumptionModel(input.modelPath);
3605
+ modelLoaded = true;
3606
+ log(`[assumption] loaded stored model ${input.modelPath} (${model.assumptions.length} assumption(s)) \u2014 no LLM call`);
3607
+ } catch {
3608
+ model = await mineAssumptions(input);
3609
+ storeAssumptionModel(model, input.modelPath);
3610
+ log(`[assumption] mined + stored model ${input.modelPath}`);
3611
+ }
3612
+ } else {
3613
+ model = await mineAssumptions(input);
3614
+ storeAssumptionModel(model, input.modelPath);
3615
+ log(`[assumption] re-mined + stored model ${input.modelPath}`);
3616
+ }
3617
+ const sources = [];
3618
+ for (const file of input.subsystemFiles) {
3619
+ const text = readSource2(input.sourceRoot, file);
3620
+ if (text != null)
3621
+ sources.push({ file, text });
3622
+ }
3623
+ if (sources.length === 0)
3624
+ throw new Error("assumption hunt could not read any subsystemFile under sourceRoot");
3625
+ const cg = buildCallGraph(sources);
3626
+ const bodies = buildFunctionBodyIndex(sources);
3627
+ const crossCheck = crossCheckAssumptions(model, bodies);
3628
+ log(`[assumption] 1b: ${crossCheck.kept.length} kept, ${crossCheck.dropped.length} dropped, ${crossCheck.reclassified.length} reclassified enforced\u2192relied`);
3629
+ const contexts = scanViolatingContexts(crossCheck.kept, cg, sources, bodies, { ...input.scanOptions, log });
3630
+ const dualViewContexts = input.dualView === false ? [] : scanDualViewContexts(crossCheck.kept, cg, sources, bodies, { ...input.dualViewOptions, log });
3631
+ const allContexts = [...contexts, ...dualViewContexts];
3632
+ const plan = assumptionsToHuntPlan(model, crossCheck.kept, allContexts);
3633
+ let witness;
3634
+ if (input.dynamicWitness && dualViewContexts.length > 0) {
3635
+ const { maxCandidates, rotationStatePath, now, ...deps } = input.dynamicWitness;
3636
+ witness = await witnessDualViewContexts({
3637
+ contexts: dualViewContexts,
3638
+ kept: crossCheck.kept,
3639
+ bodies,
3640
+ subsystem: input.subsystem,
3641
+ ...maxCandidates !== void 0 ? { maxCandidates } : {},
3642
+ ...rotationStatePath !== void 0 ? { rotationStatePath } : {},
3643
+ ...now !== void 0 ? { now } : {},
3644
+ deps,
3645
+ log
3646
+ });
3647
+ log(`[assumption] dynamic-witness: ${witness.confirmed.length} confirmed, ${witness.refuted.length} refuted, ${witness.inconclusive.length} inconclusive`);
3648
+ }
3649
+ if (input.skipHunt || plan.candidates.length === 0) {
3650
+ return { model, modelPath: input.modelPath, modelLoaded, crossCheck, contexts, dualViewContexts, plan, ...witness ? { witness } : {} };
3651
+ }
3652
+ let candidates = plan.candidates;
3653
+ if (input.finderTargeting !== false) {
3654
+ const excerptDir = input.excerptDir ?? join(tmpdir(), `0sec-assumption-excerpts-${process.pid}`);
3655
+ const focused = buildFocusedCandidates(crossCheck.kept, allContexts, bodies, excerptDir);
3656
+ const missed = new Set(focused.missedCallers);
3657
+ const fallback = plan.candidates.filter((c) => allContexts.some((ctx) => missed.has(ctx.caller) && ctx.callerFile === c.path));
3658
+ candidates = [...focused.candidates, ...fallback];
3659
+ log(`[assumption] finder-targeting: ${focused.candidates.length} focused excerpt(s) in ${excerptDir}${fallback.length ? ` + ${fallback.length} per-file fallback (unreadable caller bodies)` : ""}`);
3660
+ }
3661
+ const verify = input.verify ?? makeSkepticVerifier({ sourceRoot: input.sourceRoot, runtime: input.runtime });
3662
+ const hunt = await runHuntScan({
3663
+ sourceRoot: input.sourceRoot,
3664
+ candidates,
3665
+ brief: plan.brief,
3666
+ runtime: input.runtime,
3667
+ ...input.finderModels && input.finderModels.length > 0 ? { models: input.finderModels } : {},
3668
+ verify,
3669
+ ...input.exploitability ? { exploitability: input.exploitability } : {},
3670
+ log
3671
+ });
3672
+ return { model, modelPath: input.modelPath, modelLoaded, crossCheck, contexts, dualViewContexts, plan, hunt, ...witness ? { witness } : {} };
3673
+ }
3674
+
3675
+ export {
3676
+ parseC,
3677
+ DEFAULT_ACQUIRE_FNS,
3678
+ DEFAULT_RELEASE_FNS,
3679
+ releaseFnsFor,
3680
+ canon,
3681
+ normalizeLockToken,
3682
+ collectFunctions,
3683
+ findViolationsDataflow,
3684
+ findInterprocRefcountCouplings,
3685
+ couplingsToHuntPlan,
3686
+ extractInvariantSpec,
3687
+ INVARIANT_MODEL_VERSION,
3688
+ resolveContainedSourcePath,
3689
+ buildInvariantModel,
3690
+ storeInvariantModel,
3691
+ loadInvariantModel,
3692
+ findInvariantViolations,
3693
+ findInvariantViolationsTokenLevel,
3694
+ violationsToHuntPlan,
3695
+ runSubsystemInvariantHunt,
3696
+ dualViewCandidateFromContext,
3697
+ buildSynthesisPrompt,
3698
+ extractCFromLlmOutput,
3699
+ makeDefaultSynthesizePoc,
3700
+ defaultBootPoc,
3701
+ candidateReferenceTokens,
3702
+ extractSplatRegion,
3703
+ pocFabricatesSplat,
3704
+ checkWitness,
3705
+ witnessAssumptionViolation,
3706
+ witnessDualViewContexts,
3707
+ ASSUMPTION_MODEL_VERSION,
3708
+ mineAssumptions,
3709
+ normalizeAssumptions,
3710
+ storeAssumptionModel,
3711
+ loadAssumptionModel,
3712
+ isMechanizableEstablisher,
3713
+ subjectSelfEnforces,
3714
+ crossCheckAssumptions,
3715
+ buildFunctionBodyIndex,
3716
+ computeEstablisherWrappers,
3717
+ computeEstablisherSets,
3718
+ scanViolatingContexts,
3719
+ isCrossApiAssumption,
3720
+ objectTypeToken,
3721
+ objectFieldAccesses,
3722
+ heldLockIdsAt,
3723
+ dualViewSeamDecision,
3724
+ scoreDualViewWeaponizability,
3725
+ scanDualViewContexts,
3726
+ assumptionsToHuntPlan,
3727
+ buildFocusedCandidates,
3728
+ runAssumptionHunt
3729
+ };