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}