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}