code wiki / _hdl_build / nx_srcguard.nx

nx_srcguard.nx source

↩ module page · 411 lines · 21569 B

1// nx_srcguard.nx -- CANONICAL-SOURCE REVERT DETECTOR (seq1439 sev9, the work-destroyer's last mile). 2// THE WOUND: on 2026-07-30 canonical daemon sources were silently reverted THREE times, each revert 3// erasing EATEN debts across three lanes with no error anywhere. seq1379 closed the two SOVEREIGN write 4// paths (nx_fs_write compare-and-swap; nx_treepack refuse-to-backdate) and both hold -- but the third 5// path is RAW scp/rsync over ssh, which never touches an API, so no API-side guard can ever see it. 6// ★You cannot gate a door you do not own. What you CAN do is make the damage IMPOSSIBLE TO MISS. 7// 8// ★★★THE RULER IS SYMBOLS, NOT BYTES (v2, 2026-07-30 -- this organ's OWN first field failure). 9// v1 alarmed on any byte-count DECREASE. Within a day of shipping it, it fired twice and was WRONG BOTH 10// TIMES: once on a file sampled mid-rewrite (a truncate-then-write window read as 0 bytes, and I reported 11// the supervisor source as annihilated when it settled to 279688), and once on a sibling's legitimate 12// refactor of nx_hostctl.nx that removed 1294 bytes while keeping ALL 212 top-level symbols. A shrink is 13// not a revert. Dead code gets deleted, comments get tightened, duplication gets folded -- all of that is 14// the tree getting BETTER, and a guard that calls it destruction trains everyone to ignore the guard. 15// What actually distinguishes a revert is that WORK DISAPPEARS: a func/const/struct that existed is GONE. 16// So the verdict is symbol-set containment -- recorded symbols must all still be present, BY NAME. 17// Growth is silent, refactors are silent, and only a real loss speaks. Same instrument the sibling 18// arrived at independently in nx_treepack (REFUSED-WOULD-DROP-SYMBOLS, seq1467). 19// 20// HONEST LIMITATION, stated because an unstated one is a lie: this tracks symbol EXISTENCE, not symbol 21// BODIES. Gutting a function while keeping its name is invisible here. Byte deltas are still reported 22// (advisory) precisely so that case has a signal to be noticed by -- but it is NOT claimed as covered. 23// 24// WHY A STANDALONE ORGAN: measured in the incident forensics, the CASUALTIES were long-lived shared daemon 25// sources while every NEWLY CREATED file SURVIVED every revert. A guard living inside the file it guards 26// would be reverted with it. This one is clobber-resistant BY CONSTRUCTION. 27// nx_srcguard record [listfile] [statefile] [symsfile] -- snapshot the watched set at a known-good state 28// nx_srcguard check [listfile] [statefile] [symsfile] -- exit 0 clean · 4 = WORK LOST (named symbols) 29// Data-driven (rule 11): the watched set is knowledge/registry/srcguard.list, one path per line. 30// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 31import "nx_syscalls.nx" 32import "nx_itoa_lib.nx" // shared MSB-first emitter (zero-alloc) 33 34// derived: AT_FDCWD per the kernel ABI; struct stat is 144B on x86-64 (256 = headroom); offsets VERIFIED 35// byte-exact against `stat -c '%Y %s'` by nx_mtime_probe before this organ was written. 36const SG_AT_FDCWD: i64 = 0 - 100 37const SG_SYS_NEWFSTATAT: i64 = 262 38const SG_STATBUF: i64 = 256 39const SG_OFF_SIZE: i64 = 48 40const SG_OFF_MTIME: i64 = 88 41// sized: the watched set is a handful of canonical daemon sources; 64KiB is ~1000 rows of headroom 42const SG_CAP: i64 = 65536 43const SG_PATHCAP: i64 = 512 44// sized from the MEASURED tree: the largest watched source is nx_mgmt_api.nx; 4MiB is >10x its size, so 45// a single read always lands whole. A short read would silently under-count symbols and manufacture a 46// false LOSS, which is exactly the failure mode this rewrite exists to kill. 47const SG_FCAP: i64 = 4194304 48// sized: nx_hostctl.nx MEASURED at 212 top-level symbols; 256KiB holds ~8000 names at avg 30B. 49const SG_SYMSET_CAP: i64 = 262144 50// sized: 10 watched files x a few hundred symbols x ~60B/row 51const SG_SYMSTATE_CAP: i64 = 2097152 52// derived: a truncate-then-write window is milliseconds; 400ms is ~2 orders of margin and is only ever 53// paid on the rare suspicious path, never in the clean steady state. 54const SG_SETTLE_MS: i64 = 400 55// cap: name at most this many missing symbols in the JSON so a catastrophic loss cannot produce an 56// unbounded line; the COUNT is always exact and is what the verdict keys on. 57const SG_NAME_CAP: i64 = 24 58 59func sg_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 60// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 61// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 62// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 63// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 64func sg_n(v: i64) -> i64 { nxi_out(v); return 0 } 65func sg_eq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 } 66 67// size+mtime of a path. 1 = ok (outs[0]=size, outs[1]=mtime) · 0 = absent. Never invents a value. 68func sg_stat(path: *u8, outs: *i64) -> i64 { 69 let sb: *u8 = sys_mmap(SG_STATBUF) 70 if __syscall(SG_SYS_NEWFSTATAT, SG_AT_FDCWD, path as i64, sb as i64, 0, 0, 0) != 0 { return 0 } 71 let sp: *i64 = ((sb as i64) + SG_OFF_SIZE) as *i64 72 let mp: *i64 = ((sb as i64) + SG_OFF_MTIME) as *i64 73 outs[0] = sp[0] 74 outs[1] = mp[0] 75 return 1 76} 77 78func sg_read(path: *u8, buf: *u8, cap: i64) -> i64 { 79 let fd: i64 = sys_openat_rd(path) 80 if fd < 0 { return 0 - 1 } 81 let n: i64 = sys_read(fd, buf, cap - 1) 82 sys_close(fd) 83 if n < 0 { return 0 - 1 } 84 buf[n] = 0 as u8 85 return n 86} 87 88// copy line [ls,le) of buf into dst (NUL-terminated), returns length 89func sg_line(buf: *u8, ls: i64, le: i64, dst: *u8, cap: i64) -> i64 { 90 var o: i64 = 0 91 var i: i64 = ls 92 while i < le { if o < cap - 1 { dst[o] = buf[i]; o = o + 1 } i = i + 1 } 93 dst[o] = 0 as u8 94 return o 95} 96 97func sg_isid(c: i64) -> i64 { 98 if c >= 97 { if c <= 122 { return 1 } } 99 if c >= 65 { if c <= 90 { return 1 } } 100 if c >= 48 { if c <= 57 { return 1 } } 101 if c == 95 { return 1 } 102 return 0 103} 104 105// if line [i,le) begins with kw, return the offset just past it; else -1. 106func sg_starts(buf: *u8, i: i64, le: i64, kw: *u8) -> i64 { 107 var k: i64 = 0 108 while kw[k] != (0 as u8) { 109 if i + k >= le { return 0 - 1 } 110 if buf[i + k] != kw[k] { return 0 - 1 } 111 k = k + 1 112 } 113 return i + k 114} 115 116// membership in a NUL-separated name set of setlen bytes 117func sg_set_has(set: *u8, setlen: i64, name: *u8) -> i64 { 118 var i: i64 = 0 119 while i < setlen { 120 let ent: *u8 = ((set as i64) + i) as *u8 121 if sg_eq(ent, name) == 1 { return 1 } 122 var k: i64 = 0 123 while ent[k] != (0 as u8) { k = k + 1 } 124 i = i + k + 1 125 } 126 return 0 127} 128 129// Extract top-level declared symbols of <path> into out as NUL-separated names. 130// A "top-level symbol" is a declaration starting at COLUMN 0 -- indented forms are locals and are 131// deliberately not tracked (they are not the unit of work anyone loses sleep over). 132// Returns bytes written into out, or -1 if the file is unreadable. outs[0] receives the name count. 133func sg_syms_of(path: *u8, fb: *u8, out: *u8, ocap: i64, outs: *i64) -> i64 { 134 outs[0] = 0 135 let n: i64 = sg_read(path, fb, SG_FCAP) 136 if n < 0 { return 0 - 1 } 137 var o: i64 = 0 138 var cnt: i64 = 0 139 var i: i64 = 0 140 while i < n { 141 var le: i64 = i 142 var sc: i64 = 1 143 while sc == 1 { if le >= n { sc = 0 } else { if fb[le] == (10 as u8) { sc = 0 } else { le = le + 1 } } } 144 var s: i64 = 0 - 1 145 if s < 0 { s = sg_starts(fb, i, le, "func " as *u8) } 146 if s < 0 { s = sg_starts(fb, i, le, "fn " as *u8) } 147 if s < 0 { s = sg_starts(fb, i, le, "const " as *u8) } 148 if s < 0 { s = sg_starts(fb, i, le, "struct " as *u8) } 149 if s < 0 { s = sg_starts(fb, i, le, "export func " as *u8) } 150 if s >= 0 { 151 var e: i64 = s 152 var go: i64 = 1 153 while go == 1 { if e >= le { go = 0 } else { if sg_isid(fb[e] as i64) == 1 { e = e + 1 } else { go = 0 } } } 154 if e > s { 155 if o + (e - s) + 1 < ocap { 156 var p: i64 = s 157 while p < e { out[o] = fb[p]; o = o + 1; p = p + 1 } 158 out[o] = 0 as u8; o = o + 1 159 cnt = cnt + 1 160 } 161 } 162 } 163 i = le + 1 164 } 165 outs[0] = cnt 166 return o 167} 168 169// recorded size/mtime for <path> in the state buffer, or 0 when unrecorded. Rows: <size>\t<mtime>\t<path> 170func sg_recorded(state: *u8, sn: i64, path: *u8, outs: *i64) -> i64 { 171 var i: i64 = 0 172 let tmp: *u8 = sys_mmap(SG_PATHCAP) 173 while i < sn { 174 var le: i64 = i 175 var sc: i64 = 1 176 while sc == 1 { if le >= sn { sc = 0 } else { if state[le] == (10 as u8) { sc = 0 } else { le = le + 1 } } } 177 var p: i64 = i 178 var sz: i64 = 0 179 while p < le { let c: i64 = state[p] as i64; if c == 9 { p = le } else { if c >= 48 { if c <= 57 { sz = sz * 10 + (c - 48) } } p = p + 1 } } 180 var q: i64 = i 181 var tabs: i64 = 0 182 var pstart: i64 = 0 - 1 183 var mt: i64 = 0 184 while q < le { 185 if state[q] == (9 as u8) { 186 tabs = tabs + 1 187 if tabs == 1 { var r: i64 = q + 1; while r < le { let c2: i64 = state[r] as i64; if c2 == 9 { r = le } else { if c2 >= 48 { if c2 <= 57 { mt = mt * 10 + (c2 - 48) } } r = r + 1 } } } 188 if tabs == 2 { pstart = q + 1; q = le } 189 } 190 if q < le { q = q + 1 } 191 } 192 if pstart > 0 { 193 sg_line(state, pstart, le, tmp, SG_PATHCAP) 194 if sg_eq(tmp, path) == 1 { outs[0] = sz; outs[1] = mt; return 1 } 195 } 196 i = le + 1 197 } 198 return 0 199} 200 201// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 202// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 203// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 204// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 205func sg_emit_i(fd: i64, v: i64) -> i64 { nxi_fd(fd, v); return 0 } 206 207func main(argc: i64, argv: *i64) -> i64 { 208 // SAFE DEFAULT VERB so a SCHEDULER can run this bare (seq1439, id 1785439172). 209 // THE GAP THIS CLOSES: nx_srcguard has been BUILT, DEPLOYED and bite-proven since 2026-07-30 and NOTHING has 210 // ever invoked it -- the row asks for "a source-integrity watchdog ... on a beat" and the watchdog existed 211 // while the beat did not. The live scheduler (clock_jobs.tsv) execs a BARE command: every row is a plain 212 // binary or a legacy .sh, none carry args. So requiring a verb made this organ UNSCHEDULABLE without a shell 213 // wrapper -- which would need a +x the sovereign write path cannot set, and would put /bin/sh on the program 214 // path, which nx_standards_contract item 6 forbids outright. 215 // u00e2u02dcu2026A GUARD THAT CANNOT BE SCHEDULED WITHOUT A SHELL WRAPPER IS A GUARD THAT DOES NOT RUN. 216 // Defaulting to CHECK is safe by construction: `record` is the only mutating verb (it rewrites the baseline 217 // state+syms files) and still has to be asked for BY NAME, so a bare or malformed invocation can never 218 // overwrite the baseline it is supposed to be defending. Explicit args keep working unchanged (rule 19). 219 var verb: *u8 = "check" as *u8 220 if argc >= 2 { verb = argv[1] as *u8 } 221 var listp: *u8 = "knowledge/registry/srcguard.list" as *u8 222 var statep: *u8 = "knowledge/status/srcguard.state" as *u8 223 var symsp: *u8 = "knowledge/status/srcguard.syms" as *u8 224 if argc >= 3 { listp = argv[2] as *u8 } 225 if argc >= 4 { statep = argv[3] as *u8 } 226 if argc >= 5 { symsp = argv[4] as *u8 } 227 228 let lb: *u8 = sys_mmap(SG_CAP) 229 let ln: i64 = sg_read(listp, lb, SG_CAP) 230 if ln < 0 { sg_w("{\"organ\":\"nx_srcguard\",\"refused\":\"watch list unreadable\"}\n" as *u8); sys_exit(2); return 2 } 231 if ln == 0 { sg_w("{\"organ\":\"nx_srcguard\",\"refused\":\"watch list empty -- an empty guard is not a guard\"}\n" as *u8); sys_exit(2); return 2 } 232 233 let isrec: i64 = sg_eq(verb, "record" as *u8) 234 let sb2: *u8 = sys_mmap(SG_CAP) 235 var sn: i64 = 0 236 let yb: *u8 = sys_mmap(SG_SYMSTATE_CAP) 237 var yn: i64 = 0 238 if isrec == 0 { 239 let r: i64 = sg_read(statep, sb2, SG_CAP); if r > 0 { sn = r } 240 let r2: i64 = sg_read(symsp, yb, SG_SYMSTATE_CAP); if r2 > 0 { yn = r2 } 241 } 242 243 var ofd: i64 = 0 - 1 244 var yfd: i64 = 0 - 1 245 if isrec == 1 { ofd = sys_openat_wr(statep, 420); yfd = sys_openat_wr(symsp, 420) } 246 247 let path: *u8 = sys_mmap(SG_PATHCAP) 248 let st: *i64 = sys_mmap(32) as *i64 249 let rec: *i64 = sys_mmap(32) as *i64 250 let fb: *u8 = sys_mmap(SG_FCAP) 251 let sset: *u8 = sys_mmap(SG_SYMSET_CAP) 252 let scnt: *i64 = sys_mmap(32) as *i64 253 let tmpp: *u8 = sys_mmap(SG_PATHCAP) 254 let tmps: *u8 = sys_mmap(SG_PATHCAP) 255 var watched: i64 = 0 256 var absent: i64 = 0 257 var newf: i64 = 0 258 var shrank: i64 = 0 259 var backwards: i64 = 0 260 var okc: i64 = 0 261 var settled: i64 = 0 262 var lost: i64 = 0 263 var symsrec: i64 = 0 264 var named: i64 = 0 265 if isrec == 0 { sg_w("{\"organ\":\"nx_srcguard\",\"ruler\":\"symbol-set containment (bytes advisory)\",\"lost_symbols\":[" as *u8) } 266 var first: i64 = 1 267 var advfirst: i64 = 1 268 let advb: *u8 = sys_mmap(SG_SYMSET_CAP) 269 var advo: i64 = 0 270 var i: i64 = 0 271 while i < ln { 272 var le: i64 = i 273 var sc: i64 = 1 274 while sc == 1 { if le >= ln { sc = 0 } else { if lb[le] == (10 as u8) { sc = 0 } else { le = le + 1 } } } 275 var use: i64 = 1 276 if le <= i { use = 0 } 277 if use == 1 { if lb[i] == (35 as u8) { use = 0 } } 278 if use == 1 { 279 sg_line(lb, i, le, path, SG_PATHCAP) 280 watched = watched + 1 281 if sg_stat(path, st) == 0 { absent = absent + 1 } else { 282 let slen: i64 = sg_syms_of(path, fb, sset, SG_SYMSET_CAP, scnt) 283 if isrec == 1 { 284 if ofd >= 0 { 285 sg_emit_i(ofd, st[0]) 286 sys_write(ofd, "\t" as *u8, 1) 287 sg_emit_i(ofd, st[1]) 288 sys_write(ofd, "\t" as *u8, 1) 289 var pl: i64 = 0; while path[pl] != (0 as u8) { pl = pl + 1 } 290 sys_write(ofd, path, pl) 291 sys_write(ofd, "\n" as *u8, 1) 292 } 293 if yfd >= 0 { 294 if slen > 0 { 295 var so: i64 = 0 296 while so < slen { 297 let ent: *u8 = ((sset as i64) + so) as *u8 298 var el: i64 = 0; while ent[el] != (0 as u8) { el = el + 1 } 299 var pl2: i64 = 0; while path[pl2] != (0 as u8) { pl2 = pl2 + 1 } 300 sys_write(yfd, path, pl2) 301 sys_write(yfd, "\t" as *u8, 1) 302 sys_write(yfd, ent, el) 303 sys_write(yfd, "\n" as *u8, 1) 304 symsrec = symsrec + 1 305 so = so + el + 1 306 } 307 } 308 } 309 okc = okc + 1 310 } else { 311 var pathlost: i64 = 0 312 if slen >= 0 { 313 var j: i64 = 0 314 while j < yn { 315 var je: i64 = j 316 var jc: i64 = 1 317 while jc == 1 { if je >= yn { jc = 0 } else { if yb[je] == (10 as u8) { jc = 0 } else { je = je + 1 } } } 318 var tb: i64 = 0 - 1 319 var q: i64 = j 320 while q < je { if yb[q] == (9 as u8) { tb = q; q = je } else { q = q + 1 } } 321 if tb > 0 { 322 sg_line(yb, j, tb, tmpp, SG_PATHCAP) 323 if sg_eq(tmpp, path) == 1 { 324 sg_line(yb, tb + 1, je, tmps, SG_PATHCAP) 325 if sg_set_has(sset, slen, tmps) == 0 { 326 lost = lost + 1 327 pathlost = pathlost + 1 328 if named < SG_NAME_CAP { 329 named = named + 1 330 if first == 0 { sg_w("," as *u8) } 331 first = 0 332 sg_w("{\"path\":\"" as *u8); sg_w(path) 333 sg_w("\",\"symbol\":\"" as *u8); sg_w(tmps) 334 sg_w("\"}" as *u8) 335 } 336 } 337 } 338 } 339 j = je + 1 340 } 341 } 342 if sg_recorded(sb2, sn, path, rec) == 0 { newf = newf + 1 } else { 343 var bad: i64 = 0 344 if st[0] < rec[0] { bad = 1 } 345 if st[1] < rec[1] { bad = 1 } 346 // ★CONFIRM BEFORE ACCUSING. A writer that truncates-then-writes is momentarily 347 // indistinguishable from a wipe, so a SINGLE sample cannot tell a revert from a 348 // write in progress -- this organ made exactly that mistake in production. Re-stat 349 // after a settle and require BOTH reads to agree, the same two-consecutive-reads 350 // discipline nx_debt's loader uses to rule out a mid-commit sibling. 351 if bad == 1 { 352 sys_sleep_ms(SG_SETTLE_MS) 353 let st2: *i64 = sys_mmap(32) as *i64 354 if sg_stat(path, st2) == 0 { bad = 0; absent = absent + 1 } else { 355 bad = 0 356 if st2[0] < rec[0] { bad = 1 } 357 if st2[1] < rec[1] { bad = 1 } 358 if bad == 1 { st[0] = st2[0]; st[1] = st2[1] } else { settled = settled + 1 } 359 } 360 } 361 if bad == 1 { 362 if st[1] < rec[1] { backwards = backwards + 1 } 363 if st[0] < rec[0] { 364 shrank = shrank + 1 365 if advo + SG_PATHCAP < SG_SYMSET_CAP { 366 var ap: i64 = 0 367 if advfirst == 0 { advb[advo] = 44 as u8; advo = advo + 1 } 368 advfirst = 0 369 while path[ap] != (0 as u8) { advb[advo] = path[ap]; advo = advo + 1; ap = ap + 1 } 370 } 371 } 372 } 373 if bad == 0 { okc = okc + 1 } 374 } 375 if pathlost == 0 { okc = okc + 0 } 376 } 377 } 378 } 379 i = le + 1 380 } 381 if isrec == 1 { 382 if ofd >= 0 { sys_close(ofd) } 383 if yfd >= 0 { sys_close(yfd) } 384 sg_w("{\"organ\":\"nx_srcguard\",\"action\":\"RECORDED\",\"watched\":" as *u8); sg_n(watched) 385 sg_w(",\"recorded\":" as *u8); sg_n(okc) 386 sg_w(",\"symbols_recorded\":" as *u8); sg_n(symsrec) 387 sg_w(",\"absent\":" as *u8); sg_n(absent) 388 sg_w("}\n" as *u8) 389 return 0 390 } 391 sg_w("],\"watched\":" as *u8); sg_n(watched) 392 sg_w(",\"symbols_lost\":" as *u8); sg_n(lost) 393 sg_w(",\"symbols_watched\":" as *u8) 394 var yc: i64 = 0 395 var z: i64 = 0 396 while z < yn { if yb[z] == (10 as u8) { yc = yc + 1 } z = z + 1 } 397 sg_n(yc) 398 sg_w(",\"advisory_shrank\":" as *u8); sg_n(shrank) 399 sg_w(",\"advisory_shrank_paths\":\"" as *u8) 400 if advo > 0 { advb[advo] = 0 as u8; sg_w(advb) } 401 sg_w("\",\"mtime_went_backwards\":" as *u8); sg_n(backwards) 402 sg_w(",\"settled_on_recheck\":" as *u8); sg_n(settled) 403 sg_w(",\"unrecorded\":" as *u8); sg_n(newf) 404 sg_w(",\"absent\":" as *u8); sg_n(absent) 405 if yc == 0 { sg_w(",\"verdict\":\"REFUSED -- no symbol baseline recorded; a guard with an empty baseline passes everything and is worse than none. Run: nx_srcguard record\"}\n" as *u8); sys_exit(2); return 2 } 406 if lost > 0 { sg_w(",\"verdict\":\"WORK LOST -- a recorded top-level symbol is GONE from a canonical source. That is not a refactor, it is a revert (seq1439). Restore from the newest .bak before building, and DO NOT deploy from this tree.\"}\n" as *u8); sys_exit(4); return 4 } 407 if backwards > 0 { sg_w(",\"verdict\":\"BACKDATE DETECTED -- mtime went backwards with symbols intact; an older copy was laid down over a newer one (seq1439).\"}\n" as *u8); sys_exit(4); return 4 } 408 if shrank > 0 { sg_w(",\"verdict\":\"CLEAN (symbols intact) -- listed paths shrank in BYTES but lost no top-level symbol, which is what a refactor looks like. Advisory only: symbol bodies are not tracked.\"}\n" as *u8); return 0 } 409 sg_w(",\"verdict\":\"CLEAN -- every recorded symbol is still present and no clock ran backwards\"}\n" as *u8) 410 return 0 411}