code wiki / _hdl_build / nx_opaque_login_smoke.nx

nx_opaque_login_smoke.nx source

↩ module page · 230 lines · 14396 B

1// nx_opaque_login_smoke.nx -- R2 smoke: fork the OPAQUE login daemon, drive it as a REAL HTTP client over a 2// socket (POST /register -> POST /login -> GET /whoami[valid] -> GET /whoami[bad token]), proving the 3// no-cookie OPAQUE login works end-to-end over the wire. Requires /tmp/nx_opaque_login_daemon.sov.elf built 4// in the same WSL session (the WOMB emits there). exit 0 = pass, N = assertion N failed. 5// TOOTH 5 (2026-08-18, lane F): the daemon must not GROW under load -- SMK_LEAK_N warm GET / requests must 6// leave its VmSize where the warm-up left it. Bite-proven against the pre-fix binary: every response mapped a 7// fresh 2 MiB compose buffer, so 100 requests moved VmSize by ~204,800 kB; the tolerance is TWO syscall-arena 8// chunks (2 x 256 KiB), the only allocation a warm request path may still legitimately cross. 9import "nx_syscalls.nx" 10import "nx_connect.nx" // bounded connect: a raw sys_connect hangs ~127s on a black-holed host 11import "nx_assert.nx" 12import "nx_hr_admin.nx" // hra_invite -- the smoke provisions its OWN invite so the LAN+invite signup gate can pass 13const SMK_MAGIC_32768: i64 = 32768 14const SMK_MAGIC_2048: i64 = 2048 15const SMK_MAGIC_32767: i64 = 32767 16 17// PORT IS PROBED, NOT ASSUMED (2026-08-18): a fixed port made the smoke BIND-FAIL against a foreign 18// listener on the NAS and then drive its protocol INTO that foreign service (the readiness poll answered, 19// so the run looked live while the subject was dead). smk_pick_port bind-tests from SMK_PORT_BASE upward 20// and every later request uses the port that actually bound. The probe socket closes before the daemon 21// forks; the window between close and the child's bind is the accepted imprecision -- a loser re-run, 22// never a wrong-subject run, because the daemon's own BIND-FAIL exit makes readiness time out (exit 12). 23const SMK_PORT_BASE: i64 = 18099 24const SMK_PORT_SPAN: i64 = 32 25static smk_port_v: i64 26const SMK_LEAK_N: i64 = 100 // requests driven for the growth tooth 27const SMK_LEAK_TOL_KB: i64 = 512 // 2 x NXA_CHUNK (256 KiB) -- the arena may roll one chunk under 100 requests 28const SMK_BUDGET: *u8 = "200" as *u8 // daemon request budget: 4 protocol requests + warm-up + SMK_LEAK_N, with slack 29const SMK_STATBUF: i64 = 4096 30const SMK_READY_TRIES: i64 = 60 31const SMK_READY_MS: i64 = 250 32// DEFAULT daemon path (a WSL womb build); argv[1] OVERRIDES it -- on the NAS the builder emits 33// buildroot/_build/<t>.sov.elf, so a fixed /tmp path there is a fossil that reads as "connect refused". 34const DAEMON_ELF: *u8 = "/tmp/nx_opaque_login_daemon.sov.elf" as *u8 35// SIGNUP GATE FIXTURE (2026-08-18): registration requires LAN peer + an INVITED handle in the daemon's HR roster 36// (nx_lan_signup, argv[9]) + the registration_open flag. The old smoke forked the daemon with 5 args (no roster), 37// so tooth 1 could only ever pass on a build that predates the gate. The smoke now writes its own one-invite 38// roster under a per-run prefix and hands it to the daemon -- the fixture REACHES the condition it asserts. 39const SMK_HR_PREFIX: *u8 = "/tmp/nx_olgd_smoke_hr" as *u8 // + "-<epoch>-" per run: a fresh, empty store every time 40const SMK_REALM: *u8 = "nishi_site_admin" as *u8 41const SMK_REALM_N: i64 = 16 42const SMK_HANDLE: *u8 = "smoke" as *u8 43const SMK_HANDLE_N: i64 = 5 44 45func smk_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 46// first port in [base, base+span) that BINDS on 0.0.0.0 right now; -1 when none do. 47func smk_pick_port(base: i64, span: i64) -> i64 { 48 var t: i64 = 0 49 while t < span { 50 let cand: i64 = base + t 51 let fd: i64 = sys_socket(2, 1, 0) 52 if fd >= 0 { 53 let a: *u8 = sys_mmap(16) 54 a[0]=2 as u8; a[1]=0 as u8; a[2]=((cand>>8)&0xff) as u8; a[3]=(cand&0xff) as u8 55 var zi: i64=4; while zi<16 { a[zi]=0 as u8; zi=zi+1 } 56 let rc: i64 = sys_bind(fd, a, 16) 57 sys_close(fd) 58 if rc >= 0 { return cand } 59 } 60 t = t + 1 61 } 62 return 0 - 1 63} 64func smk_find(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 { 65 if nl==0 { return 0 } 66 var i: i64=0 67 while i+nl<=n { var j: i64=0; var ok: i64=1; while j<nl { if buf[i+j]!=needle[j]{ok=0;j=nl} else {j=j+1} } if ok==1 {return i} i=i+1 } 68 return 0-1 69} 70// VmSize (kB) of pid from /proc/<pid>/status; -1 if unreadable. One bounded read, no per-call growth. 71func smk_vmsize_kb(pid: i64, scr: *u8) -> i64 { 72 var o: i64 = 0 73 let p1: *u8 = "/proc/" as *u8; var i: i64 = 0; while p1[i] != (0 as u8) { scr[o] = p1[i]; o = o + 1; i = i + 1 } 74 let t: *u8 = ((scr as i64) + 256) as *u8; var m: i64 = pid; var k: i64 = 0 75 if m == 0 { t[0] = 48 as u8; k = 1 } 76 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 77 var q: i64 = k - 1; while q >= 0 { scr[o] = t[q]; o = o + 1; q = q - 1 } 78 let p2: *u8 = "/status" as *u8; i = 0; while p2[i] != (0 as u8) { scr[o] = p2[i]; o = o + 1; i = i + 1 } 79 scr[o] = 0 as u8 80 let fd: i64 = sys_openat_rd(scr); if fd < 0 { return 0 - 1 } 81 let buf: *u8 = ((scr as i64) + 512) as *u8 82 let n: i64 = sys_read(fd, buf, SMK_STATBUF - 512 - 1); sys_close(fd) 83 if n <= 0 { return 0 - 1 } 84 let key: *u8 = "VmSize:" as *u8 85 let at: i64 = smk_find(buf, n, key, 7); if at < 0 { return 0 - 1 } 86 var j: i64 = at + 7; var v: i64 = 0; var seen: i64 = 0; var go: i64 = 1 87 while go == 1 { if j >= n { go = 0 } else { let c: i64 = buf[j] as i64; if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); seen = 1; j = j + 1 } else { if seen == 1 { go = 0 } else { j = j + 1 } } } else { if seen == 1 { go = 0 } else { j = j + 1 } } } } 88 return v 89} 90func smk_trunc(path: *u8) -> i64 { let fd: i64=sys_openat_wr(path, 0x1a4); if fd>=0 { sys_close(fd) } return 0 } 91 92// connect 127.0.0.1:SMK_PORT, send req[0..reqn], read response into resp -> resp length (or <0). 93func smk_http(req: *u8, reqn: i64, resp: *u8, cap: i64) -> i64 { 94 let fd: i64 = sys_socket(2, 1, 0); if fd < 0 { return 0-1 } 95 sys_set_socket_timeout(fd, 5) 96 let a: *u8 = sys_mmap(16) 97 a[0]=2 as u8; a[1]=0 as u8; a[2]=((smk_port_v>>8)&0xff) as u8; a[3]=(smk_port_v&0xff) as u8 98 a[4]=127 as u8; a[5]=0 as u8; a[6]=0 as u8; a[7]=1 as u8 99 var zi: i64=8; while zi<16 { a[zi]=0 as u8; zi=zi+1 } 100 if nx_connect_bounded(fd, a, 16, NX_CONN_DEFAULT_MS) != 0 { sys_close(fd); return 0-2 } 101 sys_write(fd, req, reqn) 102 var off: i64=0; var go: i64=1 103 while go==1 { let r: i64=sys_read(fd, ((resp as i64)+off) as *u8, cap-off); if r<=0 {go=0} else { off=off+r; if off>=cap {go=0} } } 104 sys_close(fd); return off 105} 106// build a POST with form body; returns request length into reqbuf. 107func smk_post(reqbuf: *u8, path: *u8, body: *u8) -> i64 { 108 let bl: i64 = smk_slen(body) 109 var o: i64=0 110 let p1: *u8="POST " as *u8; var i: i64=0; while p1[i]!=(0 as u8){reqbuf[o]=p1[i];o=o+1;i=i+1} 111 i=0; while path[i]!=(0 as u8){reqbuf[o]=path[i];o=o+1;i=i+1} 112 let p2: *u8=" HTTP/1.1\r\nHost: x\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: " as *u8; i=0; while p2[i]!=(0 as u8){reqbuf[o]=p2[i];o=o+1;i=i+1} 113 let t: *u8=sys_mmap(28); var m: i64=bl; var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var q: i64=k-1; while q>=0{reqbuf[o]=t[q];o=o+1;q=q-1} 114 let p3: *u8="\r\nConnection: close\r\n\r\n" as *u8; i=0; while p3[i]!=(0 as u8){reqbuf[o]=p3[i];o=o+1;i=i+1} 115 i=0; while body[i]!=(0 as u8){reqbuf[o]=body[i];o=o+1;i=i+1} 116 return o 117} 118 119func main(argc: i64, margv: *i64) -> i64 { 120 var delf: *u8 = DAEMON_ELF 121 if argc > 1 { delf = margv[1] as *u8 } 122 smk_trunc("/tmp/nx_olgd_keys.log" as *u8) 123 smk_trunc("/tmp/nx_olgd_store.log" as *u8) 124 125 // per-run HR roster prefix + the one invite the register tooth needs 126 let hrp: *u8 = sys_mmap(SMK_MAGIC_2048); var ho: i64 = 0 127 var hi: i64 = 0; while SMK_HR_PREFIX[hi] != (0 as u8) { hrp[ho] = SMK_HR_PREFIX[hi]; ho = ho + 1; hi = hi + 1 } 128 hrp[ho] = 45 as u8; ho = ho + 1 129 let now0: i64 = sys_now_realtime_sec() 130 let td: *u8 = sys_mmap(28); var tm: i64 = now0; var tk: i64 = 0 131 if tm == 0 { td[0] = 48 as u8; tk = 1 } 132 while tm > 0 { td[tk] = (48 + (tm % 10)) as u8; tm = tm / 10; tk = tk + 1 } 133 var tq: i64 = tk - 1; while tq >= 0 { hrp[ho] = td[tq]; ho = ho + 1; tq = tq - 1 } 134 hrp[ho] = 45 as u8; ho = ho + 1; hrp[ho] = 0 as u8 135 let cid: *u8 = sys_mmap(96) 136 let irc: i64 = hra_invite(hrp, SMK_REALM, SMK_REALM_N, SMK_HANDLE, SMK_HANDLE_N, 1, "smoke-family" as *u8, now0, "nx_opaque_login_smoke" as *u8, cid) 137 nx_puts_err("invite rc="); nx_puti_err(irc); nx_puts_err(" roster="); nx_puts_err(hrp); nx_puts_err("\n" as *u8) 138 139 smk_port_v = smk_pick_port(SMK_PORT_BASE, SMK_PORT_SPAN) 140 if smk_port_v < 0 { nx_puts_err("no free port in the probe span\n" as *u8); return 13 } 141 let ports: *u8 = sys_mmap(28) 142 var pm: i64 = smk_port_v; var pk: i64 = 0 143 while pm > 0 { ports[pk] = (48 + (pm % 10)) as u8; pm = pm / 10; pk = pk + 1 } 144 var pq: i64 = 0; while pq < pk / 2 { let tc: u8 = ports[pq]; ports[pq] = ports[pk-1-pq]; ports[pk-1-pq] = tc; pq = pq + 1 } 145 ports[pk] = 0 as u8 146 nx_puts_err("smoke port="); nx_puti_err(smk_port_v); nx_puts_err("\n" as *u8) 147 148 let pid: i64 = sys_fork() 149 if pid == 0 { 150 let argv: *i64 = sys_mmap(96) as *i64 151 argv[0]=delf as i64; argv[1]=ports as i64; argv[2]="/tmp/nx_olgd_keys.log" as *u8 as i64 152 argv[3]="/tmp/nx_olgd_store.log" as *u8 as i64; argv[4]=SMK_BUDGET as i64; argv[5]="8192" as *u8 as i64 153 argv[6]="2" as *u8 as i64; argv[7]="1" as *u8 as i64; argv[8]="/tmp" as *u8 as i64; argv[9]=hrp as i64; argv[10]=0 154 let envp: *i64 = sys_mmap(16) as *i64; envp[0]="PATH=/usr/bin:/bin" as *u8 as i64; envp[1]=0 155 sys_execve(delf, argv, envp) 156 sys_exit(127) 157 } 158 let resp: *u8 = sys_mmap(SMK_MAGIC_32768) 159 let req: *u8 = sys_mmap(SMK_MAGIC_2048) 160 161 // READINESS: poll until the daemon ANSWERS, bounded by SMK_READY_TRIES polls of SMK_READY_MS each. 162 // First-run server-key generation outlived the old fixed 500 ms sleep on a loaded box, and the 163 // connect refusal then read as a register failure (exit 1) -- a fixture that never reached its condition. 164 let gq0: *u8 = "GET / HTTP/1.1 165Host: x 166Connection: close 167 168" as *u8 169 let gq0n: i64 = smk_slen(gq0) 170 var ready: i64 = 0; var tries: i64 = 0 171 while ready == 0 { if tries >= SMK_READY_TRIES { ready = 2 } else { sys_sleep_ms(SMK_READY_MS); let pr: i64 = smk_http(gq0, gq0n, resp, SMK_MAGIC_32767); if pr > 0 { ready = 1 } else { tries = tries + 1 } } } 172 if ready == 2 { nx_puts_err("daemon never answered on the smoke port 173" as *u8); nx_kill(pid,9); return 12 } 174 175 // 1. register 176 let rqn: i64 = smk_post(req, "/register" as *u8, "handle=smoke&pw=testpass123" as *u8) 177 let r1: i64 = smk_http(req, rqn, resp, SMK_MAGIC_32767) 178 nx_puts_err("register resp bytes="); nx_puti_err(r1) 179 if smk_find(resp, r1, "200 OK" as *u8, 6) < 0 { nx_kill(pid,9); return 1 } 180 if smk_find(resp, r1, "mnemonic" as *u8, 8) < 0 { nx_kill(pid,9); return 2 } 181 182 // 2. login -> extract the no-cookie token 183 let lqn: i64 = smk_post(req, "/login" as *u8, "handle=smoke&pw=testpass123" as *u8) 184 let r2: i64 = smk_http(req, lqn, resp, SMK_MAGIC_32767) 185 if smk_find(resp, r2, "200 OK" as *u8, 6) < 0 { nx_kill(pid,9); return 3 } 186 let tp: i64 = smk_find(resp, r2, "\"token\":\"" as *u8, 9) 187 if tp < 0 { nx_kill(pid,9); return 4 } 188 let tok: *u8 = sys_mmap(512); var ti: i64=0; var si: i64=tp+9 189 while si < r2 { if resp[si]==(34 as u8) { si=r2 } else { tok[ti]=resp[si]; ti=ti+1; si=si+1 } } 190 tok[ti]=0 as u8 191 nx_puts_err("\nlogin OK; token chars="); nx_puti_err(ti) 192 193 // 3. whoami with the valid token -> 200 + uid 194 let wq: *u8 = sys_mmap(SMK_MAGIC_2048); var o: i64=0 195 let h1: *u8="GET /whoami HTTP/1.1\r\nHost: x\r\nX-Nishi-Session: " as *u8; var i: i64=0; while h1[i]!=(0 as u8){wq[o]=h1[i];o=o+1;i=i+1} 196 i=0; while tok[i]!=(0 as u8){wq[o]=tok[i];o=o+1;i=i+1} 197 let h2: *u8="\r\nConnection: close\r\n\r\n" as *u8; i=0; while h2[i]!=(0 as u8){wq[o]=h2[i];o=o+1;i=i+1} 198 let r3: i64 = smk_http(wq, o, resp, SMK_MAGIC_32767) 199 if smk_find(resp, r3, "200 OK" as *u8, 6) < 0 { nx_kill(pid,9); return 5 } 200 if smk_find(resp, r3, "uid" as *u8, 3) < 0 { nx_kill(pid,9); return 6 } 201 nx_puts_err("\nwhoami(valid) -> 200 + uid\n" as *u8) 202 203 // 4. whoami with a bad token -> 401 204 let bq: *u8 = sys_mmap(512) 205 let b1: *u8="GET /whoami HTTP/1.1\r\nHost: x\r\nX-Nishi-Session: AAAAAAAAAAAAAAAAAAAAAAAA\r\nConnection: close\r\n\r\n" as *u8 206 var bo: i64=0; i=0; while b1[i]!=(0 as u8){bq[bo]=b1[i];bo=bo+1;i=i+1} 207 let r4: i64 = smk_http(bq, bo, resp, SMK_MAGIC_32767) 208 if smk_find(resp, r4, "401" as *u8, 3) < 0 { nx_kill(pid,9); return 7 } 209 nx_puts_err("whoami(bad token) -> 401 rejected\n" as *u8) 210 211 // 5. GROWTH TOOTH: warm up with one GET /, sample VmSize, drive SMK_LEAK_N more, sample again. 212 let scr: *u8 = sys_mmap(SMK_STATBUF) 213 let gq: *u8 = "GET / HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n" as *u8 214 let gqn: i64 = smk_slen(gq) 215 let rw: i64 = smk_http(gq, gqn, resp, SMK_MAGIC_32767) 216 if smk_find(resp, rw, "200 OK" as *u8, 6) < 0 { nx_kill(pid,9); return 8 } 217 let vm0: i64 = smk_vmsize_kb(pid, scr) 218 var li: i64 = 0; var lok: i64 = 1 219 while li < SMK_LEAK_N { let rl: i64 = smk_http(gq, gqn, resp, SMK_MAGIC_32767); if rl <= 0 { lok = 0; li = SMK_LEAK_N } else { li = li + 1 } } 220 let vm1: i64 = smk_vmsize_kb(pid, scr) 221 nx_puts_err("growth tooth: requests="); nx_puti_err(SMK_LEAK_N); nx_puts_err(" vmsize_kb_before="); nx_puti_err(vm0) 222 nx_puts_err(" after="); nx_puti_err(vm1); nx_puts_err(" delta_kb="); nx_puti_err(vm1 - vm0); nx_puts_err(" tol_kb="); nx_puti_err(SMK_LEAK_TOL_KB); nx_puts_err("\n" as *u8) 223 if lok == 0 { nx_kill(pid,9); return 9 } // a request failed mid-drive: not a growth verdict 224 if vm0 < 0 { nx_kill(pid,9); return 10 } // could not observe: refuse, do not acquit 225 if vm1 - vm0 > SMK_LEAK_TOL_KB { nx_kill(pid,9); return 11 } // THE LEAK: address space grew under load 226 227 let st: *i64 = sys_mmap(16) as *i64; nx_kill(pid, 9); sys_wait4(pid, st, 0) 228 nx_puts_err("nx_opaque_login_smoke verdict=GREEN pass=5 (HTTP register/login/whoami-valid/bad-token over the wire + no VmSize growth under load)\n" as *u8) 229 return 0 230}