code wiki / _hdl_build / nx_cms_multisite_gate.nx
nx_cms_multisite_gate.nx source
↩ module page · 373 lines · 15649 B
1// nx_cms_multisite_gate.nx -- CMS C7 GATE (re-runnable, evidence-driven): MULTI-SITE ISOLATION.
2// The deployment model is one admin daemon PER SITE (single responsibility, rule 9: a daemon does
3// one site's editing; the sites-daemon front door routes by host). This gate boots TWO real admin
4// daemons on two site dirs with different credentials and PROVES the isolation properties at the
5// wire: distinct seeds serve, site A's session is worthless at site B, A's password fails on B,
6// editing A never perturbs B's published bytes, and a cross-site save with A's session+csrf is
7// refused by B. Appends "CMSGATE row=nx_cms_multisite ... verdict=PASS|FAIL". Exit 0 iff all pass.
8// license_tier: ORIGINAL
9import "nx_cms_store.nx"
10import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host
11import "nx_sha256.nx"
12import "nx_syscalls.nx"
13import "_hdl_build/nx_kill_portable.nx"
14
15const MS_PORT_A: i64 = 8083
16const MS_PORT_B: i64 = 8084
17
18// Shared admin-path resolver -- the exec site lives in ms_boot, a DIFFERENT function from the
19// availability probe, so the path must be re-derived rather than passed as a local. Prefers the
20// runner's real output dir; falls back to /tmp for an older runner on another host.
21func ms_admin_path() -> *u8 {
22 let f: i64 = sys_openat_rd("buildroot/_build/nx_cms_admin.sov.elf" as *u8)
23 if f >= 0 { sys_close(f); return "buildroot/_build/nx_cms_admin.sov.elf" as *u8 }
24 return "/tmp/nx_cms_admin.sov.elf" as *u8
25}
26
27func ms_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 }
28func ms_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n }
29func ms_cat(dst: *u8, off: i64, s: *u8) -> i64 {
30 var o: i64 = off
31 var k: i64 = 0
32 while s[k] != (0 as u8) { dst[o] = s[k]; o = o + 1; k = k + 1 }
33 return o
34}
35func ms_catn(dst: *u8, off: i64, s: *u8, n: i64) -> i64 {
36 var o: i64 = off
37 var k: i64 = 0
38 while k < n { dst[o] = s[k]; o = o + 1; k = k + 1 }
39 return o
40}
41func ms_index(hay: *u8, n: i64, needle: *u8) -> i64 {
42 let nl: i64 = ms_len(needle)
43 var i: i64 = 0
44 while i + nl <= n {
45 var q: i64 = 0
46 var ok: i64 = 1
47 while q < nl { if (hay[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 }
48 if ok == 1 { return i }
49 i = i + 1
50 }
51 return 0 - 1
52}
53func ms_has(hay: *u8, n: i64, needle: *u8) -> i64 { if ms_index(hay, n, needle) >= 0 { return 1 } return 0 }
54
55// port-parametrized client (two daemons under test)
56func ms_http(port: i64, req: *u8, rl: i64, resp: *u8, cap: i64) -> i64 {
57 let fd: i64 = sys_socket(2, 1, 0)
58 if fd < 0 { return 0 - 1 }
59 let addr: *u8 = sys_mmap(16)
60 addr[0] = 2 as u8; addr[1] = 0 as u8
61 addr[2] = ((port >> 8) & 0xff) as u8
62 addr[3] = (port & 0xff) as u8
63 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8
64 var zi: i64 = 8
65 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 }
66 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 }
67 var off: i64 = 0
68 var go: i64 = 1
69 while go == 1 {
70 go = 0
71 if off < rl {
72 let w: i64 = sys_write(fd, (req + off) as *u8, rl - off)
73 if w > 0 { off = off + w; go = 1 }
74 }
75 }
76 sys_set_socket_timeout(fd, 10)
77 var total: i64 = 0
78 go = 1
79 while go == 1 {
80 go = 0
81 if total < cap {
82 let r: i64 = sys_read(fd, (resp + total) as *u8, cap - total)
83 if r > 0 { total = total + r; go = 1 }
84 }
85 }
86 sys_close(fd)
87 return total
88}
89
90func ms_post(req: *u8, path: *u8, cookie: *u8, body: *u8) -> i64 {
91 var o: i64 = ms_cat(req, 0, "POST " as *u8)
92 o = ms_cat(req, o, path)
93 o = ms_cat(req, o, " HTTP/1.1\r\nHost: gate\r\nContent-Type: application/x-www-form-urlencoded\r\n" as *u8)
94 if (cookie[0] as i64) != 0 {
95 o = ms_cat(req, o, "Cookie: nsess=" as *u8)
96 o = ms_catn(req, o, cookie, 32)
97 o = ms_cat(req, o, "\r\n" as *u8)
98 }
99 o = ms_cat(req, o, "Content-Length: " as *u8)
100 let bl: i64 = ms_len(body)
101 let t: *u8 = sys_mmap(24)
102 var m: i64 = bl
103 var k: i64 = 0
104 if m == 0 { t[0] = 48 as u8; k = 1 }
105 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
106 var i: i64 = 0
107 while i < k { req[o] = t[k-1-i]; o = o + 1; i = i + 1 }
108 o = ms_cat(req, o, "\r\n\r\n" as *u8)
109 o = ms_cat(req, o, body)
110 return o
111}
112func ms_get(req: *u8, path: *u8, cookie: *u8) -> i64 {
113 var o: i64 = ms_cat(req, 0, "GET " as *u8)
114 o = ms_cat(req, o, path)
115 o = ms_cat(req, o, " HTTP/1.1\r\nHost: gate\r\n" as *u8)
116 if (cookie[0] as i64) != 0 {
117 o = ms_cat(req, o, "Cookie: nsess=" as *u8)
118 o = ms_catn(req, o, cookie, 32)
119 o = ms_cat(req, o, "\r\n" as *u8)
120 }
121 o = ms_cat(req, o, "\r\n" as *u8)
122 return o
123}
124
125func ms_row(id: i64, ok: i64, what: *u8) -> i64 {
126 ms_w("MSROW " as *u8)
127 let d: *u8 = sys_mmap(8); d[0] = (48 + id/10) as u8; d[1] = (48 + id%10) as u8; d[2] = 32 as u8; d[3] = 0 as u8
128 ms_w(d)
129 if ok == 1 { ms_w("PASS " as *u8) }
130 if ok == 0 { ms_w("FAIL " as *u8) }
131 ms_w(what); ms_w("\n" as *u8)
132 return ok
133}
134
135func ms_mkdir(path: *u8, mode: i64) -> i64 {
136 let nb: *i64 = sys_mmap(8) as *i64
137 nb[0] = 258
138 return __syscall(nb[0], 0 - 100, path, mode, 0, 0, 0)
139}
140
141// write a sha256 pw file for a site
142func ms_pwfile(path: *u8, pw: *u8, pwlen: i64) -> i64 {
143 let dig: *u8 = sys_mmap(32)
144 sha256_digest(pw, pwlen, dig)
145 let hexs: *u8 = sys_mmap(80)
146 let hxc: *u8 = "0123456789abcdef" as *u8
147 var i: i64 = 0
148 while i < 32 {
149 hexs[i*2] = hxc[((dig[i] as i64) >> 4) & 15]
150 hexs[i*2+1] = hxc[(dig[i] as i64) & 15]
151 i = i + 1
152 }
153 let pf: i64 = sys_openat_wr(path, 0x1a4)
154 if pf < 0 { return 0 }
155 sys_write(pf, hexs, 64)
156 sys_close(pf)
157 return 1
158}
159
160// boot one admin daemon (already-built binary) on (port, site)
161func ms_boot(portstr: *u8, site: *u8, logpath: *u8) -> i64 {
162 let pid: i64 = sys_fork()
163 if pid == 0 {
164 let lg: i64 = sys_openat_wr(logpath, 0x1a4)
165 if lg >= 0 { sys_dup3(lg, 1, 0); sys_dup3(lg, 2, 0) }
166 let argvv: *i64 = sys_mmap(8*6) as *i64
167 argvv[0] = ms_admin_path() as i64
168 argvv[1] = portstr as i64
169 argvv[2] = site as i64
170 argvv[3] = "99" as *u8 as i64
171 argvv[4] = 0
172 let envp: *i64 = sys_mmap(16) as *i64
173 envp[0] = 0
174 sys_execve(ms_admin_path(), argvv, envp)
175 sys_exit(127)
176 }
177 return pid
178}
179
180func main() -> i64 {
181 // ---- site A = andelinwest fixtures; site B = a DISTINCT site written here ----
182 ms_mkdir("/tmp/_cms_ms_a" as *u8, 0x1ed)
183 ms_mkdir("/tmp/_cms_ms_b" as *u8, 0x1ed)
184 cst_copy("knowledge/cms/andelinwest/template.html" as *u8, "/tmp/_cms_ms_a/template.html" as *u8)
185 cst_copy("knowledge/cms/andelinwest/content.txt" as *u8, "/tmp/_cms_ms_a/content.txt" as *u8)
186 cst_copy("knowledge/cms/andelinwest/template.html" as *u8, "/tmp/_cms_ms_b/template.html" as *u8)
187 sys_renameat("/tmp/_cms_ms_a/draft.txt" as *u8, "/tmp/_cms_ms_a/draft.stale" as *u8)
188 sys_renameat("/tmp/_cms_ms_b/draft.txt" as *u8, "/tmp/_cms_ms_b/draft.stale" as *u8)
189 let bcontent: *u8 = "@hero_title\nBryce Canyon Tours\n@hero_tag\nGuided rim-to-rim adventures.\n@note_html\n<p>Season opens in May.</p>\n@foot\nSite B fixture.\n" as *u8
190 let bf: i64 = sys_openat_wr("/tmp/_cms_ms_b/content.txt" as *u8, 0x1a4)
191 sys_write(bf, bcontent, ms_len(bcontent))
192 sys_close(bf)
193 ms_pwfile("/tmp/_cms_ms_a/admin_pw.sha256" as *u8, "site-a-pw-1" as *u8, 11)
194 ms_pwfile("/tmp/_cms_ms_b/admin_pw.sha256" as *u8, "site-b-pw-1" as *u8, 11)
195
196 // ---- rebuild the admin once, boot BOTH ----
197 let bpid: i64 = sys_fork()
198 if bpid == 0 {
199 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
200 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) }
201 let bargv: *i64 = sys_mmap(32) as *i64
202 bargv[0] = "_offc/nx_sov_build_run.elf" as *u8 as i64
203 bargv[1] = "nx_cms_admin" as *u8 as i64
204 bargv[2] = 0
205 let benvp: *i64 = sys_mmap(16) as *i64
206 benvp[0] = 0
207 sys_execve("_offc/nx_sov_build_run.elf" as *u8, bargv, benvp); sys_exit(127)
208 }
209 let bst: *i64 = sys_mmap(16) as *i64
210 sys_wait4(bpid, bst, 0)
211 // CALLER-GAP FIX 2026-08-01 (seq1267 class, left behind in 11 cms gates): the runner emits
212 // buildroot/_build/<t>.sov.elf under the no-/tmp doctrine while this probed /tmp. Proven by control:
213 // buildroot/_build/nx_cms_admin.sov.elf = 208962B, /tmp/nx_cms_admin.sov.elf unreadable.
214 var bchk: i64 = sys_openat_rd("buildroot/_build/nx_cms_admin.sov.elf" as *u8)
215 var badm: *u8 = "buildroot/_build/nx_cms_admin.sov.elf" as *u8
216 if bchk < 0 { bchk = sys_openat_rd("/tmp/nx_cms_admin.sov.elf" as *u8); badm = "/tmp/nx_cms_admin.sov.elf" as *u8 }
217 if bchk < 0 { ms_w("CMSGATE row=nx_cms_multisite ADMIN-BUILD-MISSING (probed buildroot/_build AND /tmp)\n" as *u8); sys_exit(1) }
218 sys_close(bchk)
219 let pid_a: i64 = ms_boot("8083" as *u8, "/tmp/_cms_ms_a" as *u8, "/tmp/_cms_admin_ms_a.log" as *u8)
220 let pid_b: i64 = ms_boot("8084" as *u8, "/tmp/_cms_ms_b" as *u8, "/tmp/_cms_admin_ms_b.log" as *u8)
221
222 let req: *u8 = sys_mmap(262144)
223 let resp: *u8 = sys_mmap(524288)
224 let nocookie: *u8 = sys_mmap(8)
225 nocookie[0] = 0 as u8
226 let sess_a: *u8 = sys_mmap(64)
227 sess_a[0] = 0 as u8
228 let csrf_a: *u8 = sys_mmap(64)
229 csrf_a[0] = 0 as u8
230
231 // M0: both serve their DISTINCT seeds
232 var rn: i64 = 0 - 1
233 var tries: i64 = 0
234 while tries < 50 {
235 let rl0: i64 = ms_get(req, "/" as *u8, nocookie)
236 rn = ms_http(MS_PORT_A, req, rl0, resp, 524287)
237 if rn > 0 { tries = 50 }
238 if rn <= 0 { sys_sleep_ms(100); tries = tries + 1 }
239 }
240 var a_ok: i64 = 0
241 if rn > 0 { if ms_has(resp, rn, "Andelin West Law" as *u8) == 1 { if ms_has(resp, rn, "Bryce Canyon Tours" as *u8) == 0 { a_ok = 1 } } }
242 tries = 0
243 rn = 0 - 1
244 while tries < 50 {
245 let rl1: i64 = ms_get(req, "/" as *u8, nocookie)
246 rn = ms_http(MS_PORT_B, req, rl1, resp, 524287)
247 if rn > 0 { tries = 50 }
248 if rn <= 0 { sys_sleep_ms(100); tries = tries + 1 }
249 }
250 var b_ok: i64 = 0
251 if rn > 0 { if ms_has(resp, rn, "Bryce Canyon Tours" as *u8) == 1 { if ms_has(resp, rn, "Andelin West Law" as *u8) == 0 { b_ok = 1 } } }
252 var pass: i64 = 0
253 var rows: i64 = 0
254 var ok: i64 = 0
255 if a_ok == 1 { if b_ok == 1 { ok = 1 } }
256 rows = rows + 1; pass = pass + ms_row(0, ok, "both sites serve distinct seeds" as *u8)
257
258 // M1: login to A -> session + csrf
259 var rl: i64 = ms_post(req, "/admin/login" as *u8, nocookie, "pw=site-a-pw-1" as *u8)
260 rn = ms_http(MS_PORT_A, req, rl, resp, 524287)
261 ok = 0
262 if rn > 0 { if ms_has(resp, rn, "303" as *u8) == 1 {
263 let sp: i64 = ms_index(resp, rn, "nsess=" as *u8)
264 if sp >= 0 {
265 var m2: i64 = 0
266 while m2 < 32 { sess_a[m2] = resp[sp+6+m2]; m2 = m2 + 1 }
267 sess_a[32] = 0 as u8
268 ok = 1
269 }
270 } }
271 if ok == 1 {
272 rl = ms_get(req, "/admin" as *u8, sess_a)
273 rn = ms_http(MS_PORT_A, req, rl, resp, 524287)
274 ok = 0
275 let cp: i64 = ms_index(resp, rn, "name=\"csrf\" value=\"" as *u8)
276 if cp >= 0 {
277 var m3: i64 = 0
278 while m3 < 32 { csrf_a[m3] = resp[cp+19+m3]; m3 = m3 + 1 }
279 csrf_a[32] = 0 as u8
280 ok = 1
281 }
282 }
283 rows = rows + 1; pass = pass + ms_row(1, ok, "site A login + editor + csrf" as *u8)
284
285 // M2: A's session is WORTHLESS at B (login page only, no editor fields)
286 rl = ms_get(req, "/admin" as *u8, sess_a)
287 rn = ms_http(MS_PORT_B, req, rl, resp, 524287)
288 ok = 0
289 if rn > 0 { if ms_has(resp, rn, "Editor Login" as *u8) == 1 { if ms_has(resp, rn, "name=\"hero_title\"" as *u8) == 0 { ok = 1 } } }
290 rows = rows + 1; pass = pass + ms_row(2, ok, "A session rejected by B (login only)" as *u8)
291
292 // M3: A's password fails on B
293 rl = ms_post(req, "/admin/login" as *u8, nocookie, "pw=site-a-pw-1" as *u8)
294 rn = ms_http(MS_PORT_B, req, rl, resp, 524287)
295 ok = 0
296 if rn > 0 { if ms_has(resp, rn, "403" as *u8) == 1 { ok = 1 } }
297 rows = rows + 1; pass = pass + ms_row(3, ok, "A password refused by B 403" as *u8)
298
299 // M4: edit A -> A live
300 let body4: *u8 = sys_mmap(4096)
301 var b4: i64 = ms_cat(body4, 0, "csrf=" as *u8)
302 b4 = ms_catn(body4, b4, csrf_a, 32)
303 b4 = ms_cat(body4, b4, "&hero_title=Edited+Only+A" as *u8)
304 body4[b4] = 0 as u8
305 rl = ms_post(req, "/admin/save" as *u8, sess_a, body4)
306 rn = ms_http(MS_PORT_A, req, rl, resp, 524287)
307 ok = 0
308 if rn > 0 { if ms_has(resp, rn, "303" as *u8) == 1 {
309 let rl4: i64 = ms_get(req, "/" as *u8, nocookie)
310 let rn4: i64 = ms_http(MS_PORT_A, req, rl4, resp, 524287)
311 if rn4 > 0 { if ms_has(resp, rn4, "Edited Only A" as *u8) == 1 { ok = 1 } }
312 } }
313 rows = rows + 1; pass = pass + ms_row(4, ok, "A edit publishes on A" as *u8)
314
315 // M5: B's published bytes UNPERTURBED by A's edit
316 rl = ms_get(req, "/" as *u8, nocookie)
317 rn = ms_http(MS_PORT_B, req, rl, resp, 524287)
318 ok = 0
319 if rn > 0 { if ms_has(resp, rn, "Edited Only A" as *u8) == 0 { if ms_has(resp, rn, "Bryce Canyon Tours" as *u8) == 1 { ok = 1 } } }
320 rows = rows + 1; pass = pass + ms_row(5, ok, "B page unperturbed by A's edit" as *u8)
321
322 // M6: cross-site save: A's session+csrf against B -> refused
323 rl = ms_post(req, "/admin/save" as *u8, sess_a, body4)
324 rn = ms_http(MS_PORT_B, req, rl, resp, 524287)
325 ok = 0
326 if rn > 0 { if ms_has(resp, rn, "403" as *u8) == 1 {
327 let rl6: i64 = ms_get(req, "/" as *u8, nocookie)
328 let rn6: i64 = ms_http(MS_PORT_B, req, rl6, resp, 524287)
329 if rn6 > 0 { if ms_has(resp, rn6, "Edited Only A" as *u8) == 0 { ok = 1 } }
330 } }
331 rows = rows + 1; pass = pass + ms_row(6, ok, "cross-site save A->B refused, B intact" as *u8)
332
333 // ---- teardown both ----
334 nxk_kill(pid_a, 9)
335 nxk_kill(pid_b, 9)
336 let stbuf: *i64 = sys_mmap(16) as *i64
337 sys_wait4(pid_a, stbuf, 0)
338 sys_wait4(pid_b, stbuf, 0)
339
340 var verdict: *u8 = "FAIL" as *u8
341 if pass == rows { verdict = "PASS" as *u8 }
342 let line: *u8 = sys_mmap(512)
343 var lo: i64 = ms_cat(line, 0, "CMSGATE row=nx_cms_multisite rows=" as *u8)
344 let t2: *u8 = sys_mmap(24)
345 var mm: i64 = rows
346 var kk: i64 = 0
347 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
348 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
349 var ii: i64 = 0
350 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
351 lo = ms_cat(line, lo, " pass=" as *u8)
352 mm = pass; kk = 0
353 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
354 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
355 ii = 0
356 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
357 lo = ms_cat(line, lo, " verdict=" as *u8)
358 lo = ms_cat(line, lo, verdict)
359 lo = ms_cat(line, lo, " epoch=" as *u8)
360 mm = sys_now_realtime_sec(); kk = 0
361 if mm == 0 { t2[0] = 48 as u8; kk = 1 }
362 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 }
363 ii = 0
364 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 }
365 line[lo] = 10 as u8
366 lo = lo + 1
367 sys_write(1, line, lo)
368 let gf: i64 = sys_openat_append("knowledge/status/cms_gate.log" as *u8, 0x1a4)
369 if gf >= 0 { sys_write(gf, line, lo); sys_close(gf) }
370 if pass == rows { sys_exit(0) }
371 sys_exit(1)
372 return 1
373}