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}