code wiki / _hdl_build / nx_cms_gate.nx

nx_cms_gate.nx source

↩ module page · 385 lines · 17470 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 "nx_gate_verdict.nx" 12import "_hdl_build/nx_kill_portable.nx" 13 14const CG_PORT: i64 = 8077 15const CG_SITE: *u8 = "/tmp/_cms_gate_site" 16 17func 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 } 18func cg_len(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 19func cg_cat(dst: *u8, off: i64, s: *u8) -> i64 { 20 var o: i64 = off 21 var k: i64 = 0 22 while s[k] != (0 as u8) { dst[o] = s[k]; o = o + 1; k = k + 1 } 23 return o 24} 25func cg_catn(dst: *u8, off: i64, s: *u8, n: i64) -> i64 { 26 var o: i64 = off 27 var k: i64 = 0 28 while k < n { dst[o] = s[k]; o = o + 1; k = k + 1 } 29 return o 30} 31func cg_index(hay: *u8, n: i64, needle: *u8) -> i64 { 32 let nl: i64 = cg_len(needle) 33 var i: i64 = 0 34 while i + nl <= n { 35 var q: i64 = 0 36 var ok: i64 = 1 37 while q < nl { if (hay[i+q] as i64) != (needle[q] as i64) { ok = 0; q = nl } q = q + 1 } 38 if ok == 1 { return i } 39 i = i + 1 40 } 41 return 0 - 1 42} 43func cg_has(hay: *u8, n: i64, needle: *u8) -> i64 { if cg_index(hay, n, needle) >= 0 { return 1 } return 0 } 44 45// own HTTP client: send req, read to EOF (server closes). returns response length, -1 connect-fail. 46func cg_http(req: *u8, rl: i64, resp: *u8, cap: i64) -> i64 { 47 let fd: i64 = sys_socket(2, 1, 0) 48 if fd < 0 { return 0 - 1 } 49 let addr: *u8 = sys_mmap(16) 50 addr[0] = 2 as u8; addr[1] = 0 as u8 51 addr[2] = ((CG_PORT >> 8) & 0xff) as u8 52 addr[3] = (CG_PORT & 0xff) as u8 53 addr[4] = 127 as u8; addr[5] = 0 as u8; addr[6] = 0 as u8; addr[7] = 1 as u8 54 var zi: i64 = 8 55 while zi < 16 { addr[zi] = 0 as u8; zi = zi + 1 } 56 if nx_connect_bounded(fd, addr, 16, NX_CONN_DEFAULT_MS) < 0 { sys_close(fd); return 0 - 1 } 57 var off: i64 = 0 58 var go: i64 = 1 59 while go == 1 { 60 go = 0 61 if off < rl { 62 let w: i64 = sys_write(fd, (req + off) as *u8, rl - off) 63 if w > 0 { off = off + w; go = 1 } 64 } 65 } 66 sys_set_socket_timeout(fd, 10) 67 var total: i64 = 0 68 go = 1 69 while go == 1 { 70 go = 0 71 if total < cap { 72 let r: i64 = sys_read(fd, (resp + total) as *u8, cap - total) 73 if r > 0 { total = total + r; go = 1 } 74 } 75 } 76 sys_close(fd) 77 return total 78} 79 80// build a POST with urlencoded body 81func cg_post(req: *u8, path: *u8, cookie: *u8, body: *u8) -> i64 { 82 var o: i64 = cg_cat(req, 0, "POST " as *u8) 83 o = cg_cat(req, o, path) 84 o = cg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\nContent-Type: application/x-www-form-urlencoded\r\n" as *u8) 85 if (cookie[0] as i64) != 0 { 86 o = cg_cat(req, o, "Cookie: nsess=" as *u8) 87 o = cg_catn(req, o, cookie, 32) 88 o = cg_cat(req, o, "\r\n" as *u8) 89 } 90 o = cg_cat(req, o, "Content-Length: " as *u8) 91 let bl: i64 = cg_len(body) 92 // integer print 93 let t: *u8 = sys_mmap(24) 94 var m: i64 = bl 95 var k: i64 = 0 96 if m == 0 { t[0] = 48 as u8; k = 1 } 97 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 98 var i: i64 = 0 99 while i < k { req[o] = t[k-1-i]; o = o + 1; i = i + 1 } 100 o = cg_cat(req, o, "\r\n\r\n" as *u8) 101 o = cg_cat(req, o, body) 102 return o 103} 104func cg_get(req: *u8, path: *u8, cookie: *u8) -> i64 { 105 var o: i64 = cg_cat(req, 0, "GET " as *u8) 106 o = cg_cat(req, o, path) 107 o = cg_cat(req, o, " HTTP/1.1\r\nHost: gate\r\n" as *u8) 108 if (cookie[0] as i64) != 0 { 109 o = cg_cat(req, o, "Cookie: nsess=" as *u8) 110 o = cg_catn(req, o, cookie, 32) 111 o = cg_cat(req, o, "\r\n" as *u8) 112 } 113 o = cg_cat(req, o, "\r\n" as *u8) 114 return o 115} 116 117func cg_row(id: i64, ok: i64, what: *u8) -> i64 { 118 cg_w("CMSROW " as *u8) 119 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 120 cg_w(d) 121 if ok == 1 { cg_w("PASS " as *u8) } 122 if ok == 0 { cg_w("FAIL " as *u8) } 123 cg_w(what); cg_w("\n" as *u8) 124 return ok 125} 126 127// mkdirat via runtime-computed x86 number (258): sys_mkdir's rv64 34 risked the untranslated-row 128// pause-hang (same class as the nxk_kill landmine; mkdirat row was absent on a 2026-06-10 lane). 129func cg_mkdir(path: *u8, mode: i64) -> i64 { 130 let nb: *i64 = sys_mmap(8) as *i64 131 nb[0] = 258 // x86_64 mkdirat, untranslated by design 132 return __syscall(nb[0], 0 - 100, path, mode, 0, 0, 0) 133} 134 135func main() -> i64 { 136 // ---- setup the gate site dir from the canonical andelinwest content ---- 137 cg_mkdir(CG_SITE, 0x1ed) 138 cst_copy("knowledge/cms/andelinwest/template.html" as *u8, "/tmp/_cms_gate_site/template.html" as *u8) 139 cst_copy("knowledge/cms/andelinwest/content.txt" as *u8, "/tmp/_cms_gate_site/content.txt" as *u8) 140 // remove stale revision/page state from prior runs: overwrite by fresh copies only (additive law: 141 // /tmp gate scratch, not production data). pw hash = sha256("gate-test-pw-1") 142 let dig: *u8 = sys_mmap(32) 143 sha256_digest("gate-test-pw-1" as *u8, 14, dig) 144 let hexs: *u8 = sys_mmap(80) 145 let hxc: *u8 = "0123456789abcdef" as *u8 146 var i: i64 = 0 147 while i < 32 { 148 hexs[i*2] = hxc[((dig[i] as i64) >> 4) & 15] 149 hexs[i*2+1] = hxc[(dig[i] as i64) & 15] 150 i = i + 1 151 } 152 let pf: i64 = sys_openat_wr("/tmp/_cms_gate_site/admin_pw.sha256" as *u8, 0x1a4) 153 sys_write(pf, hexs, 64) 154 sys_close(pf) 155 156 // ---- REBUILD the admin daemon sovereignly (prove-all doctrine: /tmp is contested; never 157 // trust a prebuilt binary). The runner runs it arg-less after build = harmless usage-exit. ---- 158 let bpid: i64 = sys_fork() 159 if bpid == 0 { 160 let dn: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4) 161 if dn >= 0 { sys_dup3(dn, 1, 0); sys_dup3(dn, 2, 0) } 162 let bargv: *i64 = sys_mmap(32) as *i64 163 bargv[0] = "_offc/nx_sov_build_run.elf" as *u8 as i64 164 bargv[1] = "nx_cms_admin" as *u8 as i64 165 bargv[2] = 0 166 let benvp: *i64 = sys_mmap(16) as *i64 167 benvp[0] = 0 168 sys_execve("_offc/nx_sov_build_run.elf" as *u8, bargv, benvp); sys_exit(127) 169 } 170 let bst: *i64 = sys_mmap(16) as *i64 171 sys_wait4(bpid, bst, 0) 172 // CALLER-GAP FIX 2026-08-01 (seq1267 class): the runner emits buildroot/_build/<t>.sov.elf under 173 // the no-/tmp doctrine while this probed /tmp. Proven by control: _build copy 208962B, /tmp absent. 174 var bchk: i64 = sys_openat_rd("buildroot/_build/nx_cms_admin.sov.elf" as *u8) 175 var badm: *u8 = "buildroot/_build/nx_cms_admin.sov.elf" as *u8 176 if bchk < 0 { bchk = sys_openat_rd("/tmp/nx_cms_admin.sov.elf" as *u8); badm = "/tmp/nx_cms_admin.sov.elf" as *u8 } 177 if bchk < 0 { cg_w("CMSGATE ADMIN-BUILD-MISSING (probed buildroot/_build AND /tmp)\n" as *u8); sys_exit(1) } 178 sys_close(bchk) 179 180 // ---- boot the real admin daemon ---- 181 let pid: i64 = sys_fork() 182 if pid == 0 { 183 let lg: i64 = sys_openat_wr("/tmp/_cms_admin_gate.log" as *u8, 0x1a4) 184 if lg >= 0 { sys_dup3(lg, 1, 0); sys_dup3(lg, 2, 0) } 185 let argvv: *i64 = sys_mmap(8*6) as *i64 186 argvv[0] = badm as i64 187 argvv[1] = "8077" as *u8 as i64 188 argvv[2] = CG_SITE as i64 189 argvv[3] = "99" as *u8 as i64 190 argvv[4] = 0 191 let envp: *i64 = sys_mmap(16) as *i64 192 envp[0] = 0 193 sys_execve(badm, argvv, envp) 194 sys_exit(127) 195 } 196 197 let req: *u8 = sys_mmap(262144) 198 let resp: *u8 = sys_mmap(524288) 199 let nocookie: *u8 = sys_mmap(8) 200 nocookie[0] = 0 as u8 201 let sess: *u8 = sys_mmap(64) 202 sess[0] = 0 as u8 203 let csrf: *u8 = sys_mmap(64) 204 csrf[0] = 0 as u8 205 206 // R0: seed page serves (also the boot-poll: retry till the daemon accepts) 207 var rn: i64 = 0 - 1 208 var tries: i64 = 0 209 while tries < 50 { 210 let rl: i64 = cg_get(req, "/" as *u8, nocookie) 211 rn = cg_http(req, rl, resp, 524287) 212 if rn > 0 { tries = 50 } 213 if rn <= 0 { sys_sleep_ms(100); tries = tries + 1 } 214 } 215 var pass: i64 = 0 216 var rows: i64 = 0 217 var ok: i64 = 0 218 ok = 0 219 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 } } } 220 rows = rows + 1; pass = pass + cg_row(0, ok, "seed render serves GET /" as *u8) 221 222 // R1: GET /admin unauthed -> login form, NO editor fields leak 223 var rl: i64 = cg_get(req, "/admin" as *u8, nocookie) 224 rn = cg_http(req, rl, resp, 524287) 225 ok = 0 226 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 } } } 227 rows = rows + 1; pass = pass + cg_row(1, ok, "unauthed /admin = login only" as *u8) 228 229 // R2: unauth POST /admin/save -> 403 230 rl = cg_post(req, "/admin/save" as *u8, nocookie, "csrf=00000000000000000000000000000000&hero_title=HACKED" as *u8) 231 rn = cg_http(req, rl, resp, 524287) 232 ok = 0 233 if rn > 0 { if cg_has(resp, rn, "403" as *u8) == 1 { ok = 1 } } 234 rows = rows + 1; pass = pass + cg_row(2, ok, "unauth save refused 403" as *u8) 235 236 // R3: wrong password -> 403 237 rl = cg_post(req, "/admin/login" as *u8, nocookie, "pw=wrong-password" as *u8) 238 rn = cg_http(req, rl, resp, 524287) 239 ok = 0 240 if rn > 0 { if cg_has(resp, rn, "403" as *u8) == 1 { ok = 1 } } 241 rows = rows + 1; pass = pass + cg_row(3, ok, "wrong password refused 403" as *u8) 242 243 // R4: correct password -> 303 + session cookie 244 rl = cg_post(req, "/admin/login" as *u8, nocookie, "pw=gate-test-pw-1" as *u8) 245 rn = cg_http(req, rl, resp, 524287) 246 ok = 0 247 if rn > 0 { if cg_has(resp, rn, "303" as *u8) == 1 { 248 let sp: i64 = cg_index(resp, rn, "nsess=" as *u8) 249 if sp >= 0 { 250 var m2: i64 = 0 251 while m2 < 32 { sess[m2] = resp[sp+6+m2]; m2 = m2 + 1 } 252 sess[32] = 0 as u8 253 ok = 1 254 } 255 } } 256 rows = rows + 1; pass = pass + cg_row(4, ok, "login issues HttpOnly session" as *u8) 257 258 // R5: GET /admin with session -> editor with fields + CSRF 259 rl = cg_get(req, "/admin" as *u8, sess) 260 rn = cg_http(req, rl, resp, 524287) 261 ok = 0 262 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 { 263 let cp: i64 = cg_index(resp, rn, "name=\"csrf\" value=\"" as *u8) 264 if cp >= 0 { 265 var m3: i64 = 0 266 while m3 < 32 { csrf[m3] = resp[cp+19+m3]; m3 = m3 + 1 } 267 csrf[32] = 0 as u8 268 ok = 1 269 } 270 } } } 271 rows = rows + 1; pass = pass + cg_row(5, ok, "editor shows fields + csrf" as *u8) 272 273 // R6: save with session but WRONG csrf -> 403 274 rl = cg_post(req, "/admin/save" as *u8, sess, "csrf=00000000000000000000000000000000&hero_title=FORGED" as *u8) 275 rn = cg_http(req, rl, resp, 524287) 276 ok = 0 277 if rn > 0 { if cg_has(resp, rn, "403" as *u8) == 1 { ok = 1 } } 278 rows = rows + 1; pass = pass + cg_row(6, ok, "cross-site forged save refused (CSRF)" as *u8) 279 280 // R7: REAL EDIT: new headline + note with injected <script> (and legit <b>) 281 let body7: *u8 = sys_mmap(4096) 282 var b7: i64 = cg_cat(body7, 0, "csrf=" as *u8) 283 b7 = cg_catn(body7, b7, csrf, 32) 284 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) 285 body7[b7] = 0 as u8 286 rl = cg_post(req, "/admin/save" as *u8, sess, body7) 287 rn = cg_http(req, rl, resp, 524287) 288 ok = 0 289 if rn > 0 { if cg_has(resp, rn, "303" as *u8) == 1 { ok = 1 } } 290 rows = rows + 1; pass = pass + cg_row(7, ok, "legal-team edit publishes 303" as *u8) 291 292 // R8: the live page reflects the edit, keeps <b>, and the script is GONE 293 rl = cg_get(req, "/" as *u8, nocookie) 294 rn = cg_http(req, rl, resp, 524287) 295 // SPLIT 2026-08-07. This was ONE row ANDing three conditions, so a failure said only 296 // "edit LIVE + stored-XSS sanitized FAIL" and left the reader to guess which third broke -- 297 // and the natural guess is the alarming one. A COMPOUND ASSERTION THAT WILL NOT NAME THE 298 // FAILING CONJUNCT IS A FALSE-ALARM GENERATOR: read as an XSS hole, it is far more likely to 299 // be the edit simply not reaching the page. Three named rows cost nothing and diagnose themselves. 300 var ok8a: i64 = 0 301 var ok8b: i64 = 0 302 var ok8c: i64 = 0 303 if rn > 0 { 304 if cg_has(resp, rn, "Newly Edited" as *u8) == 1 { ok8a = 1 } 305 if cg_has(resp, rn, "<b>Spring hours posted.</b>" as *u8) == 1 { ok8b = 1 } 306 if cg_has(resp, rn, "<script" as *u8) == 0 { ok8c = 1 } 307 } 308 rows = rows + 1; pass = pass + cg_row(8, ok8a, "edit REACHES the live page (title updated)" as *u8) 309 rows = rows + 1; pass = pass + cg_row(9, ok8b, "benign markup SURVIVES sanitize (<b> kept)" as *u8) 310 rows = rows + 1; pass = pass + cg_row(10, ok8c, "SECURITY stored-XSS stripped (no <script)" as *u8) 311 312 // R9: rollback -> 303 313 let body9: *u8 = sys_mmap(256) 314 var b9: i64 = cg_cat(body9, 0, "csrf=" as *u8) 315 b9 = cg_catn(body9, b9, csrf, 32) 316 body9[b9] = 0 as u8 317 rl = cg_post(req, "/admin/rollback" as *u8, sess, body9) 318 rn = cg_http(req, rl, resp, 524287) 319 ok = 0 320 if rn > 0 { if cg_has(resp, rn, "303" as *u8) == 1 { ok = 1 } } 321 rows = rows + 1; pass = pass + cg_row(11, ok, "rollback accepted" as *u8) 322 323 // R10: prior publish restored 324 rl = cg_get(req, "/" as *u8, nocookie) 325 rn = cg_http(req, rl, resp, 524287) 326 ok = 0 327 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 } } } 328 rows = rows + 1; pass = pass + cg_row(12, ok, "revision rollback restores prior publish" as *u8) 329 330 // ---- teardown our own child (nxk_kill = runtime-computed x86 kill, the syscall-table escape hatch) ---- 331 nxk_kill(pid, 9) 332 let stbuf: *i64 = sys_mmap(16) as *i64 333 sys_wait4(pid, stbuf, 0) 334 335 let permil: i64 = pass * 1000 / rows 336 let line: *u8 = sys_mmap(512) 337 var lo: i64 = cg_cat(line, 0, "CMSGATE target=andelinwest rows=" as *u8) 338 let t2: *u8 = sys_mmap(24) 339 var mm: i64 = rows 340 var kk: i64 = 0 341 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 342 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 343 var ii: i64 = 0 344 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 345 lo = cg_cat(line, lo, " pass=" as *u8) 346 mm = pass; kk = 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 ii = 0 350 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 351 lo = cg_cat(line, lo, " permil=" as *u8) 352 mm = permil; 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 = cg_cat(line, lo, " epoch=" as *u8) 358 mm = sys_now_realtime_sec(); kk = 0 359 if mm == 0 { t2[0] = 48 as u8; kk = 1 } 360 while mm > 0 { t2[kk] = (48 + (mm % 10)) as u8; mm = mm / 10; kk = kk + 1 } 361 ii = 0 362 while ii < kk { line[lo] = t2[kk-1-ii]; lo = lo + 1; ii = ii + 1 } 363 // Canonical verdict token, appended LAST (nx_gate_rollup's gv_last_line anchors by POSITION). 364 // The rollup reads THIS LOG, not the exit code, and a row carrying no verdict= is fail-closed as 365 // not-passing -- so this gate was emitting rows=13 pass=13 permil=1000 and being counted RED in the 366 // fleet number: a gate that passes every tooth and is scored as a failure. The rich domain fields 367 // stay exactly as they were; the fix is in the EMITTER, never by widening the reader's vocabulary. 368 if permil == 1000 { lo = cg_cat(line, lo, " verdict=GREEN" as *u8) } else { lo = cg_cat(line, lo, " verdict=RED" as *u8) } 369 line[lo] = 10 as u8 370 lo = lo + 1 371 sys_write(1, line, lo) 372 let gf: i64 = sys_openat_append("knowledge/status/cms_gate.log" as *u8, 0x1a4) 373 if gf >= 0 { sys_write(gf, line, lo); sys_close(gf) } 374 // MIGRATED onto nx_gate_verdict (D001). No helper renames needed -- this gate names its locals cg_*, 375 // so there was no collision with the base class. Every check above is untouched, so the PASS/FAIL 376 // vector cannot change; only the hand-rolled exit is replaced, so /api/gate_run and the harness 377 // frame can READ the outcome. The denominator is `rows`, derived from the teeth actually run, never 378 // a literal -- add a tooth and the counter moves by itself instead of printing 14/13. 379 let ctr__dry: *i64 = gv_ctr() 380 ctr__dry[0] = pass 381 ctr__dry[1] = rows 382 let rc__dry: i64 = gv_verdict("CMS-GATE" as *u8, ctr__dry, "teeth unchanged; verdict emission migrated onto the shared base class" as *u8) 383 sys_exit(rc__dry) 384 return rc__dry 385}