code wiki / _hdl_build / nx_mgmt_upload_gate.nx
nx_mgmt_upload_gate.nx source
↩ module page · 327 lines · 16477 B
1// nx_mgmt_upload_gate.nx -- SOVEREIGN in-process referee for the /api/upload keystone (chunked artifact publish).
2// NO socket, NO curl, NO shell: it chdir's to /tmp (so every staging file lands there, NOT in the real build/NAS
3// dir), then feeds crafted HTTP request BYTES straight into the PURE handler ma_do_upload and asserts BOTH the
4// response bytes AND the on-disk result. Proves the reassembly + monotonic-seq gate + allowlist + sha256 + the
5// atomic .upload->.new stage, all fail-closed. Because ma_do_upload composes the shipped mu_stage_chunk write
6// primitive, this end-to-end gate also transitively covers it (retiring the old isolated primitive-only gate).
7// The target names are the REAL allowlisted basenames (exercising md_upload_target_ok for real), rooted in /tmp
8// so nothing live is ever touched.
9// T1 multi-chunk happy path: seq0(final=0)+seq1(final=1) -> <t>.new == chunkA ++ chunkB, BYTE-EXACT
10// T2 NEG unknown target -> 400, nothing written
11// T3 NEG seq gap (seq0 then seq2) -> 400, .new NOT produced
12// T4 NEG final without a prior seq0 (fresh target) -> 400 (bad seq), .new NOT produced
13// T5 sha256 mismatch on final -> 400 + staging (.upload) DELETED, .new NOT produced
14// T6 sha256 MATCH on final -> 200 + .new produced BYTE-EXACT (positive control for the hash path)
15// T7 VIDEO content namespace (2026-07-11): sites/nishifamily/video/*.js uploads + stages; html/wasm/ver.txt validate
16// T8 video namespace fail-closed: .elf ext, ".." traversal, dot-file, prefix-miss ALL refused (validator + handler)
17// GREEN iff T1..T8 hold. Sovereign: nx_mgmt_api (-> ma_do_upload -> mu_stage_chunk) + nx_sha256 + nx_fio + nx_syscalls.
18// license_tier: ORIGINAL expect_exit: 0
19import "nx_mgmt_api.nx"
20import "nx_sha256.nx"
21import "nx_fio.nx"
22import "nx_syscalls.nx"
23import "nx_gate_verdict.nx"
24
25func u_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
26func u_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
27func u_row(name: *u8, ok: i64) -> i64 {
28 if ok == 1 { u_w(" PASS " as *u8) } else { u_w(" FAIL " as *u8) }
29 u_w(name); u_w("\n" as *u8)
30 return ok
31}
32func u_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o + i] = s[i]; i = i + 1 } return o + i }
33func u_catn(d: *u8, o: i64, v: i64) -> i64 {
34 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0
35 var w: i64 = o
36 if m < 0 { d[w] = 45 as u8; w = w + 1; m = 0 - m }
37 if m == 0 { t[0] = 48 as u8; k = 1 }
38 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
39 var i: i64 = 0
40 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 }
41 return w
42}
43func u_starts(buf: *u8, n: i64, s: *u8) -> i64 {
44 let sn: i64 = u_len(s)
45 if n < sn { return 0 }
46 var i: i64 = 0
47 while i < sn { if (buf[i] as i64) != (s[i] as i64) { return 0 } i = i + 1 }
48 return 1
49}
50func u_contains(hay: *u8, n: i64, needle: *u8) -> i64 {
51 let nn: i64 = u_len(needle)
52 var i: i64 = 0
53 while i + nn <= n {
54 var m: i64 = 1
55 var j: i64 = 0
56 while j < nn { if (hay[i + j] as i64) != (needle[j] as i64) { m = 0; j = nn } else { j = j + 1 } }
57 if m == 1 { return 1 }
58 i = i + 1
59 }
60 return 0
61}
62
63// build a POST /api/upload request with the given query string + raw chunk body into req; returns total length.
64// qs is a NUL-terminated query string WITHOUT the leading '?', e.g. "target=x&seq=0&final=0".
65func u_build(req: *u8, qs: *u8, chunk: *u8, clen: i64) -> i64 {
66 var o: i64 = 0
67 o = u_cat(req, o, "POST /api/upload?" as *u8)
68 o = u_cat(req, o, qs)
69 o = u_cat(req, o, " HTTP/1.1\r\nHost: x\r\nContent-Type: application/octet-stream\r\nContent-Length: " as *u8)
70 o = u_catn(req, o, clen)
71 o = u_cat(req, o, "\r\n\r\n" as *u8)
72 var i: i64 = 0
73 while i < clen { req[o] = chunk[i]; o = o + 1; i = i + 1 }
74 return o
75}
76
77// read a whole file into out (cap). Returns bytes read, or -1 if the file can't be opened (i.e. absent).
78func u_read_file(path: *u8, out: *u8, cap: i64) -> i64 {
79 let fd: i64 = sys_openat_rd(path)
80 if fd < 0 { return 0 - 1 }
81 var off: i64 = 0
82 var go: i64 = 1
83 while go == 1 {
84 if off >= cap { go = 0 } else {
85 let r: i64 = sys_read(fd, ((out as i64) + off) as *u8, cap - off)
86 if r <= 0 { go = 0 } else { off = off + r }
87 }
88 }
89 sys_close(fd)
90 return off
91}
92
93// 1 if a[0..an) equals b[0..bn) byte-for-byte, else 0.
94func u_bytes_eq(a: *u8, an: i64, b: *u8, bn: i64) -> i64 {
95 if an != bn { return 0 }
96 var i: i64 = 0
97 while i < an { if (a[i] as i64) != (b[i] as i64) { return 0 } i = i + 1 }
98 return 1
99}
100
101// lowercase-hex of a sha256 of bytes[0..n) into out[0..64) (NUL-terminated). Independent recompute for the gate.
102func u_sha256_hex(bytes: *u8, n: i64, out: *u8) -> i64 {
103 let dig: *u8 = sys_mmap(32)
104 sha256_digest(bytes, n, dig)
105 let hx: *u8 = "0123456789abcdef" as *u8
106 var i: i64 = 0
107 while i < 32 {
108 let b: i64 = dig[i] & 0xff
109 out[i * 2] = hx[(b >> 4) & 0xf]
110 out[i * 2 + 1] = hx[b & 0xf]
111 i = i + 1
112 }
113 out[64] = 0 as u8
114 return 64
115}
116
117func main() -> i64 {
118 u_w("mgmt-upload SOVEREIGN in-process gate (no socket/curl/shell -- ma_do_upload bytes-in + on-disk assert)\n" as *u8)
119
120 // Root every staging file in /tmp so we never touch the real build dir / NAS. ma_do_upload writes
121 // <target>.upload / <target>.new / <target>.upload.seq relative to CWD, so chdir(/tmp) contains them.
122 if sys_chdir("/tmp" as *u8) != 0 { u_w("CHDIR /tmp FAIL\n" as *u8); sys_exit(1) }
123
124 let req: *u8 = sys_mmap(262144)
125 let out: *u8 = sys_mmap(SD_OUTCAP)
126 var pass: i64 = 0
127
128 // deterministic distinct chunk payloads
129 let cA: *u8 = sys_mmap(4096)
130 let cB: *u8 = sys_mmap(4096)
131 var i: i64 = 0
132 while i < 4096 { cA[i] = (65 + (i % 26)) as u8; i = i + 1 } // 'A'..'Z' repeating
133 i = 0
134 while i < 4096 { cB[i] = (48 + (i % 10)) as u8; i = i + 1 } // '0'..'9' repeating
135 let cAn: i64 = 4096
136 let cBn: i64 = 2048 // second chunk a different length (proves offset math)
137
138 // the concatenation we expect on disk after T1/T6
139 let want: *u8 = sys_mmap(8192)
140 var wn: i64 = 0
141 var k: i64 = 0
142 while k < cAn { want[wn] = cA[k]; wn = wn + 1; k = k + 1 }
143 k = 0
144 while k < cBn { want[wn] = cB[k]; wn = wn + 1; k = k + 1 }
145
146 let disk: *u8 = sys_mmap(16384)
147
148 // ------------------------------------------------------------------ T1: multi-chunk happy path ----
149 fio_unlink("nx_mgmt_api.elf.upload" as *u8)
150 fio_unlink("nx_mgmt_api.elf.upload.seq" as *u8)
151 fio_unlink("nx_mgmt_api.elf.new" as *u8)
152
153 var n1a: i64 = u_build(req, "target=nx_mgmt_api.elf&seq=0&final=0" as *u8, cA, cAn)
154 let o1a: i64 = ma_do_upload(req, n1a, out)
155 var s1a: i64 = 0
156 if u_starts(out, o1a, "HTTP/1.1 200" as *u8) == 1 { if u_contains(out, o1a, "\"final\":0" as *u8) == 1 { s1a = 1 } }
157
158 var n1b: i64 = u_build(req, "target=nx_mgmt_api.elf&seq=1&final=1" as *u8, cB, cBn)
159 let o1b: i64 = ma_do_upload(req, n1b, out)
160 var s1b: i64 = 0
161 if u_starts(out, o1b, "HTTP/1.1 200" as *u8) == 1 {
162 if u_contains(out, o1b, "\"staged\":\"nx_mgmt_api.elf.new\"" as *u8) == 1 {
163 if u_contains(out, o1b, "\"bytes\":6144" as *u8) == 1 { s1b = 1 }
164 }
165 }
166 let dn: i64 = u_read_file("nx_mgmt_api.elf.new" as *u8, disk, 16384)
167 var s1c: i64 = 0
168 if dn == wn { if u_bytes_eq(disk, dn, want, wn) == 1 { s1c = 1 } }
169 var t1: i64 = 0
170 if s1a == 1 { if s1b == 1 { if s1c == 1 { t1 = 1 } } }
171 pass = pass + u_row("T1 multi-chunk seq0+seq1(final) -> .new == chunkA++chunkB BYTE-EXACT\x00" as *u8, t1)
172
173 // ------------------------------------------------------------------ T2: NEG unknown target ----
174 fio_unlink("bogus_not_allowlisted.upload" as *u8)
175 fio_unlink("bogus_not_allowlisted.new" as *u8)
176 var n2: i64 = u_build(req, "target=bogus_not_allowlisted&seq=0&final=1" as *u8, cA, cAn)
177 let o2: i64 = ma_do_upload(req, n2, out)
178 var t2: i64 = 0
179 if u_starts(out, o2, "HTTP/1.1 400" as *u8) == 1 {
180 if u_contains(out, o2, "not allowlisted" as *u8) == 1 {
181 if u_read_file("bogus_not_allowlisted.upload" as *u8, disk, 16384) < 0 {
182 if u_read_file("bogus_not_allowlisted.new" as *u8, disk, 16384) < 0 { t2 = 1 }
183 }
184 }
185 }
186 pass = pass + u_row("T2 NEG unknown target -> 400 + nothing written\x00" as *u8, t2)
187
188 // ------------------------------------------------------------------ T3: NEG seq gap (0 then 2) ----
189 fio_unlink("sites.elf.upload" as *u8)
190 fio_unlink("sites.elf.upload.seq" as *u8)
191 fio_unlink("sites.elf.new" as *u8)
192 var n3a: i64 = u_build(req, "target=sites.elf&seq=0&final=0" as *u8, cA, cAn)
193 ma_do_upload(req, n3a, out) // seq0 ok, expected-next=1
194 var n3b: i64 = u_build(req, "target=sites.elf&seq=2&final=1" as *u8, cB, cBn) // GAP: skips seq 1
195 let o3b: i64 = ma_do_upload(req, n3b, out)
196 var t3: i64 = 0
197 if u_starts(out, o3b, "HTTP/1.1 400" as *u8) == 1 {
198 if u_contains(out, o3b, "bad seq" as *u8) == 1 {
199 if u_read_file("sites.elf.new" as *u8, disk, 16384) < 0 { t3 = 1 }
200 }
201 }
202 pass = pass + u_row("T3 NEG seq gap (0 then 2) -> 400 + .new NOT produced\x00" as *u8, t3)
203
204 // ------------------------------------------------------------------ T4: NEG final w/o prior seq0 ----
205 fio_unlink("nx_gallery_serve.elf.upload" as *u8)
206 fio_unlink("nx_gallery_serve.elf.upload.seq" as *u8)
207 fio_unlink("nx_gallery_serve.elf.new" as *u8)
208 var n4: i64 = u_build(req, "target=nx_gallery_serve.elf&seq=1&final=1" as *u8, cA, cAn)
209 let o4: i64 = ma_do_upload(req, n4, out)
210 var t4: i64 = 0
211 if u_starts(out, o4, "HTTP/1.1 400" as *u8) == 1 {
212 if u_contains(out, o4, "bad seq" as *u8) == 1 {
213 if u_read_file("nx_gallery_serve.elf.new" as *u8, disk, 16384) < 0 { t4 = 1 }
214 }
215 }
216 pass = pass + u_row("T4 NEG final without prior seq0 -> 400 (fail-closed) + no .new\x00" as *u8, t4)
217
218 // ------------------------------------------------------------------ T5: sha256 MISMATCH ----
219 fio_unlink("nx_hostctl.upload" as *u8)
220 fio_unlink("nx_hostctl.upload.seq" as *u8)
221 fio_unlink("nx_hostctl.new" as *u8)
222 var n5: i64 = u_build(req, "target=nx_hostctl&seq=0&final=1&sha256=0000000000000000000000000000000000000000000000000000000000000000" as *u8, cA, cAn)
223 let o5: i64 = ma_do_upload(req, n5, out)
224 var t5: i64 = 0
225 if u_starts(out, o5, "HTTP/1.1 400" as *u8) == 1 {
226 if u_contains(out, o5, "sha256 mismatch" as *u8) == 1 {
227 if u_read_file("nx_hostctl.upload" as *u8, disk, 16384) < 0 {
228 if u_read_file("nx_hostctl.new" as *u8, disk, 16384) < 0 { t5 = 1 }
229 }
230 }
231 }
232 pass = pass + u_row("T5 sha256 mismatch -> 400 + staging deleted + no .new\x00" as *u8, t5)
233
234 // ------------------------------------------------------------------ T6: sha256 MATCH (positive control) ----
235 fio_unlink("nx_wiki_gw.elf.upload" as *u8)
236 fio_unlink("nx_wiki_gw.elf.upload.seq" as *u8)
237 fio_unlink("nx_wiki_gw.elf.new" as *u8)
238 let hexbuf: *u8 = sys_mmap(72)
239 u_sha256_hex(want, wn, hexbuf)
240 var n6a: i64 = u_build(req, "target=nx_wiki_gw.elf&seq=0&final=0" as *u8, cA, cAn)
241 ma_do_upload(req, n6a, out)
242 let qs6: *u8 = sys_mmap(256)
243 var q: i64 = u_cat(qs6, 0, "target=nx_wiki_gw.elf&seq=1&final=1&sha256=" as *u8)
244 var hi: i64 = 0
245 while hi < 64 { qs6[q] = hexbuf[hi]; q = q + 1; hi = hi + 1 }
246 qs6[q] = 0 as u8
247 var n6b: i64 = u_build(req, qs6, cB, cBn)
248 let o6b: i64 = ma_do_upload(req, n6b, out)
249 var t6: i64 = 0
250 if u_starts(out, o6b, "HTTP/1.1 200" as *u8) == 1 {
251 if u_contains(out, o6b, "\"staged\":\"nx_wiki_gw.elf.new\"" as *u8) == 1 {
252 let dn6: i64 = u_read_file("nx_wiki_gw.elf.new" as *u8, disk, 16384)
253 if dn6 == wn { if u_bytes_eq(disk, dn6, want, wn) == 1 { t6 = 1 } }
254 }
255 }
256 pass = pass + u_row("T6 sha256 MATCH -> 200 + .new produced BYTE-EXACT (positive control)\x00" as *u8, t6)
257
258 // ------------------------------------------- T7: VIDEO content namespace (2026-07-11) positive ----
259 // sites/nishifamily/video/* + .js/.wasm now ride /api/upload -> /api/promote_content (retires the
260 // nx_aw_send ssh push of the codec client). Full-handler proof for .js; pure-validator proof for the
261 // rest of the client set (index.html / nx_video_client.wasm / ver.txt).
262 fio_unlink("sites/nishifamily/video/app.v2.js.upload" as *u8)
263 fio_unlink("sites/nishifamily/video/app.v2.js.upload.seq" as *u8)
264 fio_unlink("sites/nishifamily/video/app.v2.js.new" as *u8)
265 var n7: i64 = u_build(req, "target=sites/nishifamily/video/app.v2.js&seq=0&final=1" as *u8, cA, cAn)
266 let o7: i64 = ma_do_upload(req, n7, out)
267 var t7: i64 = 0
268 if u_starts(out, o7, "HTTP/1.1 200" as *u8) == 1 {
269 let dn7: i64 = u_read_file("sites/nishifamily/video/app.v2.js.new" as *u8, disk, 16384)
270 if dn7 == cAn { if u_bytes_eq(disk, dn7, cA, cAn) == 1 {
271 let v1: *u8 = "sites/nishifamily/video/index.html" as *u8
272 let v2: *u8 = "sites/nishifamily/video/nx_video_client.wasm" as *u8
273 let v3: *u8 = "sites/nishifamily/video/ver.txt" as *u8
274 if md_content_target_ok(v1, 0, u_len(v1)) == 1 {
275 if md_content_target_ok(v2, 0, u_len(v2)) == 1 {
276 if md_content_target_ok(v3, 0, u_len(v3)) == 1 { t7 = 1 }
277 }
278 }
279 } }
280 }
281 pass = pass + u_row("T7 video namespace: .js upload staged BYTE-EXACT + html/wasm/ver.txt validate\x00" as *u8, t7)
282
283 // ------------------------------------------- T8: video namespace stays FAIL-CLOSED ----
284 // a binary ext, a ".." traversal, a dot-file, and a prefix miss must ALL refuse (validator + full handler).
285 var t8: i64 = 0
286 let b1: *u8 = "sites/nishifamily/video/x.elf" as *u8
287 let b2: *u8 = "sites/nishifamily/video/../../../etc/passwd.js" as *u8
288 let b3: *u8 = "sites/nishifamily/video/.hidden.js" as *u8
289 let b4: *u8 = "sites/nishifamilyx/video/a.js" as *u8
290 if md_content_target_ok(b1, 0, u_len(b1)) == 0 {
291 if md_content_target_ok(b2, 0, u_len(b2)) == 0 {
292 if md_content_target_ok(b3, 0, u_len(b3)) == 0 {
293 if md_content_target_ok(b4, 0, u_len(b4)) == 0 {
294 var n8: i64 = u_build(req, "target=sites/nishifamily/video/x.elf&seq=0&final=1" as *u8, cA, cAn)
295 let o8: i64 = ma_do_upload(req, n8, out)
296 if u_starts(out, o8, "HTTP/1.1 400" as *u8) == 1 {
297 if u_contains(out, o8, "not allowlisted" as *u8) == 1 { t8 = 1 }
298 }
299 }
300 }
301 }
302 }
303 pass = pass + u_row("T8 video namespace fail-closed: .elf + traversal + dotfile + prefix-miss refused\x00" as *u8, t8)
304
305 // ------------------------------------------------------------------ cleanup (leave /tmp tidy) ----
306 fio_unlink("sites/nishifamily/video/app.v2.js.new" as *u8)
307 fio_unlink("sites/nishifamily/video/app.v2.js.upload" as *u8)
308 fio_unlink("sites/nishifamily/video/app.v2.js.upload.seq" as *u8)
309 fio_unlink("nx_mgmt_api.elf.new" as *u8)
310 fio_unlink("nx_mgmt_api.elf.upload" as *u8)
311 fio_unlink("nx_mgmt_api.elf.upload.seq" as *u8)
312 fio_unlink("sites.elf.upload" as *u8)
313 fio_unlink("sites.elf.upload.seq" as *u8)
314 fio_unlink("nx_wiki_gw.elf.new" as *u8)
315 fio_unlink("nx_wiki_gw.elf.upload.seq" as *u8)
316
317 u_w("pass=" as *u8); let pb: *u8 = sys_mmap(8); let pk: i64 = u_catn(pb, 0, pass); sys_write(1, pb, pk); u_w("/8\n" as *u8)
318 // MIGRATED onto nx_gate_verdict by nx_gate_dry_apply (D001, minimal form): every check
319 // row above is untouched, so the PASS/FAIL vector cannot change; only the hand-rolled
320 // verdict emission is replaced by the ONE shared base class. Proven by nx_gate_migrate verify.
321 let ctr__dry: *i64 = gv_ctr()
322 ctr__dry[0] = pass
323 ctr__dry[1] = 8
324 let rc__dry: i64 = gv_verdict("MGMT-UPLOAD-GATE" as *u8, ctr__dry, "chunked publish: reassembly + monotonic-seq + allowlist + sha256 + video content namespace, all fail-closed, staging-only never-brick)" as *u8)
325 sys_exit(rc__dry)
326 return rc__dry
327}