code wiki / _hdl_build / nx_routeguard_candidate_t184.nx
nx_routeguard_candidate_t184.nx source
↩ module page · 187 lines · 7576 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
102// Binary help text is weak route evidence. A sentence-ending period is a
103// redundant prose alias only when a separate exact base-route literal exists.
104// Quoted/NUL-delimited dotted routes and ambiguous cases remain guarded.
105func rg_prose_period_alias(buf:*u8,n:i64,start:i64,end:i64)->i64{
106 if start<=0 || end<=start+RG_NEEDLE{return 0}
107 if buf[end-1]!=(46 as u8){return 0}
108 let before:i64=buf[start-1] as i64
109 if before!=32 && before!=9 && before!=10 && before!=13{return 0}
110 if end<n{
111 let after:i64=buf[end] as i64
112 if after!=0 && after!=32 && after!=9 && after!=10 && after!=13{return 0}
113 }
114 let len:i64=end-start-1
115 var p:i64=0
116 while p+len<n{
117 var left:i64=0
118 if p==0{left=1}else{if buf[p-1]==(0 as u8) || buf[p-1]==(34 as u8){left=1}}
119 if left==1{
120 let right:i64=buf[p+len] as i64
121 if right==0 || right==34{
122 var j:i64=0
123 while j<len && buf[p+j]==buf[start+j]{j=j+1}
124 if j==len{return 1}
125 }
126 }
127 p=p+1
128 }
129 return 0
130}
131
132// EXTRACT the route surface from a binary image: every distinct "/api/..." literal it carries.
133// Bounded by RG_MAXR; the caller REPORTS truncation rather than silently guarding a subset.
134func rg_extract(buf: *u8, n: i64, names: *u8, lens: *i64, trunc: *i64) -> i64 {
135 trunc[0] = 0
136 var cnt: i64 = 0
137 var i: i64 = 0
138 let last: i64 = n - RG_NEEDLE
139 while i <= last {
140 var hit: i64 = 0
141 if buf[i] == (47 as u8) { if buf[i+1] == (97 as u8) { if buf[i+2] == (112 as u8) {
142 if buf[i+3] == (105 as u8) { if buf[i+4] == (47 as u8) { hit = 1 } } } } }
143 if hit == 1 {
144 var end: i64 = i
145 var go: i64 = 1
146 while go == 1 {
147 if end >= n { go = 0 } else {
148 if rg_isroute_ch(buf[end] as i64) == 1 { end = end + 1 } else { go = 0 }
149 }
150 }
151 let l: i64 = end - i
152 if l > RG_NEEDLE && rg_prose_period_alias(buf,n,i,end)==0 {
153 if l < RG_NAMEMAX {
154 if rg_seen(names, lens, cnt, (buf as i64 + i) as *u8, l) == 0 {
155 if cnt < RG_MAXR {
156 let base: i64 = cnt * RG_NAMEMAX
157 var k: i64 = 0
158 while k < l { names[base + k] = buf[i + k]; k = k + 1 }
159 names[base + l] = 0 as u8
160 lens[cnt] = l
161 cnt = cnt + 1
162 } else { trunc[0] = trunc[0] + 1 }
163 }
164 }
165 }
166 i = end
167 } else { i = i + 1 }
168 }
169 return cnt
170}
171
172// THE VERDICT. Returns the number of routes present in LIVE but MISSING from CANDIDATE.
173// 0 = the candidate is a superset = safe to promote on this axis.
174// Missing route indices are written into miss[] (bounded by misscap).
175func rg_missing(lnames: *u8, llens: *i64, lcnt: i64, cbuf: *u8, cn: i64, miss: *i64, misscap: i64) -> i64 {
176 var m: i64 = 0
177 var i: i64 = 0
178 while i < lcnt {
179 let base: i64 = i * RG_NAMEMAX
180 if rg_contains(cbuf, cn, (lnames as i64 + base) as *u8, llens[i]) == 0 {
181 if m < misscap { miss[m] = i }
182 m = m + 1
183 }
184 i = i + 1
185 }
186 return m
187}