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}