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}