code wiki / _hdl_build / nx_cms_gate.nx

nx_cms_gate.nx source

↩ module page · 357 lines · 15292 B

1// nx_cms_gate.nx -- THE CMS ARC GATE (re-runnable, evidence-driven): boots the real admin daemon on 2// loopback, then plays the LEGAL-TEAM USER + THE ATTACKER over real HTTP with its own client (no curl, 3// no .sh): unauth surfaces refused, login lockout-guarded, CSRF enforced, stored-XSS sanitized on 4// write, edit->publish live, revision rollback restores the prior publish. These are the spec's 5// acceptance criteria (2026-06-06-sovereign-cms-architecture.md) mechanized. Appends a CMSGATE line 6// to knowledge/status/cms_gate.log. Exit 0 iff permil=1000. license_tier: ORIGINAL 7import "nx_cms_store.nx" 8import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 9import "nx_sha256.nx" 10import "nx_syscalls.nx" 11import "_hdl_build/nx_kill_portable.nx" 12 13const CG_PORT: i64 = 8077 14const CG_SITE: *u8 = "/tmp/_cms_gate_site" 15 16func cg_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 17func cg_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 18func cg_cat(dst: *u8, off: i64, s: *u8) -> i64 { 19 var o: i64 = off 20 var k: i64 = 0 21 while s[k] != (0 as u8) { dst[o] = s[k]; o = o + 1; k = k + 1 } 22 return o 23} 24func cg_catn(dst: *u8, off: i64, s: *u8, n: i64) -> i64 { 25 var o: i64 = off 26 var k: i64 = 0 27 while k < n { dst[o] = s[k]; o = o + 1; k = k + 1 } 28 return o 29} 30func cg_index(hay: *u8, n: i64, needle: *u8) -> i64 { 31 let nl: i64 = cg_len(needle) 32 var i: i64 = 0 33 while i + nl <= n { 34 var q: i64 = 0 35 var ok: i64 = 1 36 while q < nl { if (hay[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 } 37 if ok == 1 { return i } 38 i = i + 1 39 } 40 return 0 - 1 41} 42func cg_has(hay: *u8, n: i64, needle: *u8) -> i64 { if cg_index(hay, n, needle) >= 0 { return 1 } return 0 } 43 44// own HTTP client: send req, read to EOF (server closes). returns response length, -1 connect-fail. 45func cg_http(req: *u8, rl: i64, resp: *u8, cap: i64) -> i64 { 46 let fd: i64 = sys_socket(2, 1, 0) 47 if fd < 0 { return 0 - 1 } 48 let addr: *u8 = sys_mmap(16) 49 addr[0] = 2 as u8; addr[1] = 0 as u8 50 addr[2] = ((CG_PORT >> 8) & 0xff) as u8 51 addr[3] = (CG_PORT & 0xff) as u8 52 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8 53 var zi: i64 = 8 54 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 } 55 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 } 56 var off: i64 = 0 57 var go: i64 = 1 58 while go == 1 { 59 go = 0 60 if off < rl { 61 let w: i64 = sys_write(fd, (req + off) as *u8, rl - off) 62 if w > 0 { off = off + w; go = 1 } 63 } 64 } 65 sys_set_socket_timeout(fd, 10) 66 var total: i64 = 0 67 go = 1 68 while go == 1 { 69 go = 0 70 if total < cap { 71 let r: i64 = sys_read(fd, (resp + total) as *u8, cap - total) 72 if r > 0 { total = total + r; go = 1 } 73 } 74 } 75 sys_close(fd) 76 return total 77} 78 79// build a POST with urlencoded body 80func cg_post(req: *u8, path: *u8, cookie: *u8, body: *u8) -> i64 { 81 var o: i64 = cg_cat(req, 0, "POST " as *u8) 82 o = cg_cat(req, o, path) 83 o = cg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\nContent-Type: application/x-www-form-urlencoded\r\n" as *u8) 84 if (cookie[0] as i64) != 0 { 85 o = cg_cat(req, o, "Cookie: nsess=" as *u8) 86 o = cg_catn(req, o, cookie, 32) 87 o = cg_cat(req, o, "\r\n" as *u8) 88 } 89 o = cg_cat(req, o, "Content-Length: " as *u8) 90 let bl: i64 = cg_len(body) 91 // integer print 92 let t: *u8 = sys_mmap(24) 93 var m: i64 = bl 94 var k: i64 = 0 95 if m == 0 { t[0] = 48 as u8; k = 1 } 96 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 97 var i: i64 = 0 98 while i < k { req[o] = t[k-1-i]; o = o + 1; i = i + 1 } 99 o = cg_cat(req, o, "\r\n\r\n" as *u8) 100 o = cg_cat(req, o, body) 101 return o 102} 103func cg_get(req: *u8, path: *u8, cookie: *u8) -> i64 { 104 var o: i64 = cg_cat(req, 0, "GET " as *u8) 105 o = cg_cat(req, o, path) 106 o = cg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\n" as *u8) 107 if (cookie[0] as i64) != 0 { 108 o = cg_cat(req, o, "Cookie: nsess=" as *u8) 109 o = cg_catn(req, o, cookie, 32) 110 o = cg_cat(req, o, "\r\n" as *u8) 111 } 112 o = cg_cat(req, o, "\r\n" as *u8) 113 return o 114} 115 116func cg_row(id: i64, ok: i64, what: *u8) -> i64 { 117 cg_w("CMSROW " as *u8) 118 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 119 cg_w(d) 120 if ok == 1 { cg_w("PASS " as *u8) } 121 if ok == 0 { cg_w("FAIL " as *u8) } 122 cg_w(what); cg_w("\n" as *u8) 123 return ok 124} 125 126// mkdirat via runtime-computed x86 number (258): sys_mkdir's rv64 34 risked the untranslated-row 127// pause-hang (same class as the nxk_kill landmine; mkdirat row was absent on a 2026-06-10 lane). 128func cg_mkdir(path: *u8, mode: i64) -> i64 { 129 let nb: *i64 = sys_mmap(8) as *i64 130 nb[0] = 258 // x86_64 mkdirat, untranslated by design 131 return __syscall(nb[0], 0 - 100, path, mode, 0, 0, 0) 132} 133 134func main() -> i64 { 135 // ---- setup the gate site dir from the canonical andelinwest content ---- 136 cg_mkdir(CG_SITE, 0x1ed) 137 cst_copy("knowledge/cms/andelinwest/template.html" as *u8, "/tmp/_cms_gate_site/template.html" as *u8) 138 cst_copy("knowledge/cms/andelinwest/content.txt" as *u8, "/tmp/_cms_gate_site/content.txt" as *u8) 139 // remove stale revision/page state from prior runs: overwrite by fresh copies only (additive law: 140 // /tmp gate scratch, not production data). pw hash = sha256("gate-test-pw-1") 141 let dig: *u8 = sys_mmap(32) 142 sha256_digest("gate-test-pw-1" as *u8, 14, dig) 143 let hexs: *u8 = sys_mmap(80) 144 let hxc: *u8 = "0123456789abcdef" as *u8 145 var i: i64 = 0 146 while i < 32 { 147 hexs[i*2] = hxc[((dig[i] as i64) >> 4) & 15] 148 hexs[i*2+1] = hxc[(dig[i] as i64) & 15] 149 i = i + 1 150 } 151 let pf: i64 = sys_openat_wr("/tmp/_cms_gate_site/admin_pw.sha256" as *u8, 0x1a4) 152 sys_write(pf, hexs, 64) 153 sys_close(pf) 154 155 // ---- REBUILD the admin daemon sovereignly (prove-all doctrine: /tmp is contested; never 156 // trust a prebuilt binary). The runner runs it arg-less after build = harmless usage-exit. ---- 157 let bpid: i64 = sys_fork() 158 if bpid == 0 { 159 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4) 160 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) } 161 let bargv: *i64 = sys_mmap(32) as *i64 162 bargv[0] = "_offc/nx_sov_build_run.elf" as *u8 as i64 163 bargv[1] = "nx_cms_admin" as *u8 as i64 164 bargv[2] = 0 165 let benvp: *i64 = sys_mmap(16) as *i64 166 benvp[0] = 0 167 sys_execve("_offc/nx_sov_build_run.elf" as *u8, bargv, benvp); sys_exit(127) 168 } 169 let bst: *i64 = sys_mmap(16) as *i64 170 sys_wait4(bpid, bst, 0) 171 // CALLER-GAP FIX 2026-08-01 (seq1267 class): the runner emits buildroot/_build/<t>.sov.elf under 172 // the no-/tmp doctrine while this probed /tmp. Proven by control: _build copy 208962B, /tmp absent. 173 var bchk: i64 = sys_openat_rd("buildroot/_build/nx_cms_admin.sov.elf" as *u8) 174 var badm: *u8 = "buildroot/_build/nx_cms_admin.sov.elf" as *u8 175 if bchk < 0 { bchk = sys_openat_rd("/tmp/nx_cms_admin.sov.elf" as *u8); badm = "/tmp/nx_cms_admin.sov.elf" as *u8 } 176 if bchk < 0 { cg_w("CMSGATE ADMIN-BUILD-MISSING (probed buildroot/_build AND /tmp)\n" as *u8); sys_exit(1) } 177 sys_close(bchk) 178 179 // ---- boot the real admin daemon ---- 180 let pid: i64 = sys_fork() 181 if pid == 0 { 182 let lg: i64 = sys_openat_wr("/tmp/_cms_admin_gate.log" as *u8, 0x1a4) 183 if lg >= 0 { sys_dup3(lg, 1, 0); sys_dup3(lg, 2, 0) } 184 let argvv: *i64 = sys_mmap(8*6) as *i64 185 argvv[0] = badm as i64 186 argvv[1] = "8077" as *u8 as i64 187 argvv[2] = CG_SITE as i64 188 argvv[3] = "99" as *u8 as i64 189 argvv[4] = 0 190 let envp: *i64 = sys_mmap(16) as *i64 191 envp[0] = 0 192 sys_execve(badm, argvv, envp) 193 sys_exit(127) 194 } 195 196 let req: *u8 = sys_mmap(262144) 197 let resp: *u8 = sys_mmap(524288) 198 let nocookie: *u8 = sys_mmap(8) 199 nocookie[0] = 0 as u8 200 let sess: *u8 = sys_mmap(64) 201 sess[0] = 0 as u8 202 let csrf: *u8 = sys_mmap(64) 203 csrf[0] = 0 as u8 204 205 // R0: seed page serves (also the boot-poll: retry till the daemon accepts) 206 var rn: i64 = 0 - 1 207 var tries: i64 = 0 208 while tries < 50 { 209 let rl: i64 = cg_get(req, "/" as *u8, nocookie) 210 rn = cg_http(req, rl, resp, 524287) 211 if rn > 0 { tries = 50 } 212 if rn <= 0 { sys_sleep_ms(100); tries = tries + 1 } 213 } 214 var pass: i64 = 0 215 var rows: i64 = 0 216 var ok: i64 = 0 217 ok = 0 218 if rn > 0 { if cg_has(resp, rn, "200 OK" as *u8) == 1 { if cg_has(resp, rn, "Andelin West Law" as *u8) == 1 { ok = 1 } } } 219 rows = rows + 1; pass = pass + cg_row(0, ok, "seed render serves GET /" as *u8) 220 221 // R1: GET /admin unauthed -> login form, NO editor fields leak 222 var rl: i64 = cg_get(req, "/admin" as *u8, nocookie) 223 rn = cg_http(req, rl, resp, 524287) 224 ok = 0 225 if rn > 0 { if cg_has(resp, rn, "Editor Login" as *u8) == 1 { if cg_has(resp, rn, "hero_title" as *u8) == 0 { ok = 1 } } } 226 rows = rows + 1; pass = pass + cg_row(1, ok, "unauthed /admin = login only" as *u8) 227 228 // R2: unauth POST /admin/save -> 403 229 rl = cg_post(req, "/admin/save" as *u8, nocookie, "csrf=00000000000000000000000000000000&hero_title=HACKED" as *u8) 230 rn = cg_http(req, rl, resp, 524287) 231 ok = 0 232 if rn > 0 { if cg_has(resp, rn, "403" as *u8) == 1 { ok = 1 } } 233 rows = rows + 1; pass = pass + cg_row(2, ok, "unauth save refused 403" as *u8) 234 235 // R3: wrong password -> 403 236 rl = cg_post(req, "/admin/login" as *u8, nocookie, "pw=wrong-password" as *u8) 237 rn = cg_http(req, rl, resp, 524287) 238 ok = 0 239 if rn > 0 { if cg_has(resp, rn, "403" as *u8) == 1 { ok = 1 } } 240 rows = rows + 1; pass = pass + cg_row(3, ok, "wrong password refused 403" as *u8) 241 242 // R4: correct password -> 303 + session cookie 243 rl = cg_post(req, "/admin/login" as *u8, nocookie, "pw=gate-test-pw-1" as *u8) 244 rn = cg_http(req, rl, resp, 524287) 245 ok = 0 246 if rn > 0 { if cg_has(resp, rn, "303" as *u8) == 1 { 247 let sp: i64 = cg_index(resp, rn, "nsess=" as *u8) 248 if sp >= 0 { 249 var m2: i64 = 0 250 while m2 < 32 { sess[m2] = resp[sp+6+m2]; m2 = m2 + 1 } 251 sess[32] = 0 as u8 252 ok = 1 253 } 254 } } 255 rows = rows + 1; pass = pass + cg_row(4, ok, "login issues HttpOnly session" as *u8) 256 257 // R5: GET /admin with session -> editor with fields + CSRF 258 rl = cg_get(req, "/admin" as *u8, sess) 259 rn = cg_http(req, rl, resp, 524287) 260 ok = 0 261 if rn > 0 { if cg_has(resp, rn, "name=\"hero_title\"" as *u8) == 1 { if cg_has(resp, rn, "Andelin West Law" as *u8) == 1 { 262 let cp: i64 = cg_index(resp, rn, "name=\"csrf\" value=\"" as *u8) 263 if cp >= 0 { 264 var m3: i64 = 0 265 while m3 < 32 { csrf[m3] = resp[cp+19+m3]; m3 = m3 + 1 } 266 csrf[32] = 0 as u8 267 ok = 1 268 } 269 } } } 270 rows = rows + 1; pass = pass + cg_row(5, ok, "editor shows fields + csrf" as *u8) 271 272 // R6: save with session but WRONG csrf -> 403 273 rl = cg_post(req, "/admin/save" as *u8, sess, "csrf=00000000000000000000000000000000&hero_title=FORGED" as *u8) 274 rn = cg_http(req, rl, resp, 524287) 275 ok = 0 276 if rn > 0 { if cg_has(resp, rn, "403" as *u8) == 1 { ok = 1 } } 277 rows = rows + 1; pass = pass + cg_row(6, ok, "cross-site forged save refused (CSRF)" as *u8) 278 279 // R7: REAL EDIT: new headline + note with injected <script> (and legit <b>) 280 let body7: *u8 = sys_mmap(4096) 281 var b7: i64 = cg_cat(body7, 0, "csrf=" as *u8) 282 b7 = cg_catn(body7, b7, csrf, 32) 283 b7 = cg_cat(body7, b7, "&hero_title=Andelin+West+Law+-+Newly+Edited&note_html=%3Cscript%3Ealert%281%29%3C%2Fscript%3E%3Cb%3ESpring+hours+posted.%3C%2Fb%3E" as *u8) 284 body7[b7] = 0 as u8 285 rl = cg_post(req, "/admin/save" as *u8, sess, body7) 286 rn = cg_http(req, rl, resp, 524287) 287 ok = 0 288 if rn > 0 { if cg_has(resp, rn, "303" as *u8) == 1 { ok = 1 } } 289 rows = rows + 1; pass = pass + cg_row(7, ok, "legal-team edit publishes 303" as *u8) 290 291 // R8: the live page reflects the edit, keeps <b>, and the script is GONE 292 rl = cg_get(req, "/" as *u8, nocookie) 293 rn = cg_http(req, rl, resp, 524287) 294 ok = 0 295 if rn > 0 { if cg_has(resp, rn, "Newly Edited" as *u8) == 1 { if cg_has(resp, rn, "<b>Spring hours posted.</b>" as *u8) == 1 { if cg_has(resp, rn, "<script" as *u8) == 0 { ok = 1 } } } } 296 rows = rows + 1; pass = pass + cg_row(8, ok, "edit LIVE + stored-XSS sanitized" as *u8) 297 298 // R9: rollback -> 303 299 let body9: *u8 = sys_mmap(256) 300 var b9: i64 = cg_cat(body9, 0, "csrf=" as *u8) 301 b9 = cg_catn(body9, b9, csrf, 32) 302 body9[b9] = 0 as u8 303 rl = cg_post(req, "/admin/rollback" as *u8, sess, body9) 304 rn = cg_http(req, rl, resp, 524287) 305 ok = 0 306 if rn > 0 { if cg_has(resp, rn, "303" as *u8) == 1 { ok = 1 } } 307 rows = rows + 1; pass = pass + cg_row(9, ok, "rollback accepted" as *u8) 308 309 // R10: prior publish restored 310 rl = cg_get(req, "/" as *u8, nocookie) 311 rn = cg_http(req, rl, resp, 524287) 312 ok = 0 313 if rn > 0 { if cg_has(resp, rn, "Newly Edited" as *u8) == 0 { if cg_has(resp, rn, "Andelin West Law" as *u8) == 1 { ok = 1 } } } 314 rows = rows + 1; pass = pass + cg_row(10, ok, "revision rollback restores prior publish" as *u8) 315 316 // ---- teardown our own child (nxk_kill = runtime-computed x86 kill, the syscall-table escape hatch) ---- 317 nxk_kill(pid, 9) 318 let stbuf: *i64 = sys_mmap(16) as *i64 319 sys_wait4(pid, stbuf, 0) 320 321 let permil: i64 = pass * 1000 / rows 322 let line: *u8 = sys_mmap(512) 323 var lo: i64 = cg_cat(line, 0, "CMSGATE target=andelinwest rows=" as *u8) 324 let t2: *u8 = sys_mmap(24) 325 var mm: i64 = rows 326 var kk: i64 = 0 327 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 328 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 329 var ii: i64 = 0 330 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 331 lo = cg_cat(line, lo, " pass=" as *u8) 332 mm = pass; kk = 0 333 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 334 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 335 ii = 0 336 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 337 lo = cg_cat(line, lo, " permil=" as *u8) 338 mm = permil; kk = 0 339 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 340 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 341 ii = 0 342 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 343 lo = cg_cat(line, lo, " epoch=" as *u8) 344 mm = sys_now_realtime_sec(); kk = 0 345 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 346 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 347 ii = 0 348 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 349 line[lo] = 10 as u8 350 lo = lo + 1 351 sys_write(1, line, lo) 352 let gf: i64 = sys_openat_append("knowledge/status/cms_gate.log" as *u8, 0x1a4) 353 if gf >= 0 { sys_write(gf, line, lo); sys_close(gf) } 354 if permil == 1000 { sys_exit(0) } 355 sys_exit(1) 356 return 1 357}