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¬e_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}