code wiki / _hdl_build / nx_toolsafety_lib.nx

nx_toolsafety_lib.nx source

↩ module page · 608 lines · 27036 B

1// nx_toolsafety_lib.nx -- THE ONE MCP SAFETY-FLAG DERIVER (2026-08-25). Write reach, destructive reach, 2// network reach and exec reach for an organ, derived from its TRANSITIVE import closure, with line 3// comments AND string literals stripped, and syscall DEFINITIONS skipped. 4// 5// WHY A LIB, AND WHY NOW. Two derivers already shipped and DISAGREED BY CONSTRUCTION. nx_schema_backfill 6// (sb_derive_flags) scans ONE FILE, so an organ that writes through an imported helper reaches the write 7// syscall in the LIB and is published readOnly=1 -- wrong in the UNSAFE direction. nx_toolflags folds ONE 8// level of the import closure and strips comments, and its own header set the condition for this 9// extraction in advance: two consumers is not yet a lib, but if a third appears, extract it and delete 10// the copy. The third consumer arrived (the drift control), so this is that extraction. 11// WHEN TWO ORGANS MUST AGREE, MAKE DISAGREEMENT IMPOSSIBLE BY CONSTRUCTION: THERE IS ONLY ONE OF IT. 12// 13// THREE CHANGES FROM THE INCUMBENTS, EACH FOR A REASON THAT WAS MEASURED, NOT PREFERRED: 14// 1. TRANSITIVE closure, not one level. A helper that reaches a writer through a helper of its own is 15// two hops away and one hop cannot see it. The depth LADDER is published per subject, so the 16// contribution of the extra depth is evidence rather than a claim about what it bought. 17// 2. STRING LITERALS ARE STRIPPED TOO. The incumbent declared this a rare limitation erring 18// conservative. It stops being rare the moment the deriver is a LIB that every consumer imports: 19// a marker table written as literals would put write, destructive and network markers into the 20// closure of every organ that composed it. That is why the table is a CONF and this source names 21// no marker at all -- but stripping literals is what makes the property hold for any OTHER scanner 22// too, including the ones already shipped. 23// 3. sys_read_file, so there is no source-size cap left to be inconclusive about. Both incumbents cap 24// at 256 KiB and honestly refuse past it; the primitive that closes that hole already exists, sizes 25// its buffer from the file and cannot short-read. 26// 27// FAIL-CLOSED EVERYWHERE. Unreadable subject, unreadable marker table, an empty marker class, or a walk 28// that hits its budget all return INCONCLUSIVE. None of them may return "nothing matched", because 29// nothing-matched is byte-identical to "this organ is read-only", which is the permissive direction and 30// the entire defect this ruler exists to stop. 31// license_tier: ORIGINAL No hw writes (Rule 26). 32import "nx_syscalls.nx" 33 34const TS_MARKCONF: *u8 = "knowledge/tool_safety_markers.conf" as *u8 35// sized: the marker table declares four classes; a fifth needs a code change here on purpose, because a 36// silently-ignored class would be a marker set that reads as matching nothing. 37const TS_CLASSES: i64 = 4 38const TS_CW: i64 = 0 39const TS_CD: i64 = 1 40const TS_CN: i64 = 2 41const TS_CX: i64 = 3 42// sized: the shipping table carries 5 markers in its largest class; this is generous headroom and the 43// loader REFUSES past it rather than dropping a marker (a dropped marker fails permissive). 44const TS_MAXMARK: i64 = 64 45const TS_MARKCAP: i64 = 64 46const TS_PATHCAP: i64 = 512 47// sized: distinct sources reachable across every closure this lib is asked about in one process. The 48// memo is what makes a transitive walk affordable: nx_syscalls.nx is in essentially every closure and is 49// scanned ONCE. Filling it forces INCONCLUSIVE, never a quiet partial. 50const TS_MEMOSLOTS: i64 = 8192 51const TS_IMPBLK: i64 = 1536 52// sized: files visited in ONE subject's closure walk. 53const TS_MAXWALK: i64 = 1024 54const TS_LF: i64 = 10 55const TS_TAB: i64 = 9 56const TS_HASH_OFF: i64 = 1469598103934665603 57const TS_HASH_PRIME: i64 = 1099511628211 58// odd multiplier for the rolling hash; wrapping i64 multiply, magnitude folded positive each round 59const TS_HASH_MUL: i64 = 131 60const TS_CH_SLASH: i64 = 47 61const TS_CH_QUOTE: i64 = 34 62const TS_CH_BSLASH: i64 = 92 63const TS_CH_SPACE: i64 = 32 64const TS_CH_HASH: i64 = 35 65const TS_CH_F: i64 = 102 66const TS_CH_U: i64 = 117 67const TS_CH_N: i64 = 110 68const TS_CH_C: i64 = 99 69const TS_CH_I: i64 = 105 70const TS_CH_M: i64 = 109 71const TS_CH_P: i64 = 112 72const TS_CH_O: i64 = 111 73const TS_CH_R: i64 = 114 74const TS_CH_T: i64 = 116 75const TS_FUNCPFX: i64 = 5 76const TS_IMPPFX: i64 = 8 77const TS_ROOT_HDL: *u8 = "buildroot/runtime/_hdl_build/" as *u8 78const TS_ROOT_RT: *u8 = "buildroot/runtime/" as *u8 79// depth argument sentinel: walk until the closure is exhausted 80const TS_DEPTH_ALL: i64 = 0 - 1 81 82static TS_MK_P: i64 = 0 83static TS_MKN_P: i64 = 0 84static TS_READY: i64 = 0 85static TS_MEMO_K: i64 = 0 86static TS_MEMO_W: i64 = 0 87static TS_MEMO_D: i64 = 0 88static TS_MEMO_N: i64 = 0 89static TS_MEMO_X: i64 = 0 90static TS_MEMO_OK: i64 = 0 91static TS_MEMO_PATH: i64 = 0 92static TS_MEMO_IMP: i64 = 0 93static TS_MEMO_USED: i64 = 0 94static TS_MEMO_FULL: i64 = 0 95static TS_STAT_FILES: i64 = 0 96static TS_STAT_BYTES: i64 = 0 97static TS_QUEUE_P: i64 = 0 98// The memo slot of the FIRST closure file that supplied the write marker, or -1. A COUNT WITHOUT A 99// WORKLIST IS NOT ACTIONABLE, AND A WORKLIST WITHOUT THE REASON IS STILL ONE STEP SHORT: the attributing 100// file is already in hand at measure time and throwing it away costs the reader one re-run per row. 101// It also separates the two populations that matter -- an organ whose OWN source writes, and an organ 102// that merely imports a lib which can. 103// NOTE: a static may not carry a non-zero initialiser in this dialect (the assembler refuses 104// G3_NONZERO_INIT_STATIC on both lanes), so "no attribution yet" is carried by its OWN flag rather than 105// by a -1 sentinel. Slot 0 is a perfectly valid memo slot, so a 0 sentinel would silently blame it. 106static TS_WSRC: i64 = 0 107static TS_WSRC_OK: i64 = 0 108static TS_TRACE: i64 = 0 109static TS_QDEPTH_P: i64 = 0 110static TS_SEEN_P: i64 = 0 111static TS_SCRATCH_P: i64 = 0 112 113func ts_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 114 115func ts_cat(d: *u8, o: i64, s: *u8) -> i64 { 116 var i: i64 = 0 117 var p: i64 = o 118 while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } 119 return p 120} 121 122// Polynomial rolling hash over the NUL-terminated string. Never returns 0: slot 0 means EMPTY in the 123// memo, so a real key hashing to 0 would read as an empty slot forever -- a cache that never hits. 124// 125// THE FIRST VERSION OF THIS FUNCTION WAS WRONG AND IT FAILED PERMISSIVE, WHICH IS THE EXACT DEFECT CLASS 126// THIS WHOLE LIB EXISTS TO CATCH. It reduced h modulo a prime and then multiplied by that same prime 127// every round, which collapses the state onto a subgroup and collides freely between same-length 128// strings. MEASURED by the trace verb: nx_divprobe's closure visited 2 files instead of 3 -- 129// "buildroot/runtime/nx_syscalls.nx" collided with "buildroot/runtime/nx_divprobe.nx" (both 32 bytes) 130// and the shim was silently dropped from the walk. Fewer files scanned means less reach found, so the 131// bug's error direction was TOWARD read-only. 132// A HASH COLLISION IN A VISITED-SET IS NOT A PERFORMANCE BUG, IT IS SILENT DATA LOSS -- AND HERE IT LOST 133// IT IN THE FLATTERING DIRECTION, WHICH IS WHY NOTHING LOOKED WRONG. 134// Fixed in BOTH places it could bite: the hash distributes properly now, AND every dedup hit is verified 135// by string compare, so a future collision costs a comparison instead of a closure file. 136func ts_hash(s: *u8) -> i64 { 137 var h: i64 = TS_HASH_OFF 138 var i: i64 = 0 139 while s[i] != (0 as u8) { 140 h = h * TS_HASH_MUL 141 h = h + (s[i] as i64) 142 if h < 0 { h = 0 - h } 143 i = i + 1 144 } 145 if h < 0 { h = 0 - h } 146 if h == 0 { h = TS_HASH_PRIME } 147 return h 148} 149 150func ts_streq(a: *u8, b: *u8) -> i64 { 151 var i: i64 = 0 152 while a[i] != (0 as u8) { 153 if a[i] != b[i] { return 0 } 154 i = i + 1 155 } 156 if b[i] != (0 as u8) { return 0 } 157 return 1 158} 159 160// ---- marker table --------------------------------------------------------------------------------- 161// Returns 1 loaded, 0 REFUSED. A refusal is the fail-closed answer: no caller may proceed to publish a 162// quad without markers, because a scan with no markers matches nothing and reads exactly like a clean, 163// read-only organ. 164func ts_load_markers() -> i64 { 165 if TS_READY == 1 { return 1 } 166 let szp: *i64 = sys_mmap(16) as *i64 167 let buf: *u8 = sys_read_file(TS_MARKCONF, szp) 168 if (buf as i64) == 0 { return 0 } 169 let n: i64 = szp[0] 170 if n <= 0 { return 0 } 171 let mk: *u8 = sys_mmap(TS_CLASSES * TS_MAXMARK * TS_MARKCAP) 172 let mkn: *i64 = sys_mmap(TS_CLASSES * 8) as *i64 173 var c: i64 = 0 174 while c < TS_CLASSES { mkn[c] = 0; c = c + 1 } 175 var overflow: i64 = 0 176 var ls: i64 = 0 177 var i: i64 = 0 178 while i <= n { 179 var eol: i64 = 0 180 if i == n { eol = 1 } else { if buf[i] == (TS_LF as u8) { eol = 1 } } 181 if eol == 1 { 182 let le: i64 = i 183 var use: i64 = 1 184 if le <= ls { use = 0 } 185 if use == 1 { if buf[ls] == (TS_CH_HASH as u8) { use = 0 } } 186 if use == 1 { 187 var tab: i64 = 0 - 1 188 var q: i64 = ls 189 while q < le { if buf[q] == (TS_TAB as u8) { if tab < 0 { tab = q } } q = q + 1 } 190 if tab > 0 { 191 let kl: i64 = tab - ls 192 var cls: i64 = 0 - 1 193 if buf[ls] == (TS_CH_U as u8) { cls = 0 - 1 } 194 if kl == 5 { if buf[ls] == (119 as u8) { cls = TS_CW } } 195 if kl == 11 { if buf[ls] == (100 as u8) { cls = TS_CD } } 196 if kl == 7 { if buf[ls] == (TS_CH_N as u8) { cls = TS_CN } } 197 if kl == 4 { if buf[ls] == (101 as u8) { cls = TS_CX } } 198 if cls >= 0 { 199 let ml: i64 = le - tab - 1 200 if ml > 0 { 201 if ml >= TS_MARKCAP { overflow = 1 } else { 202 let idx: i64 = mkn[cls] 203 if idx >= TS_MAXMARK { overflow = 1 } else { 204 let base: i64 = (cls * TS_MAXMARK + idx) * TS_MARKCAP 205 var k: i64 = 0 206 while k < ml { mk[base + k] = buf[tab + 1 + k]; k = k + 1 } 207 mk[base + ml] = 0 as u8 208 mkn[cls] = idx + 1 209 } 210 } 211 } 212 } 213 } 214 } 215 ls = i + 1 216 } 217 i = i + 1 218 } 219 sys_free_file(buf, n) 220 if overflow == 1 { return 0 } 221 var empty: i64 = 0 222 c = 0 223 while c < TS_CLASSES { if mkn[c] == 0 { empty = 1 } c = c + 1 } 224 if empty == 1 { return 0 } 225 TS_MK_P = mk as i64 226 TS_MKN_P = mkn as i64 227 TS_READY = 1 228 return 1 229} 230 231func ts_marker_count(cls: i64) -> i64 { 232 if TS_READY == 0 { return 0 } 233 let mkn: *i64 = TS_MKN_P as *i64 234 return mkn[cls] 235} 236 237func ts_marker_at(cls: i64, idx: i64) -> *u8 { 238 let mk: *u8 = TS_MK_P as *u8 239 return ((mk as i64) + (cls * TS_MAXMARK + idx) * TS_MARKCAP) as *u8 240} 241 242// ---- the stripper --------------------------------------------------------------------------------- 243// Drops line comments AND string literals, so what remains is code. A DEFINITION and a CALL are the same 244// bytes and only their left context tells them apart, so ts_has handles that separately; a marker inside 245// PROSE or inside a LITERAL is not a reach at all and must not survive to be counted. 246// Newlines are PRESERVED for comments (line structure stays) and a newline INSIDE a string literal does 247// not close it -- this dialect permits one, and treating it as a terminator shifts the mask and dumps 248// real code into the dropped region. 249func ts_strip(src: *u8, n: i64, out: *u8) -> i64 { 250 var o: i64 = 0 251 var i: i64 = 0 252 var mode: i64 = 0 253 while i < n { 254 let c: i64 = src[i] as i64 255 if mode == 1 { 256 if c == TS_LF { mode = 0; out[o] = TS_LF as u8; o = o + 1 } 257 i = i + 1 258 } else { 259 if mode == 2 { 260 if c == TS_CH_BSLASH { i = i + 2 } else { 261 if c == TS_CH_QUOTE { mode = 0 } 262 i = i + 1 263 } 264 } else { 265 var starts: i64 = 0 266 if c == TS_CH_SLASH { if i + 1 < n { if src[i+1] == (TS_CH_SLASH as u8) { starts = 1 } } } 267 if starts == 1 { mode = 1; i = i + 2 } else { 268 if c == TS_CH_QUOTE { mode = 2; i = i + 1 } else { 269 out[o] = src[i]; o = o + 1; i = i + 1 270 } 271 } 272 } 273 } 274 } 275 return o 276} 277 278// A CALL, not a DEFINITION. Every organ imports the syscall shim, which DEFINES func sys_openat_wr and 279// the rest; counting a definition marks the whole estate a writer the instant imports are folded in. 280// Occurrences immediately preceded by the five bytes of "func " are declarations and are skipped. The 281// rule is structural, so no filename is special-cased and it holds for any future shim. 282func ts_has(buf: *u8, n: i64, needle: *u8) -> i64 { 283 let m: i64 = ts_slen(needle) 284 if m == 0 { return 0 } 285 var i: i64 = 0 286 while i + m <= n { 287 var j: i64 = 0 288 var hit: i64 = 1 289 while j < m { if buf[i+j] != needle[j] { hit = 0; j = m } else { j = j + 1 } } 290 if hit == 1 { 291 var pfx: i64 = 0 292 if i >= TS_FUNCPFX { 293 if (buf[i-5] as i64) == TS_CH_F { pfx = pfx + 1 } 294 if (buf[i-4] as i64) == TS_CH_U { pfx = pfx + 1 } 295 if (buf[i-3] as i64) == TS_CH_N { pfx = pfx + 1 } 296 if (buf[i-2] as i64) == TS_CH_C { pfx = pfx + 1 } 297 if (buf[i-1] as i64) == TS_CH_SPACE { pfx = pfx + 1 } 298 } 299 if pfx < TS_FUNCPFX { return 1 } 300 } 301 i = i + 1 302 } 303 return 0 304} 305 306// box[TS_CW..TS_CX] are ORed into, never cleared: the caller accumulates across a closure. 307func ts_scan_buf(buf: *u8, n: i64, box: *i64) -> i64 { 308 var cls: i64 = 0 309 while cls < TS_CLASSES { 310 if box[cls] == 0 { 311 let cnt: i64 = ts_marker_count(cls) 312 var k: i64 = 0 313 while k < cnt { 314 if ts_has(buf, n, ts_marker_at(cls, k)) == 1 { box[cls] = 1; k = cnt } else { k = k + 1 } 315 } 316 } 317 cls = cls + 1 318 } 319 return 0 320} 321 322// Collect import module names out of the RAW bytes into a NUL-terminated, LF-separated block. Parsed 323// from raw on purpose: the stripper removes the very literal the module name lives in, and a 324// commented-out import counted as present errs toward MORE files scanned, which is the safe direction. 325func ts_imports_buf(buf: *u8, n: i64, out: *u8) -> i64 { 326 var o: i64 = 0 327 var i: i64 = 0 328 while i + TS_IMPPFX < n { 329 var m: i64 = 1 330 if (buf[i] as i64) != TS_CH_I { m = 0 } 331 if m == 1 { if (buf[i+1] as i64) != TS_CH_M { m = 0 } } 332 if m == 1 { if (buf[i+2] as i64) != TS_CH_P { m = 0 } } 333 if m == 1 { if (buf[i+3] as i64) != TS_CH_O { m = 0 } } 334 if m == 1 { if (buf[i+4] as i64) != TS_CH_R { m = 0 } } 335 if m == 1 { if (buf[i+5] as i64) != TS_CH_T { m = 0 } } 336 if m == 1 { if (buf[i+6] as i64) != TS_CH_SPACE { m = 0 } } 337 if m == 1 { if (buf[i+7] as i64) != TS_CH_QUOTE { m = 0 } } 338 if m == 1 { 339 var p: i64 = i + TS_IMPPFX 340 var st: i64 = o 341 var go: i64 = 1 342 while go == 1 { 343 if p >= n { go = 0 } else { 344 if buf[p] == (TS_CH_QUOTE as u8) { go = 0 } else { 345 if o < TS_IMPBLK - 2 { out[o] = buf[p]; o = o + 1 } 346 p = p + 1 347 } 348 } 349 } 350 if o > st { out[o] = TS_LF as u8; o = o + 1 } 351 i = p + 1 352 } else { i = i + 1 } 353 } 354 out[o] = 0 as u8 355 return o 356} 357 358func ts_resolve(mod: *u8, out: *u8) -> i64 { 359 var o: i64 = ts_cat(out, 0, TS_ROOT_HDL) 360 o = ts_cat(out, o, mod) 361 out[o] = 0 as u8 362 var fd: i64 = sys_openat_rd(out) 363 if fd >= 0 { sys_close(fd); return 1 } 364 o = ts_cat(out, 0, TS_ROOT_RT) 365 o = ts_cat(out, o, mod) 366 out[o] = 0 as u8 367 fd = sys_openat_rd(out) 368 if fd >= 0 { sys_close(fd); return 1 } 369 return 0 370} 371 372func ts_memo_init() -> i64 { 373 if TS_MEMO_K != 0 { return 1 } 374 TS_MEMO_K = sys_mmap(TS_MEMOSLOTS * 8) as i64 375 TS_MEMO_W = sys_mmap(TS_MEMOSLOTS * 8) as i64 376 TS_MEMO_D = sys_mmap(TS_MEMOSLOTS * 8) as i64 377 TS_MEMO_N = sys_mmap(TS_MEMOSLOTS * 8) as i64 378 TS_MEMO_X = sys_mmap(TS_MEMOSLOTS * 8) as i64 379 TS_MEMO_OK = sys_mmap(TS_MEMOSLOTS * 8) as i64 380 TS_MEMO_PATH = sys_mmap(TS_MEMOSLOTS * TS_PATHCAP) as i64 381 TS_MEMO_IMP = sys_mmap(TS_MEMOSLOTS * TS_IMPBLK) as i64 382 TS_QUEUE_P = sys_mmap(TS_MAXWALK * TS_PATHCAP) as i64 383 TS_QDEPTH_P = sys_mmap(TS_MAXWALK * 8) as i64 384 TS_SEEN_P = sys_mmap(TS_MAXWALK * 8) as i64 385 TS_SCRATCH_P = sys_mmap(TS_PATHCAP) as i64 386 return 1 387} 388 389// Slot for path, scanning the file on first sight. Returns the slot index, or -1 if the memo is FULL. 390// A full memo is INCONCLUSIVE and is reported; it is never allowed to degrade into a fresh scan whose 391// result silently is not cached, because that turns a budget into a performance cliff nobody can see. 392func ts_slot(path: *u8) -> i64 { 393 ts_memo_init() 394 let keys: *i64 = TS_MEMO_K as *i64 395 let paths: *u8 = TS_MEMO_PATH as *u8 396 let h: i64 = ts_hash(path) 397 var probe: i64 = h - ((h / TS_MEMOSLOTS) * TS_MEMOSLOTS) 398 var tries: i64 = 0 399 var slot: i64 = 0 - 1 400 while tries < TS_MEMOSLOTS { 401 if keys[probe] == 0 { slot = probe; tries = TS_MEMOSLOTS } else { 402 if keys[probe] == h { 403 let cand: *u8 = ((paths as i64) + probe * TS_PATHCAP) as *u8 404 if ts_streq(cand, path) == 1 { return probe } 405 } 406 probe = probe + 1 407 if probe >= TS_MEMOSLOTS { probe = 0 } 408 tries = tries + 1 409 } 410 } 411 if slot < 0 { TS_MEMO_FULL = 1; return 0 - 1 } 412 413 let fw: *i64 = TS_MEMO_W as *i64 414 let fd2: *i64 = TS_MEMO_D as *i64 415 let fn2: *i64 = TS_MEMO_N as *i64 416 let fx: *i64 = TS_MEMO_X as *i64 417 let fok: *i64 = TS_MEMO_OK as *i64 418 let imps: *u8 = TS_MEMO_IMP as *u8 419 keys[slot] = h 420 var pc: i64 = 0 421 while path[pc] != (0 as u8) { 422 if pc < TS_PATHCAP - 1 { paths[slot * TS_PATHCAP + pc] = path[pc] } 423 pc = pc + 1 424 } 425 paths[slot * TS_PATHCAP + pc] = 0 as u8 426 fw[slot] = 0 427 fd2[slot] = 0 428 fn2[slot] = 0 429 fx[slot] = 0 430 fok[slot] = 0 431 imps[slot * TS_IMPBLK] = 0 as u8 432 433 let szp: *i64 = sys_mmap(16) as *i64 434 let raw: *u8 = sys_read_file(path, szp) 435 if (raw as i64) == 0 { return slot } 436 let rn: i64 = szp[0] 437 if rn <= 0 { sys_free_file(raw, rn); return slot } 438 let stripped: *u8 = sys_mmap(rn + 1) 439 let sn: i64 = ts_strip(raw, rn, stripped) 440 let box: *i64 = sys_mmap(TS_CLASSES * 8) as *i64 441 var c: i64 = 0 442 while c < TS_CLASSES { box[c] = 0; c = c + 1 } 443 ts_scan_buf(stripped, sn, box) 444 let impout: *u8 = ((imps as i64) + slot * TS_IMPBLK) as *u8 445 ts_imports_buf(raw, rn, impout) 446 fw[slot] = box[TS_CW] 447 fd2[slot] = box[TS_CD] 448 fn2[slot] = box[TS_CN] 449 fx[slot] = box[TS_CX] 450 fok[slot] = 1 451 TS_STAT_FILES = TS_STAT_FILES + 1 452 TS_STAT_BYTES = TS_STAT_BYTES + rn 453 sys_munmap(stripped, rn + 1) 454 sys_free_file(raw, rn) 455 TS_MEMO_USED = TS_MEMO_USED + 1 456 return slot 457} 458 459// THE RULER. maxdepth 0 = the organ's own source only (what the single-file deriver sees); 1 = one level 460// of imports (what nx_toolflags sees); TS_DEPTH_ALL = the whole closure. 461// box[0]=write box[1]=destructive box[2]=network box[3]=exec 462// box[4]=files_visited box[5]=unresolved_imports box[6]=budget_hit box[7]=subject_readable 463// Returns 1 CONCLUSIVE, 0 INCONCLUSIVE (subject unreadable, markers unloadable, or a budget was hit). 464func ts_reach(path: *u8, maxdepth: i64, box: *i64) -> i64 { 465 var z: i64 = 0 466 while z < 8 { box[z] = 0; z = z + 1 } 467 TS_WSRC = 0 468 TS_WSRC_OK = 0 469 if ts_load_markers() == 0 { return 0 } 470 ts_memo_init() 471 let q: *u8 = TS_QUEUE_P as *u8 472 let qd: *i64 = TS_QDEPTH_P as *i64 473 let seen: *i64 = TS_SEEN_P as *i64 474 let scratch: *u8 = TS_SCRATCH_P as *u8 475 let fw: *i64 = TS_MEMO_W as *i64 476 let fdd: *i64 = TS_MEMO_D as *i64 477 let fnn: *i64 = TS_MEMO_N as *i64 478 let fxx: *i64 = TS_MEMO_X as *i64 479 let fok: *i64 = TS_MEMO_OK as *i64 480 let imps: *u8 = TS_MEMO_IMP as *u8 481 482 var qn: i64 = 1 483 var head: i64 = 0 484 var sn2: i64 = 1 485 var pc: i64 = 0 486 while path[pc] != (0 as u8) { q[pc] = path[pc]; pc = pc + 1 } 487 q[pc] = 0 as u8 488 qd[0] = 0 489 seen[0] = ts_hash(path) 490 var conclusive: i64 = 1 491 492 while head < qn { 493 let cur: *u8 = ((q as i64) + head * TS_PATHCAP) as *u8 494 let depth: i64 = qd[head] 495 let slot: i64 = ts_slot(cur) 496 if slot < 0 { conclusive = 0; box[6] = 1 } else { 497 if fok[slot] == 0 { 498 if head == 0 { conclusive = 0 } else { box[5] = box[5] + 1 } 499 } else { 500 if head == 0 { box[7] = 1 } 501 box[4] = box[4] + 1 502 if TS_TRACE == 1 { 503 sys_write(1, " visit " as *u8, 10) 504 var tl: i64 = ts_slen(cur) 505 sys_write(1, cur, tl) 506 if fw[slot] == 1 { sys_write(1, " [WRITE]" as *u8, 9) } 507 if fdd[slot] == 1 { sys_write(1, " [DESTR]" as *u8, 9) } 508 if fnn[slot] == 1 { sys_write(1, " [NET]" as *u8, 7) } 509 sys_write(1, "\n" as *u8, 1) 510 } 511 if fw[slot] == 1 { if box[TS_CW] == 0 { TS_WSRC = slot; TS_WSRC_OK = 1 } box[TS_CW] = 1 } 512 if fdd[slot] == 1 { box[TS_CD] = 1 } 513 if fnn[slot] == 1 { box[TS_CN] = 1 } 514 if fxx[slot] == 1 { box[TS_CX] = 1 } 515 var descend: i64 = 0 516 if maxdepth == TS_DEPTH_ALL { descend = 1 } else { if depth < maxdepth { descend = 1 } } 517 if descend == 1 { 518 let blk: *u8 = ((imps as i64) + slot * TS_IMPBLK) as *u8 519 var bi: i64 = 0 520 var ms: i64 = 0 521 while blk[bi] != (0 as u8) { 522 if blk[bi] == (TS_LF as u8) { 523 var ml: i64 = bi - ms 524 if ml > 0 { 525 var k: i64 = 0 526 let modn: *u8 = sys_mmap(TS_PATHCAP) 527 while k < ml { modn[k] = blk[ms + k]; k = k + 1 } 528 modn[ml] = 0 as u8 529 if ts_resolve(modn, scratch) == 1 { 530 let hh: i64 = ts_hash(scratch) 531 var dup: i64 = 0 532 var si: i64 = 0 533 // seen[] and the queue grow in lockstep, so index si names the same 534 // path in both. VERIFY THE STRING: a hash-only visited-set turns a 535 // collision into a dropped closure file, and it drops it silently. 536 while si < sn2 { 537 if seen[si] == hh { 538 let prev: *u8 = ((q as i64) + si * TS_PATHCAP) as *u8 539 if ts_streq(prev, scratch) == 1 { dup = 1; si = sn2 } else { si = si + 1 } 540 } else { si = si + 1 } 541 } 542 if dup == 0 { 543 if qn >= TS_MAXWALK { conclusive = 0; box[6] = 1 } else { 544 let dst: *u8 = ((q as i64) + qn * TS_PATHCAP) as *u8 545 var cc: i64 = 0 546 while scratch[cc] != (0 as u8) { dst[cc] = scratch[cc]; cc = cc + 1 } 547 dst[cc] = 0 as u8 548 qd[qn] = depth + 1 549 seen[sn2] = hh 550 sn2 = sn2 + 1 551 qn = qn + 1 552 } 553 } 554 } else { box[5] = box[5] + 1 } 555 sys_munmap(modn, TS_PATHCAP) 556 } 557 ms = bi + 1 558 } 559 bi = bi + 1 560 } 561 } 562 } 563 } 564 head = head + 1 565 } 566 if box[7] == 0 { conclusive = 0 } 567 return conclusive 568} 569 570// The published quad, in the SAME per-flag depth policy the shipping deriver chose, and for the same 571// reason: readOnly takes the DEEPEST view because claiming read-only wrongly is the unsafe error, while 572// destructive and openWorld take the organ's OWN source because a fat lib imported for one helper drags 573// an unlink somewhere into every closure and a warning that fires on everything carries no signal. 574// PICK THE CONSERVATIVE SIDE PER FLAG, NOT PER SCAN -- one depth for all four forces a choice between a 575// lie and a blindfold. quad[0]=readOnly quad[1]=destructive quad[2]=idempotent quad[3]=openWorld. 576// idempotent is UNDECLARED (0) and never guessed: nothing in a source declares it. 577// Returns 1 CONCLUSIVE, 0 INCONCLUSIVE -- and an INCONCLUSIVE quad must not be published at all. 578func ts_quad(path: *u8, quad: *i64, deep: *i64, own: *i64) -> i64 { 579 let okd: i64 = ts_reach(path, TS_DEPTH_ALL, deep) 580 let oko: i64 = ts_reach(path, 0, own) 581 if okd == 0 { return 0 } 582 if oko == 0 { return 0 } 583 if deep[TS_CW] == 0 { quad[0] = 1 } else { quad[0] = 0 } 584 quad[1] = own[TS_CD] 585 quad[2] = 0 586 quad[3] = own[TS_CN] 587 return 1 588} 589 590func ts_set_trace(v: i64) -> i64 { TS_TRACE = v; return v } 591 592// The closure file that supplied the write marker for the last ts_reach, copied into out. Returns 1 when 593// there was one, 0 when the subject reached no writer at all. ATTRIBUTION IS THE DIFFERENCE BETWEEN A 594// COUNT AND A CAMPAIGN: 124 rows that all name ONE shared lib are ONE decision, not 124. 595func ts_write_src(out: *u8) -> i64 { 596 out[0] = 0 as u8 597 if TS_WSRC_OK == 0 { return 0 } 598 let paths: *u8 = TS_MEMO_PATH as *u8 599 let src: *u8 = ((paths as i64) + TS_WSRC * TS_PATHCAP) as *u8 600 var i: i64 = 0 601 while src[i] != (0 as u8) { out[i] = src[i]; i = i + 1 } 602 out[i] = 0 as u8 603 return 1 604} 605 606func ts_stat_files() -> i64 { return TS_STAT_FILES } 607func ts_stat_bytes() -> i64 { return TS_STAT_BYTES } 608func ts_memo_full() -> i64 { return TS_MEMO_FULL }