code wiki / _hdl_build / nx_routeguard_lib.nx

nx_routeguard_lib.nx source

↩ module page · 156 lines · 6585 B

1// nx_routeguard_lib.nx -- DEPLOY CONTRACT GUARD: does the candidate binary still serve every route 2// the live one does? 3// 4// THE DEFECT THIS EXISTS FOR (measured 2026-07-30, FIVE occurrences, one of them mine): 5// nx_mgmt_api has now lost /api/gate_run + /api/proc_kill from the LIVE binary five separate times 6// (21 routes -> 19). Mine happened because I built mgmt from a backdated tree and deployed a 528323-byte 7// artifact over a 575195-byte one -- a 47KB DECREASE that was visible at the time and that I did not 8// check. Every occurrence had the same shape: the source was fine, the BINARY silently lost surface, 9// and the deploy reported success. nx_route_diff already prints "route(s) vanished = deploy contract 10// regression" but is not wired into the deploy path, so nothing ever consulted it. 11// 12// WHY THIS GATE AND NOT LOAD-ADMISSION (self-correction, id=1785450386): my first instinct was to gate 13// deploys on host load the way /api/build is gated. That is WRONG and would deadlock the ecosystem -- 14// the repair for a saturated host IS a deploy, so a load-gated deploy refuses the fix BY THE CONDITION 15// IT REPAIRS. Route loss, by contrast, is ALWAYS wrong regardless of load, so this guard can never 16// block a repair. That property is the whole reason this is the right guard. 17// 18// DERIVED, NOT DECLARED: the expected route set is EXTRACTED FROM THE LIVE BINARY, never hardcoded. 19// A baked list would be the same declared-denominator defect already filed (id=1785446417) -- it would 20// go stale the moment a route is legitimately added, and it would fail OPEN. 21// 22// ASYMMETRIC BY DESIGN: candidate ADDING routes is fine (that is a normal feature deploy). Only 23// DISAPPEARANCE is a defect. So this is a SUPERSET check, not an equality check. 24// license_tier: ORIGINAL Read-only. No hw writes (Rule 26). 25import "nx_syscalls.nx" 26 27const RG_BUF: i64 = 4194304 // per-binary read cap 28const RG_MAXR: i64 = 128 // max distinct routes tracked 29const RG_NAMEMAX: i64 = 64 30const RG_NEEDLE: i64 = 5 // strlen("/api/") 31 32func rg_isroute_ch(c: i64) -> i64 { 33 if c >= 97 { if c <= 122 { return 1 } } 34 if c >= 65 { if c <= 90 { return 1 } } 35 if c >= 48 { if c <= 57 { return 1 } } 36 if c == 95 { return 1 } 37 if c == 47 { return 1 } 38 if c == 46 { return 1 } 39 return 0 40} 41 42// read a whole file into a REUSED bounded buffer. returns bytes, or -1 if unopenable. 43func rg_read(path: *u8, buf: *u8, cap: i64) -> i64 { 44 let fd: i64 = sys_openat_rd(path) 45 if fd < 0 { return 0 - 1 } 46 var n: i64 = 0 47 var go: i64 = 1 48 while go == 1 { 49 let r: i64 = sys_read(fd, (buf as i64 + n) as *u8, cap - n) 50 if r <= 0 { go = 0 } else { 51 n = n + r 52 if n >= cap { go = 0 } 53 } 54 } 55 sys_close(fd) 56 return n 57} 58 59// Does buf[0..n) contain the needle AS A WHOLE ROUTE TOKEN? 60// 61// BOUNDARY-CHECKED, and that is not pedantry -- it was a real hole caught by this organ's own gate. 62// A plain substring scan says /api/healthcheck SATISFIES /api/health, so RENAMING a route would pass 63// the guard silently: the old name vanishes from the surface while the check reports 0 missing. That 64// is the same class as the @priv lexer needing a trailing boundary so @privv could not match @priv. 65// A route is only "still served" if the literal ENDS where the live one ends. 66func rg_contains(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 { 67 if nl <= 0 { return 0 } 68 var i: i64 = 0 69 let last: i64 = n - nl 70 while i <= last { 71 var j: i64 = 0 72 var ok: i64 = 1 73 while j < nl { if buf[i + j] != needle[j] { ok = 0; j = nl } else { j = j + 1 } } 74 if ok == 1 { 75 let e: i64 = i + nl 76 var bounded: i64 = 1 77 if e < n { if rg_isroute_ch(buf[e] as i64) == 1 { bounded = 0 } } 78 if bounded == 1 { return 1 } 79 } 80 i = i + 1 81 } 82 return 0 83} 84 85// already collected? 86func rg_seen(names: *u8, lens: *i64, cnt: i64, p: *u8, l: i64) -> i64 { 87 var i: i64 = 0 88 while i < cnt { 89 if lens[i] == l { 90 let base: i64 = i * RG_NAMEMAX 91 var k: i64 = 0 92 var same: i64 = 1 93 while k < l { if names[base + k] != p[k] { same = 0; k = l } else { k = k + 1 } } 94 if same == 1 { return 1 } 95 } 96 i = i + 1 97 } 98 return 0 99} 100 101// EXTRACT the route surface from a binary image: every distinct "/api/..." literal it carries. 102// Bounded by RG_MAXR; the caller REPORTS truncation rather than silently guarding a subset. 103func rg_extract(buf: *u8, n: i64, names: *u8, lens: *i64, trunc: *i64) -> i64 { 104 trunc[0] = 0 105 var cnt: i64 = 0 106 var i: i64 = 0 107 let last: i64 = n - RG_NEEDLE 108 while i <= last { 109 var hit: i64 = 0 110 if buf[i] == (47 as u8) { if buf[i+1] == (97 as u8) { if buf[i+2] == (112 as u8) { 111 if buf[i+3] == (105 as u8) { if buf[i+4] == (47 as u8) { hit = 1 } } } } } 112 if hit == 1 { 113 var end: i64 = i 114 var go: i64 = 1 115 while go == 1 { 116 if end >= n { go = 0 } else { 117 if rg_isroute_ch(buf[end] as i64) == 1 { end = end + 1 } else { go = 0 } 118 } 119 } 120 let l: i64 = end - i 121 if l > RG_NEEDLE { 122 if l < RG_NAMEMAX { 123 if rg_seen(names, lens, cnt, (buf as i64 + i) as *u8, l) == 0 { 124 if cnt < RG_MAXR { 125 let base: i64 = cnt * RG_NAMEMAX 126 var k: i64 = 0 127 while k < l { names[base + k] = buf[i + k]; k = k + 1 } 128 names[base + l] = 0 as u8 129 lens[cnt] = l 130 cnt = cnt + 1 131 } else { trunc[0] = trunc[0] + 1 } 132 } 133 } 134 } 135 i = end 136 } else { i = i + 1 } 137 } 138 return cnt 139} 140 141// THE VERDICT. Returns the number of routes present in LIVE but MISSING from CANDIDATE. 142// 0 = the candidate is a superset = safe to promote on this axis. 143// Missing route indices are written into miss[] (bounded by misscap). 144func rg_missing(lnames: *u8, llens: *i64, lcnt: i64, cbuf: *u8, cn: i64, miss: *i64, misscap: i64) -> i64 { 145 var m: i64 = 0 146 var i: i64 = 0 147 while i < lcnt { 148 let base: i64 = i * RG_NAMEMAX 149 if rg_contains(cbuf, cn, (lnames as i64 + base) as *u8, llens[i]) == 0 { 150 if m < misscap { miss[m] = i } 151 m = m + 1 152 } 153 i = i + 1 154 } 155 return m 156}