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