code wiki / _hdl_build / nx_cron_reconcile.nx
nx_cron_reconcile.nx source
↩ module page · 360 lines · 19297 B
1// nx_cron_reconcile.nx -- SOVEREIGN crontab reconciler (operator directive 2026-07-16: "make the
2// capabilities native to the nishi ecosystem, don't make me do it"). The nishi-managed block of a crontab
3// becomes DATA: cron.reg declares exactly the nishi rows that must exist; this organ rewrites the crontab
4// so its nishi rows (any line containing /nishihost/) equal EXACTLY the declared set, leaving every
5// non-nishi row BYTE-EXACT. Dead rows (retired reconcilers) vanish by construction; new rows = a registry
6// line. Runs as root with ZERO operator action by piggybacking on the existing root cron row
7// (nx_edge443_reconcile.sh -- the same proven pattern as the vsz_watchdog on the rail).
8//
9// NEVER-BRICK BY CONSTRUCTION (this writes a root-owned BOOT FILE):
10// R1 only lines containing /nishihost/ are ever touched -- the filter IS the blast-radius bound
11// R2 refuse (exit 3, no write) if the registry is missing/empty -- an absent SSOT must never wipe rows
12// R3 refuse (exit 5, no write) if ANY declared row lacks /nishihost/ -- the organ cannot inject
13// non-nishi rows even if the registry is corrupted/hostile
14// R4 refuse (exit 4, no write) if the crontab is unreadable or EMPTY -- a boot file is never empty
15// R5 idempotent: byte-identical result -> NO-CHANGE, zero writes (safe at any frequency)
16// R6 atomic: write <crontab>.nxnew then renameat over -- no torn boot file ever exists on disk
17// After a real change, SIGHUP every `crond` (reload -- standard, non-destructive; if crond ignores it,
18// stale in-memory entries keep harmlessly firing removed paths until its next restart = no regression).
19//
20// ---- THE SILENT-TRUNCATION DEFECT, FIXED 2026-08-20 (debt 1787260381) ---------------------------
21// THIS ORGAN SPENT AT LEAST TWO WEEKS APPLYING A PREFIX OF ITS OWN SSOT AND REPORTING IT AS THE WHOLE.
22// cr_read used to do ONE sys_read of CR_REGCAP-1 = 16383 bytes with no truncation check, while cron.reg
23// had grown to 24356. Every declared row past 16 KiB was invisible; the organ then counted what it had
24// SEEN into decl and printed "NO-CHANGE (crontab already matches cron.reg; declared=55)" -- a number that
25// reads exactly like a complete count. MEASURED: 66 rows declared, 55 applied, 11 silently inert, among
26// them the estate's only compensating control over a fail-open authentication path.
27// **** A CAP REACHED IN SILENCE BECOMES A MEASUREMENT NOBODY KNOWS IS PARTIAL. ****
28// The fingerprint was in this organ's own log the whole time and nobody had a reason to look: `declared`
29// FELL 58 -> 57 -> 55 across 1346 runs while the registry only ever GREW. A registry that only grows
30// whose applied count only shrinks is a byte cap being crossed, one comment block at a time. And R5
31// (idempotence) hid it perfectly: the crontab genuinely DID match the truncated view, so CHANGED events
32// over that entire window numbered ZERO and every run looked healthy.
33//
34// THE FIX IS THE REMOVAL OF THE CAP, NOT A BIGGER CAP. A raised ceiling is the same defect with a later
35// trigger date, and this file's history is three variants of the same class already. cr_slurp composes
36// sys_read_file (nx_syscalls.nx), which sizes its buffer from the file itself via lseek END and cannot
37// short-read -- the estate primitive that already ate this exact debt twice (a 9 GB gguf 2026-07-15, and
38// the address-space regression 2026-08-19). There is no length constant left to outgrow.
39//
40// AND REMOVAL ALONE IS NOT ENOUGH, BECAUSE THE OLD BUG WAS UNDETECTABLE RATHER THAN MERELY WRONG. Three
41// guards now make the class announce instead of degrade:
42// G1 TWO INSTRUMENTS, ONE FACT: the bytes handed back are compared against the file's OWN size taken
43// independently (cr_fsize / lseek END). A disagreement is a PARTIAL READ and REFUSES (exit 7). This
44// is the tooth that fails against the pre-fix binary: 16383 != 24356.
45// G2 THE PARSE MUST REACH THE END: `scanned=` is printed beside `reg_bytes=`, so "I only ever saw a
46// prefix" is visible in the evidence line rather than derivable only by someone who already suspects
47// it. A count with no denominator beside it is how this hid.
48// G3 THE PARTITION MUST SUM: rows DECLARED by the registry and rows APPLIED into the output are counted
49// separately and must be equal, and the kept/removed/applied split must account for every line.
50// A mismatch REFUSES (exit 8) rather than writing a boot file nobody can reconcile.
51// ⚠G1 is the one that would have caught the original; G2 and G3 catch the next variant. Stated that way
52// deliberately -- claiming all three would have caught it would be an overclaim about my own instrument.
53//
54// usage: nx_cron_reconcile <crontab-path> <reg-path> (gate runs it on COPIES; prod = /etc/crontab)
55// exit: 0 ok (CHANGED or NO-CHANGE) | 2 usage | 3 reg-refuse | 4 crontab-refuse | 5 reg-taint | 6 write-fail
56// | 7 partial-read (the two size instruments disagree) | 8 partition-mismatch (declared != applied)
57// license_tier: ORIGINAL module: nishi-core.ops.cron_reconcile
58import "nx_syscalls.nx"
59const CR_MAGIC_65536: i64 = 65536
60
61// CR_CTCAP (65536) and CR_REGCAP (16384) ARE DELIBERATELY GONE, NOT RAISED. Both files are now read whole.
62// The crontab one was a latent copy of the same bug: /etc/crontab is GENERATED FROM cron.reg, so it
63// inherits the registry's growth and would have crossed its own ceiling next.
64// The output buffer is DERIVED from the two inputs instead of being a third guess. Upper bound is exact:
65// the crontab pass emits at most every byte it kept plus one newline if the file lacked a trailing one,
66// and the registry pass the same -- so cn + rn + 2 suffices. The slack is a small named margin over that
67// proven bound, not a tuning knob.
68const CR_OUT_SLACK: i64 = 16
69const CR_SEEK_END: i64 = 2
70const CR_EXIT_PARTIAL_READ: i64 = 7
71const CR_EXIT_PARTITION: i64 = 8
72const CR_MARK: *u8 = "/nishihost/" as *u8
73
74func cr_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
75func cr_p(s: *u8) -> i64 { sys_write(1, s, cr_slen(s)); return 0 }
76func cr_decw(v: i64, out: *u8) -> i64 {
77 var o: i64 = 0
78 var m: i64 = v
79 if m < 0 { out[0] = 45 as u8; o = 1; m = 0 - m }
80 if m == 0 { out[o] = 48 as u8; return o + 1 }
81 var nd: i64 = 1
82 var mm: i64 = m
83 while mm >= 10 { nd = nd + 1; mm = mm / 10 }
84 var i: i64 = nd - 1
85 while m > 0 { out[o + i] = (48 + (m % 10)) as u8; m = m / 10; i = i - 1 }
86 return o + nd
87}
88func cr_pn(v: i64) -> i64 { let b: *u8 = sys_mmap(28); let n: i64 = cr_decw(v, b); sys_write(1, b, n); sys_munmap(b, 28); return 0 }
89func cr_contains(hay: *u8, hlen: i64, needle: *u8) -> i64 {
90 let nlen: i64 = cr_slen(needle)
91 if hlen < nlen { return 0 }
92 var s: i64 = 0
93 let last: i64 = hlen - nlen
94 while s <= last {
95 var m: i64 = 0
96 var eq: i64 = 1
97 while m < nlen { if hay[s + m] != needle[m] { eq = 0; m = nlen } m = m + 1 }
98 if eq == 1 { return 1 }
99 s = s + 1
100 }
101 return 0
102}
103func cr_line_end(buf: *u8, pos: i64, end: i64) -> i64 {
104 var p: i64 = pos
105 while p < end { if (buf[p] as i64) == 10 { return p } p = p + 1 }
106 return end
107}
108// THE FILE'S OWN SIZE, taken INDEPENDENTLY of the read. This is the second instrument in G1: two ways of
109// answering "how big is this file" that must agree. Returns -1 if the size cannot be established, and the
110// caller then treats the check as UNOBSERVABLE rather than as a pass -- "I could not look" is not "it is fine".
111func cr_fsize(path: *u8) -> i64 {
112 let fd: i64 = sys_openat_rd(path)
113 if fd < 0 { return 0 - 1 }
114 let n: i64 = sys_lseek(fd, 0, CR_SEEK_END)
115 sys_close(fd)
116 return n
117}
118
119// READ THE WHOLE FILE, WITH NO LENGTH CONSTANT ANYWHERE IN THE PATH. sys_read_file sizes its buffer from
120// the file (lseek END) and drains to EOF, so a short read is not a thing this can do. Returns the buffer
121// (or 0 on failure) and writes the length through out_len.
122func cr_slurp(path: *u8, out_len: *i64) -> *u8 {
123 let n: *i64 = sys_mmap(16) as *i64
124 n[0] = 0
125 let b: *u8 = sys_read_file(path, n)
126 out_len[0] = n[0]
127 return b
128}
129
130func main(argc: i64, argv: *i64) -> i64 {
131 if argc < 3 { cr_p("usage: nx_cron_reconcile <crontab-path> <reg-path>\n" as *u8); return 2 }
132 let ctpath: *u8 = argv[1] as *u8
133 let regpath: *u8 = argv[2] as *u8
134 let cnp: *i64 = sys_mmap(16) as *i64
135 let rnp: *i64 = sys_mmap(16) as *i64
136
137 // R4: crontab must exist and be non-empty
138 let ct: *u8 = cr_slurp(ctpath, cnp)
139 let cn: i64 = cnp[0]
140 if (ct as i64) == 0 { cr_p("REFUSED: crontab unreadable (a boot file is never absent) -- no write\n" as *u8); return 4 }
141 if cn <= 0 { cr_p("REFUSED: crontab unreadable/empty (a boot file is never empty) -- no write\n" as *u8); return 4 }
142 // G1 for the crontab too. Same guard, same reason: WE generate this file from the registry, so it
143 // inherits the registry growth that broke the other side. A latent copy of a live bug is still a bug.
144 let ctsz: i64 = cr_fsize(ctpath)
145 if ctsz >= 0 { if cn != ctsz {
146 cr_p("REFUSED: PARTIAL READ of the crontab -- got " as *u8); cr_pn(cn)
147 cr_p(" bytes but the file is " as *u8); cr_pn(ctsz)
148 cr_p(" -- refusing rather than reconciling against a prefix\n" as *u8)
149 return CR_EXIT_PARTIAL_READ
150 } }
151 // R2: registry must exist with >=1 declared row; R3: every row must carry the nishi mark
152 let reg: *u8 = cr_slurp(regpath, rnp)
153 let rn: i64 = rnp[0]
154 if (reg as i64) == 0 { cr_p("REFUSED: cron.reg missing -- an absent SSOT never wipes rows\n" as *u8); return 3 }
155 if rn <= 0 { cr_p("REFUSED: cron.reg missing/empty -- an absent SSOT never wipes rows\n" as *u8); return 3 }
156 // ---- G1: TWO INSTRUMENTS, ONE FACT. THIS IS THE TOOTH THE PRE-FIX BINARY FAILS. -----------
157 // The old reader arrived here holding 16383 bytes of a 24356-byte file and was perfectly happy,
158 // because nothing downstream had any way to know what it had NOT been given. Comparing the bytes we
159 // were handed against the file OWN size is the cheapest possible check and the only one that can see
160 // a prefix AS a prefix. If the size cannot be established the check is UNOBSERVABLE and is skipped
161 // rather than counted as a pass -- "I could not look" is not "I looked and it is fine".
162 let regsz: i64 = cr_fsize(regpath)
163 if regsz >= 0 { if rn != regsz {
164 cr_p("REFUSED: PARTIAL READ of cron.reg -- got " as *u8); cr_pn(rn)
165 cr_p(" bytes but the file is " as *u8); cr_pn(regsz)
166 cr_p(" -- every row past a short read is SILENTLY INERT, so this refuses instead\n" as *u8)
167 return CR_EXIT_PARTIAL_READ
168 } }
169 // The output is DERIVED from the two inputs, so there is no third size left to outgrow.
170 let out: *u8 = sys_mmap(cn + rn + CR_OUT_SLACK)
171 let np: *u8 = sys_mmap(512)
172 var decl: i64 = 0
173 var scanned: i64 = 0
174 var pos: i64 = 0
175 while pos < rn {
176 let le: i64 = cr_line_end(reg, pos, rn)
177 if le > pos { if (reg[pos] as i64) != 35 { if (reg[pos] as i64) != 13 {
178 if cr_contains((((reg as i64) + pos)) as *u8, le - pos, CR_MARK) == 0 {
179 cr_p("REFUSED: declared row lacks /nishihost/ (registry taint -- this organ cannot manage non-nishi rows)\n" as *u8)
180 return 5
181 }
182 decl = decl + 1
183 } } }
184 scanned = le
185 pos = le + 1
186 }
187 if decl == 0 { cr_p("REFUSED: cron.reg has no declared rows -- no write\n" as *u8); return 3 }
188
189 // build: non-nishi crontab lines byte-exact, then the declared block
190 var w: i64 = 0
191 var removed: i64 = 0
192 var kept: i64 = 0
193 var applied: i64 = 0
194 pos = 0
195 while pos < cn {
196 let le2: i64 = cr_line_end(ct, pos, cn)
197 if cr_contains((((ct as i64) + pos)) as *u8, le2 - pos, CR_MARK) == 1 { removed = removed + 1 }
198 else {
199 kept = kept + 1
200 var k: i64 = pos
201 while k < le2 { out[w] = ct[k]; w = w + 1; k = k + 1 }
202 out[w] = 10 as u8
203 w = w + 1
204 }
205 pos = le2 + 1
206 }
207 pos = 0
208 while pos < rn {
209 let le3: i64 = cr_line_end(reg, pos, rn)
210 if le3 > pos { if (reg[pos] as i64) != 35 { if (reg[pos] as i64) != 13 {
211 var k2: i64 = pos
212 while k2 < le3 { out[w] = reg[k2]; w = w + 1; k2 = k2 + 1 }
213 out[w] = 10 as u8
214 w = w + 1
215 applied = applied + 1
216 } } }
217 pos = le3 + 1
218 }
219
220 // ---- G3: THE PARTITION MUST SUM, AND IT IS CHECKED BEFORE ANY BOOT FILE IS TOUCHED ---------
221 // Every row the registry DECLARED must be a row we APPLIED. The two counters walk the same buffer
222 // by the same rule, so today they cannot disagree -- which is exactly why asserting it costs
223 // nothing and why a later edit that breaks the correspondence cannot ship quietly.
224 // A PARTITION IS A CLAIM: CHECK THAT THE PARTS SUM, AND PRINT THEM EITHER WAY.
225 if applied != decl {
226 cr_p("REFUSED: partition does not reconcile -- declared=" as *u8); cr_pn(decl)
227 cr_p(" applied=" as *u8); cr_pn(applied)
228 cr_p(" -- refusing to write a crontab whose contents cannot be accounted for\n" as *u8)
229 return CR_EXIT_PARTITION
230 }
231
232 // R5: idempotence -- byte-identical means zero writes
233 var same: i64 = 0
234 if w == cn {
235 same = 1
236 var ci: i64 = 0
237 while ci < cn { if out[ci] != ct[ci] { same = 0; ci = cn } ci = ci + 1 }
238 }
239 if same == 1 {
240 // ts= ON THE QUIET PATH. A healthy reconciler emits NO-CHANGE almost every run, so this is the
241 // line that proves it is alive -- and without a timestamp nx_cron_watch cannot age it, which is
242 // why this organ sat UNWATCHED. It is the worst possible one to leave unwatched: its own registry
243 // header records that it failed silently TWICE (invoked argless, then as the wrong user), and each
244 // time every row added to cron.reg was inert for weeks while the file read like a live schedule.
245 // AN APPLIER THAT CANNOT BE AGED IS AN APPLIER NOBODY CAN TELL HAS STOPPED.
246 // EVERY NUMBER HERE HAS ITS DENOMINATOR BESIDE IT. The defect this organ shipped for two weeks
247 // was a bare `declared=55` with nothing to compare it against: no file size, no scan extent, no
248 // applied count. A reader had no way to ask "55 of what?" and so nobody did.
249 // **** A COUNT PUBLISHED WITHOUT ITS DENOMINATOR IS AN INVITATION TO TRUST IT. ****
250 // reg_scanned == reg_bytes-1 (the final newline) is the visible proof the parse reached the end.
251 cr_p("NO-CHANGE (crontab already matches cron.reg; declared=" as *u8)
252 cr_pn(decl)
253 cr_p(")" as *u8)
254 cr_p(" applied=" as *u8); cr_pn(applied)
255 cr_p(" kept=" as *u8); cr_pn(kept)
256 cr_p(" removed=" as *u8); cr_pn(removed)
257 cr_p(" reg_bytes=" as *u8); cr_pn(rn)
258 cr_p(" reg_scanned=" as *u8); cr_pn(scanned)
259 cr_p(" crontab_bytes=" as *u8); cr_pn(cn)
260 cr_p(" ts=" as *u8)
261 cr_pn(sys_now_realtime_sec())
262 cr_p("\n" as *u8)
263 return 0
264 }
265
266 // R6: atomic write -- <path>.nxnew then rename over
267 var npw: i64 = 0
268 var pi: i64 = 0
269 while ctpath[pi] != (0 as u8) { np[npw] = ctpath[pi]; npw = npw + 1; pi = pi + 1 }
270 let sfx: *u8 = ".nxnew" as *u8
271 var si: i64 = 0
272 while sfx[si] != (0 as u8) { np[npw] = sfx[si]; npw = npw + 1; si = si + 1 }
273 np[npw] = 0 as u8
274 let wfd: i64 = sys_openat_wr(np, 420)
275 if wfd < 0 { cr_p("WRITE-FAIL: cannot create .nxnew (permission?) -- crontab untouched\n" as *u8); return 6 }
276 var off: i64 = 0
277 while off < w {
278 let wr: i64 = sys_write(wfd, (((out as i64) + off)) as *u8, w - off)
279 if wr <= 0 { sys_close(wfd); cr_p("WRITE-FAIL: short write on .nxnew -- crontab untouched\n" as *u8); return 6 }
280 off = off + wr
281 }
282 sys_close(wfd)
283 if sys_renameat(np, ctpath) != 0 { cr_p("WRITE-FAIL: rename over crontab failed -- crontab untouched\n" as *u8); return 6 }
284 cr_p("CHANGED: nishi-rows removed=" as *u8)
285 cr_pn(removed)
286 cr_p(" declared=" as *u8)
287 cr_pn(decl)
288 cr_p(" applied=" as *u8)
289 cr_pn(applied)
290 cr_p(" kept=" as *u8)
291 cr_pn(kept)
292 cr_p(" reg_bytes=" as *u8)
293 cr_pn(rn)
294 cr_p(" reg_scanned=" as *u8)
295 cr_pn(scanned)
296 cr_p(" bytes " as *u8)
297 cr_pn(cn)
298 cr_p("->" as *u8)
299 cr_pn(w)
300 cr_p(" ts=" as *u8)
301 cr_pn(sys_now_realtime_sec())
302 cr_p("\n" as *u8)
303
304 // SIGHUP crond so the in-memory schedule reloads (safe standard reload; no-op if none found)
305 let dfd: i64 = sys_openat_rd("/proc\x00" as *u8)
306 if dfd >= 0 {
307 let dbuf: *u8 = sys_mmap(CR_MAGIC_65536)
308 let cpath: *u8 = sys_mmap(64)
309 let cbuf: *u8 = sys_mmap(32)
310 var hup: i64 = 0
311 var reading: i64 = 1
312 while reading == 1 {
313 let dn: i64 = sys_getdents64(dfd, dbuf, CR_MAGIC_65536)
314 if dn <= 0 { reading = 0 }
315 else {
316 var doff: i64 = 0
317 while doff < dn {
318 let recp: *u8 = (((dbuf as i64) + doff)) as *u8
319 let reclen: i64 = dirent_reclen(recp)
320 let nm: *u8 = dirent_name(recp)
321 if nm[0] >= (48 as u8) { if nm[0] <= (57 as u8) {
322 var qi: i64 = 0
323 let pre: *u8 = "/proc/" as *u8
324 var ppi: i64 = 0
325 while pre[ppi] != (0 as u8) { cpath[ppi] = pre[ppi]; ppi = ppi + 1 }
326 while nm[qi] != (0 as u8) { cpath[ppi] = nm[qi]; ppi = ppi + 1; qi = qi + 1 }
327 let suf: *u8 = "/comm" as *u8
328 var ssi: i64 = 0
329 while suf[ssi] != (0 as u8) { cpath[ppi] = suf[ssi]; ppi = ppi + 1; ssi = ssi + 1 }
330 cpath[ppi] = 0 as u8
331 let cfd: i64 = sys_openat_rd(cpath)
332 if cfd >= 0 {
333 let cl: i64 = sys_read(cfd, cbuf, 31)
334 sys_close(cfd)
335 // comm == "crond\n"
336 if cl >= 5 { if cbuf[0]==(99 as u8) { if cbuf[1]==(114 as u8) { if cbuf[2]==(111 as u8) { if cbuf[3]==(110 as u8) { if cbuf[4]==(100 as u8) {
337 var tail_ok: i64 = 0
338 if cl == 5 { tail_ok = 1 }
339 if cl == 6 { if (cbuf[5] as i64) == 10 { tail_ok = 1 } }
340 if tail_ok == 1 {
341 var pid: i64 = 0
342 var di: i64 = 0
343 while nm[di] != (0 as u8) { pid = pid * 10 + ((nm[di] as i64) - 48); di = di + 1 }
344 nx_kill(pid, 1)
345 hup = hup + 1
346 }
347 } } } } } }
348 }
349 } }
350 doff = doff + reclen
351 }
352 }
353 }
354 sys_close(dfd)
355 cr_p("crond HUP'd: " as *u8)
356 cr_pn(hup)
357 cr_p("\n" as *u8)
358 }
359 return 0
360}