code wiki / _hdl_build / nx_mgmt_promote_gate.nx
nx_mgmt_promote_gate.nx source
↩ module page · 182 lines · 10664 B
1// nx_mgmt_promote_gate.nx -- prove POST /api/promote (organ ELF) is fail-closed + never-brick. In-process
2// referee (no socket): crafts request BYTES and feeds the pure handler ma_do_promote. Auth is the SAME
3// ma_level_of gate as every other write route (proven in nx_mgmt_api_gate), so this focuses on the NEW logic:
4// T1 NEG daemon target ("sites") -> 400 allowlist, staged file UNTOUCHED (daemons must use /api/deploy)
5// T2 NEG path-escape ("../etc/x") -> 400 invalid name (sanitize refuses)
6// T3 NEG missing staged (allowlisted, no .sov.elf.new) -> 400 no-valid-staged, no brick
7// T4 NEG non-ELF staged (garbage bytes) -> 400 must-be-ELF, live UNTOUCHED (never promote corruption)
8// T5 NEG no confirm=yes -> 400, staged UNTOUCHED
9// T6 POS promote -> live == the new ELF, <t>.elf.prev == the old live (never-brick backup), staged consumed
10// Runs FS under /tmp (WSL-native ext4-like) to avoid 9p flakiness. license_tier: ORIGINAL expect_exit: 0
11import "nx_mgmt_api.nx"
12import "nx_syscalls.nx"
13
14func w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
15func pn(v: i64) -> i64 {
16 let b: *u8 = sys_mmap(32) as *u8
17 var x: i64 = v; var neg: i64 = 0
18 if x < 0 { neg = 1; x = 0 - x }
19 var i: i64 = 31
20 if x == 0 { b[i] = 48 as u8; i = i - 1 }
21 while x > 0 { b[i] = (48 + x % 10) as u8; x = x / 10; i = i - 1 }
22 if neg == 1 { b[i] = 45 as u8; i = i - 1 }
23 sys_write(1, (b as i64 + i + 1) as *u8, 31 - i)
24 return 0
25}
26func glen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
27func gcat(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 }
28func gcatn(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64=0; while i<n {d[o+i]=s[i]; i=i+1} return o+n }
29func gcontains(hay: *u8, n: i64, needle: *u8) -> i64 {
30 let nn: i64 = glen(needle)
31 var i: i64 = 0
32 while i + nn <= n {
33 var m: i64 = 1; var j: i64 = 0
34 while j < nn { if (hay[i+j] as i64) != (needle[j] as i64) { m = 0; j = nn } else { j = j + 1 } }
35 if m == 1 { return 1 }
36 i = i + 1
37 }
38 return 0
39}
40// build "POST <line> HTTP/1.1\r\n...\r\n\r\n<body>" into dst; returns total length.
41func mkreq(dst: *u8, line: *u8, body: *u8) -> i64 {
42 let bn: i64 = glen(body)
43 var o: i64 = gcat(dst, 0, line)
44 o = gcat(dst, o, " HTTP/1.1\r\nHost: x\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8)
45 let t: *u8 = sys_mmap(24); var m: i64 = bn; var k: i64 = 0
46 if m == 0 { t[0]=48 as u8; k=1 }
47 while m > 0 { t[k]=(48+(m%10)) as u8; m=m/10; k=k+1 }
48 var z: i64 = 0
49 while z < k { dst[o]=t[k-1-z]; o=o+1; z=z+1 }
50 o = gcat(dst, o, "\r\n\r\n" as *u8)
51 o = gcatn(dst, o, body, bn)
52 return o
53}
54// write n bytes to path (create/truncate, mode 0644).
55func wfile(path: *u8, buf: *u8, n: i64) -> i64 {
56 let fd: i64 = sys_openat_wr(path, 0x1a4)
57 if fd < 0 { return 0 - 1 }
58 sys_write(fd, buf, n)
59 sys_close(fd)
60 return 0
61}
62// 1 iff the file exists and its first n bytes == want[0..n].
63func bytes_eq(path: *u8, want: *u8, wn: i64) -> i64 {
64 let szp: *i64 = sys_mmap(16) as *i64
65 let b: *u8 = sys_read_file(path, szp)
66 if (b as i64) == 0 { return 0 }
67 if szp[0] != wn { return 0 }
68 var i: i64 = 0
69 while i < wn { if (b[i] as i64) != (want[i] as i64) { return 0 } i = i + 1 }
70 return 1
71}
72func file_absent(path: *u8) -> i64 {
73 let fd: i64 = sys_openat_rd(path)
74 if fd < 0 { return 1 }
75 sys_close(fd)
76 return 0
77}
78
79func main() -> i64 {
80 w("=== nx_mgmt_promote_gate -- POST /api/promote organ ELF, fail-closed + never-brick ===\n" as *u8)
81 sys_mkdir("/tmp/nx_promote_gt" as *u8, 0x1ed)
82 sys_chdir("/tmp/nx_promote_gt" as *u8)
83 let out: *u8 = sys_mmap(262144)
84 let req: *u8 = sys_mmap(262144)
85 var fails: i64 = 0
86
87 // a valid ELF header (magic 0x7f 'E' 'L' 'F' + a few bytes) and a distinct "old live" + "garbage".
88 let elfnew: *u8 = sys_mmap(16)
89 elfnew[0]=0x7f as u8; elfnew[1]=69 as u8; elfnew[2]=76 as u8; elfnew[3]=70 as u8
90 elfnew[4]=78 as u8; elfnew[5]=69 as u8; elfnew[6]=87 as u8 // "NEW"
91 let oldlive: *u8 = sys_mmap(16)
92 oldlive[0]=79 as u8; oldlive[1]=76 as u8; oldlive[2]=68 as u8 // "OLD"
93 let garbage: *u8 = sys_mmap(16)
94 garbage[0]=71 as u8; garbage[1]=65 as u8; garbage[2]=82 as u8 // "GAR" (not an ELF)
95
96 // clean slate
97 fio_unlink("sites.sov.elf.new" as *u8); fio_unlink("sites.elf" as *u8)
98 fio_unlink("nx_ecomat_beat.sov.elf.new" as *u8); fio_unlink("nx_ecomat_beat.elf" as *u8)
99 fio_unlink("nx_tool_argecho.sov.elf.new" as *u8); fio_unlink("nx_tool_argecho.elf" as *u8); fio_unlink("nx_tool_argecho.elf.prev" as *u8)
100
101 // T1 NEG: a DAEMON target must be refused by the allowlist, staged file left untouched.
102 wfile("sites.sov.elf.new" as *u8, elfnew, 7)
103 let n1: i64 = mkreq(req, "POST /api/promote" as *u8, "target=sites&confirm=yes" as *u8)
104 let r1: i64 = ma_do_promote(req, n1, out)
105 var t1: i64 = 1
106 if gcontains(out, r1, "400" as *u8) == 0 { t1 = 0 }
107 if gcontains(out, r1, "allowlist" as *u8) == 0 { t1 = 0 }
108 if bytes_eq("sites.sov.elf.new" as *u8, elfnew, 7) == 0 { t1 = 0 } // still staged, NOT promoted
109 if file_absent("sites.elf" as *u8) == 0 { t1 = 0 } // no live daemon binary created
110 if t1 == 1 { w("T1 PASS daemon target refused (allowlist), staged untouched\n" as *u8) } else { fails = fails + 1; w("T1 FAIL daemon guard\n" as *u8); sys_write(1, out, r1); w("\n" as *u8) }
111
112 // T2 NEG: path escape -> sanitize refuses.
113 let n2: i64 = mkreq(req, "POST /api/promote" as *u8, "target=../etc/x&confirm=yes" as *u8)
114 let r2: i64 = ma_do_promote(req, n2, out)
115 var t2: i64 = 1
116 if gcontains(out, r2, "400" as *u8) == 0 { t2 = 0 }
117 if gcontains(out, r2, "invalid target name" as *u8) == 0 { t2 = 0 }
118 if t2 == 1 { w("T2 PASS path-escape refused (sanitize)\n" as *u8) } else { fails = fails + 1; w("T2 FAIL path-escape\n" as *u8); sys_write(1, out, r2); w("\n" as *u8) }
119
120 // T3 NEG: allowlisted name but NO staged .sov.elf.new -> refuse, no brick.
121 let n3: i64 = mkreq(req, "POST /api/promote" as *u8, "target=nx_ecomat_seed&confirm=yes" as *u8)
122 let r3: i64 = ma_do_promote(req, n3, out)
123 var t3: i64 = 1
124 if gcontains(out, r3, "400" as *u8) == 0 { t3 = 0 }
125 if gcontains(out, r3, "no valid staged" as *u8) == 0 { t3 = 0 }
126 if t3 == 1 { w("T3 PASS missing-staged refused\n" as *u8) } else { fails = fails + 1; w("T3 FAIL missing-staged\n" as *u8); sys_write(1, out, r3); w("\n" as *u8) }
127
128 // T4 NEG: staged file is NOT an ELF -> refuse, live untouched (never promote corruption).
129 wfile("nx_ecomat_beat.sov.elf.new" as *u8, garbage, 3)
130 wfile("nx_ecomat_beat.elf" as *u8, oldlive, 3)
131 let n4: i64 = mkreq(req, "POST /api/promote" as *u8, "target=nx_ecomat_beat&confirm=yes" as *u8)
132 let r4: i64 = ma_do_promote(req, n4, out)
133 var t4: i64 = 1
134 if gcontains(out, r4, "400" as *u8) == 0 { t4 = 0 }
135 if gcontains(out, r4, "must be an ELF" as *u8) == 0 { t4 = 0 }
136 if bytes_eq("nx_ecomat_beat.elf" as *u8, oldlive, 3) == 0 { t4 = 0 } // live still the OLD one
137 if t4 == 1 { w("T4 PASS non-ELF staged refused, live untouched\n" as *u8) } else { fails = fails + 1; w("T4 FAIL non-ELF guard\n" as *u8); sys_write(1, out, r4); w("\n" as *u8) }
138
139 // T5 NEG: valid staged ELF but NO confirm=yes -> refuse, staged untouched.
140 wfile("nx_tool_argecho.sov.elf.new" as *u8, elfnew, 7)
141 wfile("nx_tool_argecho.elf" as *u8, oldlive, 3)
142 let n5: i64 = mkreq(req, "POST /api/promote" as *u8, "target=nx_tool_argecho" as *u8)
143 let r5: i64 = ma_do_promote(req, n5, out)
144 var t5: i64 = 1
145 if gcontains(out, r5, "400" as *u8) == 0 { t5 = 0 }
146 if gcontains(out, r5, "confirm=yes" as *u8) == 0 { t5 = 0 }
147 if bytes_eq("nx_tool_argecho.sov.elf.new" as *u8, elfnew, 7) == 0 { t5 = 0 } // still staged
148 if bytes_eq("nx_tool_argecho.elf" as *u8, oldlive, 3) == 0 { t5 = 0 } // live unchanged
149 if t5 == 1 { w("T5 PASS no-confirm refused, staged untouched\n" as *u8) } else { fails = fails + 1; w("T5 FAIL no-confirm guard\n" as *u8); sys_write(1, out, r5); w("\n" as *u8) }
150
151 // T6 POS: promote (staged ELF + old live from T5) -> live == new ELF, .prev == old live, staged consumed.
152 let n6: i64 = mkreq(req, "POST /api/promote" as *u8, "target=nx_tool_argecho&confirm=yes" as *u8)
153 let r6: i64 = ma_do_promote(req, n6, out)
154 var t6: i64 = 1
155 if gcontains(out, r6, "200 OK" as *u8) == 0 { t6 = 0 }
156 if gcontains(out, r6, "PROMOTED" as *u8) == 0 { t6 = 0 }
157 if bytes_eq("nx_tool_argecho.elf" as *u8, elfnew, 7) == 0 { t6 = 0 } // live is now the new ELF
158 if bytes_eq("nx_tool_argecho.elf.prev" as *u8, oldlive, 3) == 0 { t6 = 0 } // never-brick backup
159 if file_absent("nx_tool_argecho.sov.elf.new" as *u8) == 0 { t6 = 0 } // staged consumed
160 if t6 == 1 { w("T6 PASS promote -> live==new ELF, .prev==old, staged consumed\n" as *u8) } else { fails = fails + 1; w("T6 FAIL promote\n" as *u8); sys_write(1, out, r6); w("\n" as *u8) }
161
162 // T7 (07-17): the MCP TOOL organ family is now allowlisted -- nx_shelltool with no staged file
163 // must reach "no valid staged" (proof it PASSED md_promote_organ_ok), while the tools DAEMON
164 // (nx_tools_api_serve) stays allowlist-refused.
165 let n7: i64 = mkreq(req, "POST /api/promote" as *u8, "target=nx_shelltool&confirm=yes" as *u8)
166 let r7: i64 = ma_do_promote(req, n7, out)
167 var t7: i64 = 1
168 if gcontains(out, r7, "no valid staged" as *u8) == 0 { t7 = 0 }
169 let n7b: i64 = mkreq(req, "POST /api/promote" as *u8, "target=nx_tools_api_serve&confirm=yes" as *u8)
170 let r7b: i64 = ma_do_promote(req, n7b, out)
171 if gcontains(out, r7b, "allowlist" as *u8) == 0 { t7 = 0 }
172 if t7 == 1 { w("T7 PASS tool-organ family allowlisted; tools daemon still refused\n" as *u8) } else { fails = fails + 1; w("T7 FAIL tool-family allowlist\n" as *u8) }
173
174 // cleanup
175 fio_unlink("sites.sov.elf.new" as *u8); fio_unlink("sites.elf" as *u8)
176 fio_unlink("nx_ecomat_beat.sov.elf.new" as *u8); fio_unlink("nx_ecomat_beat.elf" as *u8)
177 fio_unlink("nx_tool_argecho.sov.elf.new" as *u8); fio_unlink("nx_tool_argecho.elf" as *u8); fio_unlink("nx_tool_argecho.elf.prev" as *u8)
178
179 if fails == 0 { w("MGMT-PROMOTE-GATE 7/7 GREEN -- organ ELF promote is fail-closed (daemon/path/missing/non-ELF/no-confirm) + never-brick (.prev backup) + tool-family allowlisted\n" as *u8); sys_exit(0); return 0 }
180 w("MGMT-PROMOTE-GATE RED fails=" as *u8); pn(fails); w("\n" as *u8)
181 sys_exit(1); return 1
182}