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}