code wiki / _hdl_build / nx_tool_run_timeout_gate.nx

nx_tool_run_timeout_gate.nx source

↩ module page · 723 lines · 32486 B

1// nx_tool_run_timeout_gate.nx -- proves tr_run_capture_to actually BOUNDS a hanging child (seq1412). 2// 3// The claim under test is not "the function returns a timeout constant" -- that is trivial to fake. It is 4// "a child that would run for 10 seconds is KILLED at the deadline, its output is still captured, it is 5// REAPED rather than orphaned, and a fast child is NOT falsely timed out". So every tooth measures 6// something an empty implementation would fail: 7// 8// T1 the deadline FIRES -- 10s sleeper, 500ms budget -> TR_ERR_TIMEOUT 9// T2 the deadline is REAL TIME -- that call returns in well under the sleeper's 10s (the load-bearing 10// tooth: returning -5 after waiting the full 10s would be a lie) 11// T3 NEG-CONTROL fast child -- a 50ms sleeper with a 5s budget exits 0, NOT timed out 12// T4 output survives -- the fast child's stdout is captured intact 13// T5 REPEATABLE -- a second timeout run behaves identically; resource-leak 14// accounting requires separate lifecycle evidence 15// T6 opt-out is explicit -- timeout_ms<=0 delegates to the unbounded path and still succeeds 16// 17// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 18import "nx_syscalls.nx" 19import "nx_tool_run.nx" 20import "nx_os_proc.nx" 21import "nx_gate_verdict.nx" 22static TG_COUNTER: *i64 23// Linux resource ABI, not a workload policy. 24const TG_RLIMIT_NOFILE: i64 = 7 25// Regression witness beyond the former descriptor cleanup boundary of 1024. 26const TG_HIGH_FD_WITNESS: i64 = 2048 27const TG_CHILDREN_READ_WINDOW: i64 = 4096 28 29const TG_STDOUT: i64 = 1 30const TG_SLEEPER: *u8 = "_build/nx_tr_sleeper.sov.elf" 31const TG_OUTCAP: i64 = 65536 32 33// A 10s sleeper against a 500ms budget. If the kill did not happen the call cannot return before 10s. 34const TG_HANG_MS: i64 = 10000 35const TG_BUDGET_MS: i64 = 500 36// Generous ceiling: anything under this proves we did not sit out the full sleep. Deliberately far from 37// both 0.5s and 10s so neither scheduler jitter nor a slow NAS can flip the verdict either way. 38const TG_MAX_ELAPSED_S: i64 = 5 39const TG_FAST_MS: i64 = 50 40const TG_FAST_BUDGET_MS: i64 = 5000 41 42func tg_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(TG_STDOUT, s, n); return 0 } 43func tg_num(v: i64) -> i64 { 44 var m: i64 = v 45 if m < 0 { m = 0 - m; sys_write(TG_STDOUT, "-" as *u8, 1) } 46 let t: *u8 = sys_mmap(28) 47 var k: i64 = 0 48 if m == 0 { t[0] = 48 as u8; k = 1 } 49 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 50 let o: *u8 = sys_mmap(28) 51 var i: i64 = 0 52 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 53 sys_write(TG_STDOUT, o, k) 54 return 0 55} 56func tg_check(ok: i64, label: *u8, got: i64, want: i64) -> i64 { 57 gv_check(label,ok,TG_COUNTER) 58 if ok == 1 { tg_puts(" PASS " as *u8) } else { tg_puts(" FAIL " as *u8) } 59 tg_puts(label) 60 tg_puts(" got=" as *u8); tg_num(got) 61 tg_puts(" want=" as *u8); tg_num(want) 62 tg_puts("\n" as *u8) 63 if ok == 1 { return 0 } 64 return 1 65} 66 67// run the sleeper for ms with a budget; returns tr_run_capture_to's result 68func tg_run(ms: *u8, budget: i64, out: *u8, ol: *i64) -> i64 { 69 let av: *i64 = sys_mmap(32) as *i64 70 av[0] = TG_SLEEPER as i64 71 av[1] = ms as *u8 as i64 72 av[2] = 0 73 return tr_run_capture_to(TG_SLEEPER, av, out, TG_OUTCAP, ol, budget) 74} 75 76 77const TG_STREAM_CAP: i64 = 16384 78const TG_STREAM_BLOCKS: i64 = 16 79const TG_STREAM_EXIT: i64 = 23 80const TG_SYS_GETPID: i64 = 172 // portable syscall ABI used by nx_getpid_const_probe 81func tg_stream_child(mode: i64) -> i64 { 82 if mode == 102 { let probe: *u8=sys_mmap(16); if sys_read(2048,probe,1) == (0-9) { return 0 }; return 31 } 83 if mode == 114 { return 126 } 84 if mode == 107 { 85 let self: i64=__syscall(TG_SYS_GETPID,0,0,0,0,0,0) 86 if self <= 1 { return 27 } 87 nx_kill(self,TR_SIGKILL); return 28 88 } 89 if mode == 120 { sys_close(1); sys_close(2); sys_sleep_ms(TG_HANG_MS); return 29 } 90 if mode == 103 { 91 let helper: i64=sys_fork() 92 if helper < 0 { return 26 } 93 if helper == 0 { sys_sleep_ms(TG_HANG_MS); sys_exit(0); return 0 } 94 } 95 let block: *u8 = sys_mmap(TG_STREAM_CAP) 96 var i: i64 = 0 97 while i < TG_STREAM_CAP { block[i] = 65 as u8; i=i+1 } 98 var remaining: i64 = TG_STREAM_CAP 99 if mode == 98 || mode == 104 || mode == 103 { remaining = TG_STREAM_CAP*TG_STREAM_BLOCKS } 100 if mode == 115 { remaining = TG_STREAM_CAP-1 } 101 while remaining > 0 { 102 var n: i64 = remaining 103 if n > TG_STREAM_CAP { n = TG_STREAM_CAP } 104 let wrote: i64 = sys_write(1,block,n) 105 if wrote <= 0 { return 24 } 106 remaining = remaining-wrote 107 } 108 if mode == 104 { sys_sleep_ms(TG_HANG_MS) } 109 return TG_STREAM_EXIT 110} 111func tg_stream_check(self: *u8, mode: *u8, traced: i64, wantcut: i64, wantlen: i64) -> i64 { 112 let av: *i64 = sys_mmap(32) as *i64 113 av[0]=self as i64; av[1]=mode as i64; av[2]=0 114 let buf: *u8 = sys_mmap(TG_STREAM_CAP+1) 115 buf[TG_STREAM_CAP]=90 as u8 116 let len: *i64 = sys_mmap(16) as *i64 117 let cut: *i64 = sys_mmap(16) as *i64 118 var rc: i64 = 0 119 if traced == 1 { 120 rc=tr_run_capture_tr(self,av,buf,TG_STREAM_CAP,len,TG_FAST_BUDGET_MS,cut) 121 } else { 122 rc=tr_run_capture_to(self,av,buf,TG_STREAM_CAP,len,TG_FAST_BUDGET_MS) 123 } 124 var fail: i64 = 0 125 var ok: i64 = 0; if rc == TG_STREAM_EXIT { ok=1 } 126 fail=fail+tg_check(ok,"stream child reaches post-output exit" as *u8,rc,TG_STREAM_EXIT) 127 ok=0; if len[0] == wantlen { ok=1 } 128 fail=fail+tg_check(ok,"captured prefix length" as *u8,len[0],wantlen) 129 ok=1 130 var i: i64=0 131 while i < len[0] { if buf[i] != 65 as u8 { ok=0 }; i=i+1 } 132 if buf[TG_STREAM_CAP] != 90 as u8 { ok=0 } 133 fail=fail+tg_check(ok,"prefix bytes and boundary canary" as *u8,ok,1) 134 if traced == 1 { 135 ok=0; if cut[0] == wantcut { ok=1 } 136 fail=fail+tg_check(ok,"observed truncation" as *u8,cut[0],wantcut) 137 } 138 return fail 139} 140 141 142func tg_stream_timeout(self: *u8) -> i64 { 143 let av: *i64 = sys_mmap(32) as *i64 144 av[0]=self as i64; av[1]="h" as *u8 as i64; av[2]=0 145 let buf: *u8=sys_mmap(TG_STREAM_CAP) 146 let len: *i64=sys_mmap(16) as *i64 147 let start: i64=sys_now_realtime_sec() 148 let rc: i64=tr_run_capture_to(self,av,buf,TG_STREAM_CAP,len,TG_BUDGET_MS) 149 let elapsed: i64=sys_now_realtime_sec()-start 150 var ok: i64=0; if rc == TR_ERR_TIMEOUT { ok=1 } 151 var fails: i64=tg_check(ok,"deadline after overflow" as *u8,rc,TR_ERR_TIMEOUT) 152 ok=0; if elapsed < TG_MAX_ELAPSED_S { ok=1 } 153 fails=fails+tg_check(ok,"overflow timeout elapsed" as *u8,elapsed,TG_MAX_ELAPSED_S) 154 ok=0; if len[0] == TG_STREAM_CAP { ok=1 } 155 fails=fails+tg_check(ok,"overflow timeout prefix retained" as *u8,len[0],TG_STREAM_CAP) 156 return fails 157} 158 159func tg_invalid_capacity() -> i64 { 160 let av: *i64 = sys_mmap(32) as *i64 161 av[0]=TG_SLEEPER as i64; av[1]="50" as *u8 as i64; av[2]=0 162 let buf: *u8=sys_mmap(TG_STREAM_CAP) 163 let len: *i64=sys_mmap(16) as *i64 164 let cut: *i64=sys_mmap(16) as *i64 165 let rc: i64=tr_run_capture_tr(TG_SLEEPER,av,buf,0-1,len,TG_FAST_BUDGET_MS,cut) 166 var ok: i64=0; if rc == TR_ERR_DRAIN { ok=1 } 167 var fails: i64=tg_check(ok,"invalid capacity returns drain error" as *u8,rc,TR_ERR_DRAIN) 168 ok=0; if len[0] == 0 { ok=1 } 169 fails=fails+tg_check(ok,"error does not become negative byte count" as *u8,len[0],0) 170 return fails 171} 172 173func tg_descendant_deadline(self: *u8) -> i64 { 174 let av: *i64=sys_mmap(32) as *i64 175 av[0]=self as i64; av[1]="g" as *u8 as i64; av[2]=0 176 let buf: *u8=sys_mmap(TG_STREAM_CAP) 177 let len: *i64=sys_mmap(16) as *i64 178 let cut: *i64=sys_mmap(16) as *i64 179 let start: i64=sys_now_realtime_sec() 180 let rc: i64=tr_run_capture_tr(self,av,buf,TG_STREAM_CAP,len,TG_BUDGET_MS,cut) 181 let elapsed: i64=sys_now_realtime_sec()-start 182 var ok: i64=0; if elapsed < TG_MAX_ELAPSED_S { ok=1 } 183 var fails: i64=tg_check(ok,"inherited writer cannot extend deadline" as *u8,elapsed,TG_MAX_ELAPSED_S) 184 ok=0; if rc == TR_ERR_TIMEOUT { ok=1 } 185 fails=fails+tg_check(ok,"inherited writer reports deadline" as *u8,rc,TR_ERR_TIMEOUT) 186 return fails 187} 188 189func tg_lifecycle_exit(self: *u8, mode: *u8, budget: i64, expected: i64) -> i64 { 190 let av: *i64=sys_mmap(32) as *i64 191 av[0]=self as i64; av[1]=mode as i64; av[2]=0 192 let buf: *u8=sys_mmap(TG_STREAM_CAP) 193 let len: *i64=sys_mmap(16) as *i64 194 let start: i64=sys_now_realtime_sec() 195 let rc: i64=tr_run_capture_to(self,av,buf,TG_STREAM_CAP,len,budget) 196 let elapsed: i64=sys_now_realtime_sec()-start 197 var ok: i64=0; if rc == expected { ok=1 } 198 var fails: i64=tg_check(ok,"termination cause retained" as *u8,rc,expected) 199 ok=0; if elapsed < TG_MAX_ELAPSED_S { ok=1 } 200 fails=fails+tg_check(ok,"closed-output lifecycle is bounded" as *u8,elapsed,TG_MAX_ELAPSED_S) 201 return fails 202} 203 204func tg_wait_observe() -> i64 { 205 let pid: i64=sys_fork() 206 if pid < 0 { return 1 } 207 if pid == 0 { sys_exit(TG_STREAM_EXIT); return 0 } 208 let info: *u8=sys_mmap(NX_WAIT_SIGINFO_BYTES) 209 let status: *i64=sys_mmap(16) as *i64 210 let first: i64=sys_waitid(NX_WAIT_P_PID,pid,info,NX_WAIT_EXITED | NX_WAIT_NOWAIT) 211 var ok: i64=0; if first == 0 { ok=1 } 212 var fails: i64=tg_check(ok,"observe termination without reap",first,0) 213 let again: i64=sys_waitid(NX_WAIT_P_PID,pid,info,NX_WAIT_EXITED | NX_WAIT_NOWAIT) 214 ok=0; if again == 0 { ok=1 } 215 fails=fails+tg_check(ok,"observed child remains waitable",again,0) 216 let reaped: i64=tr_reap(pid,status) 217 ok=0; if reaped == pid { ok=1 } 218 fails=fails+tg_check(ok,"explicit reap retains ownership",reaped,pid) 219 let ec: i64=wait_status_rc(status[0]) 220 ok=0; if ec == TG_STREAM_EXIT { ok=1 } 221 fails=fails+tg_check(ok,"observation preserves exit status",ec,TG_STREAM_EXIT) 222 return fails 223} 224 225 226func tg_cwd_check(self: *u8, mode: *u8, cwd: *u8, expected: i64) -> i64 { 227 let av: *i64=sys_mmap(32) as *i64 228 av[0]=self as i64; av[1]=mode as i64; av[2]=0 229 let buf: *u8=sys_mmap(TG_STREAM_CAP) 230 let len: *i64=sys_mmap(16) as *i64 231 var budget: i64=TG_FAST_BUDGET_MS 232 if mode[0] == 120 as u8 { budget=TG_BUDGET_MS } 233 let start: i64=sys_now_realtime_sec() 234 let rc: i64=tr_run_capture_cwd(self,av,buf,TG_STREAM_CAP,len,budget,cwd) 235 var ok: i64=0; if rc == expected { ok=1 } 236 var fails: i64=tg_check(ok,"cwd capture preserves actual cause",rc,expected) 237 if mode[0] == 120 as u8 { 238 let elapsed: i64=sys_now_realtime_sec()-start 239 ok=0; if elapsed < TG_MAX_ELAPSED_S { ok=1 } 240 fails=fails+tg_check(ok,"cwd closed-output deadline bounded",elapsed,TG_MAX_ELAPSED_S) 241 } 242 return fails 243} 244 245 246// Bounded contention fixtures requested by the operator; these are test workloads, not admission limits. 247const TG_PARALLEL_SMALL: i64=8 248const TG_PARALLEL_LARGE: i64=15 249const TG_PARALLEL_ROW: i64=8 250func tg_parallel_producer(identity: i64) -> i64 { 251 sys_sleep_ms(TG_FAST_MS) 252 let buf: *u8=sys_mmap(TG_STREAM_CAP) 253 var i: i64=0 254 while i < TG_STREAM_CAP { buf[i]=identity as u8; i=i+1 } 255 var blocks: i64=1 256 if (identity-65)%2 == 0 { blocks=TG_STREAM_BLOCKS } 257 var off: i64=0 258 let total: i64=blocks*TG_STREAM_CAP 259 while off < total { 260 var amount: i64=total-off 261 if amount > TG_STREAM_CAP { amount=TG_STREAM_CAP } 262 let rc: i64=sys_write(1,buf,amount) 263 if rc <= 0 { return 24 } 264 off=off+rc 265 } 266 return identity 267} 268func tg_parallel_worker(self: *u8, index: i64, row: *i64) -> i64 { 269 let av: *i64=sys_mmap(32) as *i64 270 let arg: *u8=sys_mmap(8) 271 arg[0]=(65+index) as u8; arg[1]=0 as u8 272 av[0]=self as i64; av[1]="q" as *u8 as i64; av[2]=arg as i64; av[3]=0 273 let buf: *u8=sys_mmap(TG_STREAM_CAP+1) 274 buf[TG_STREAM_CAP]=90 as u8 275 let ts: *i64=sys_mmap(16) as *i64 276 let selfpid: i64=__syscall(TG_SYS_GETPID,0,0,0,0,0,0) 277 let fdBefore: i64=osp_fd_count(selfpid) 278 row[0]=tr_clock_ms(ts) 279 row[2]=tr_run_capture_tr(self,av,buf,TG_STREAM_CAP,((row as i64)+24) as *i64,TG_FAST_BUDGET_MS,((row as i64)+32) as *i64) 280 row[1]=tr_clock_ms(ts) 281 let fdAfter: i64=osp_fd_count(selfpid) 282 row[6]=0-1 283 if fdBefore >= 0 && fdAfter >= 0 { row[6]=fdAfter-fdBefore } 284 let childStatus: *i64=sys_mmap(16) as *i64 285 row[7]=sys_wait4(0-1,childStatus,WNOHANG) 286 row[5]=1 287 var i: i64=0 288 if row[3] < 0 || row[3] > TG_STREAM_CAP { row[5]=0 } else { 289 while i < row[3] { if buf[i] != arg[0] { row[5]=0 }; i=i+1 } 290 } 291 if buf[TG_STREAM_CAP] != 90 as u8 { row[5]=0 } 292 return 0 293} 294func tg_parallel_capture(self: *u8, count: i64) -> i64 { 295 let shared: *i64=sys_mmap_shared(TR_CONTROL_BYTES) as *i64 296 if (shared as i64) <= 0 { return 1 } 297 let fds: *i64=sys_mmap(16) as *i64 298 if sys_pipe2(fds,0) != 0 { sys_munmap(shared as *u8,TR_CONTROL_BYTES); return 1 } 299 let readfd: i64=fds[0] & 0xFFFFFFFF 300 let writefd: i64=(fds[0] >> 32) & 0xFFFFFFFF 301 let pids: *i64=sys_mmap(TR_CONTROL_BYTES) as *i64 302 var made: i64=0 303 while made < count { 304 let pid: i64=sys_fork() 305 if pid < 0 { break } 306 if pid == 0 { 307 sys_close(writefd) 308 let start: *u8=sys_mmap(8) 309 let ready: i64=sys_read(readfd,start,1) 310 sys_close(readfd) 311 if ready != 1 { sys_exit(30); return 30 } 312 tg_parallel_worker(self,made,((shared as i64)+made*TG_PARALLEL_ROW*8) as *i64) 313 sys_exit(0); return 0 314 } 315 pids[made]=pid; made=made+1 316 } 317 sys_close(readfd) 318 let starts: *u8=sys_mmap(TR_CONTROL_BYTES) 319 var sent: i64=0 320 while sent < made { 321 let wrote: i64=sys_write(writefd,starts+sent,made-sent) 322 if wrote <= 0 { break } 323 sent=sent+wrote 324 } 325 sys_close(writefd) 326 var ok: i64=0; if made == count && sent == made { ok=1 } 327 var fails: i64=tg_check(ok,"parallel workers released",made,count) 328 var latestStart: i64=0 329 var earliestEnd: i64=0 330 var i: i64=0 331 let status: *i64=sys_mmap(16) as *i64 332 while i < made { 333 let reaped: i64=tr_reap(pids[i],status) 334 ok=0; if reaped == pids[i] && wait_status_rc(status[0]) == 0 { ok=1 } 335 fails=fails+tg_check(ok,"parallel caller reaped",reaped,pids[i]) 336 let row: *i64=((shared as i64)+i*TG_PARALLEL_ROW*8) as *i64 337 tg_puts(" WORKER index="); tg_num(i); tg_puts(" start_ms="); tg_num(row[0]); tg_puts(" end_ms="); tg_num(row[1]); tg_puts("\n") 338 ok=0; if row[2] == 65+i { ok=1 } 339 fails=fails+tg_check(ok,"parallel exit identity",row[2],65+i) 340 ok=0; if row[3] == TG_STREAM_CAP { ok=1 } 341 fails=fails+tg_check(ok,"parallel capture length",row[3],TG_STREAM_CAP) 342 var expectedCut: i64=0; if i%2 == 0 { expectedCut=1 } 343 ok=0; if row[4] == expectedCut { ok=1 } 344 fails=fails+tg_check(ok,"parallel truncation identity",row[4],expectedCut) 345 fails=fails+tg_check(row[5],"parallel byte identity and canary",row[5],1) 346 ok=0; if row[6] == 0 { ok=1 } 347 fails=fails+tg_check(ok,"parallel caller descriptor delta",row[6],0) 348 ok=0; if row[7] == TR_ECHILD { ok=1 } 349 fails=fails+tg_check(ok,"parallel caller has no remaining children",row[7],TR_ECHILD) 350 if row[0] > latestStart { latestStart=row[0] } 351 if i == 0 || row[1] < earliestEnd { earliestEnd=row[1] } 352 i=i+1 353 } 354 ok=0; if latestStart > 0 && earliestEnd > latestStart { ok=1 } 355 fails=fails+tg_check(ok,"all capture intervals overlap ms",earliestEnd-latestStart,1) 356 sys_munmap(shared as *u8,TR_CONTROL_BYTES) 357 return fails 358} 359 360 361func tg_resource_finish(pid: i64, baseline: i64) -> i64 { 362 var ok: i64=0; if baseline >= 0 { ok=1 } 363 var fails: i64=tg_check(ok,"descriptor baseline available",baseline,baseline) 364 let before: i64=osp_fd_count(pid) 365 ok=0; if before == baseline { ok=1 } 366 fails=fails+tg_check(ok,"capture suite restores descriptor count",before,baseline) 367 let fds: *i64=sys_mmap(16) as *i64 368 let opened: i64=sys_pipe2(fds,0) 369 let during: i64=osp_fd_count(pid) 370 ok=0; if opened == 0 && during == before+2 { ok=1 } 371 fails=fails+tg_check(ok,"descriptor meter detects real pipe",during,before+2) 372 if opened == 0 { sys_close(fds[0] & 0xFFFFFFFF); sys_close((fds[0] >> 32) & 0xFFFFFFFF) } 373 let after: i64=osp_fd_count(pid) 374 ok=0; if after == before { ok=1 } 375 fails=fails+tg_check(ok,"closed pipe releases both descriptors",after,before) 376 let nonDirectory: i64=sys_openat_rd("/dev/null") 377 let meterCause: *i64=sys_mmap(16) as *i64 378 let invalidCount: i64=osp_fd_count_from_fd_result(nonDirectory,meterCause) 379 if nonDirectory >= 0 { sys_close(nonDirectory) } 380 ok=0; if nonDirectory >= 0 && invalidCount == OSP_SENTINEL { ok=1 } 381 fails=fails+tg_check(ok,"directory read error is unavailable",invalidCount,OSP_SENTINEL) 382 ok=0; if meterCause[0] == 0-20 { ok=1 } 383 fails=fails+tg_check(ok,"meter retains ENOTDIR cause",meterCause[0],0-20) 384 let badFdCount: i64=osp_fd_count_from_fd(0-1) 385 ok=0; if badFdCount == OSP_SENTINEL { ok=1 } 386 fails=fails+tg_check(ok,"invalid descriptor is unavailable",badFdCount,OSP_SENTINEL) 387 let status: *i64=sys_mmap(16) as *i64 388 let remaining: i64=sys_wait4(0-1,status,WNOHANG) 389 ok=0; if remaining == TR_ECHILD { ok=1 } 390 fails=fails+tg_check(ok,"no remaining direct children or zombies",remaining,TR_ECHILD) 391 return fails 392} 393 394// Descriptor 2048 deliberately lies above both existing cleanup boundaries. 395func tg_high_descriptor_subject(self: *u8) -> i64 { 396 let opened: i64=sys_openat_rd("/dev/null") 397 if opened < 0 { return tg_check(0,"high descriptor fixture opens source",opened,0) } 398 let copied: i64=sys_dup3(opened,2048,0) 399 sys_close(opened) 400 if copied != 2048 { 401 let limits: *i64=sys_mmap(16) as *i64 402 let measured: i64=nx_prlimit(0,TG_RLIMIT_NOFILE,0 as *u8,limits as *u8) 403 tg_puts(" FD-FIXTURE requested=2048 dup_result=");tg_num(copied) 404 tg_puts(" rlimit_result=");tg_num(measured);tg_puts(" soft=");tg_num(limits[0]);tg_puts(" hard=");tg_num(limits[1]);tg_puts("\n") 405 sys_munmap(limits as *u8,16) 406 return tg_check(0,"high descriptor fixture allocation",copied,TG_HIGH_FD_WITNESS) 407 } 408 let av: *i64=sys_mmap(32) as *i64 409 av[0]=self as i64; av[1]="f" as *u8 as i64; av[2]=0 410 let out: *u8=sys_mmap(4096) 411 let len: *i64=sys_mmap(16) as *i64 412 var fails: i64=0 413 var rc: i64=tr_run_capture_to(self,av,out,4096,len,TG_FAST_BUDGET_MS) 414 var ok: i64=0; if rc == 0 { ok=1 } 415 fails=fails+tg_check(ok,"bounded child cannot inherit descriptor 2048",rc,0) 416 rc=tr_run_capture(self,av,out,4096,len) 417 ok=0; if rc == 0 { ok=1 } 418 fails=fails+tg_check(ok,"legacy child cannot inherit descriptor 2048",rc,0) 419 let fallback: i64=sys_fork() 420 if fallback == 0 { 421 let cleaned: i64=sys_close_inherited_proc(3) 422 if cleaned != 0 { sys_exit(32); return 32 } 423 if sys_read(2048,out,1) != (0-9) { sys_exit(33); return 33 } 424 if sys_write(1,"",0) != 0 { sys_exit(34); return 34 } 425 sys_exit(0); return 0 426 } 427 let status: *i64=sys_mmap(16) as *i64 428 var fallbackrc: i64=0-1 429 if fallback > 0 { if tr_reap(fallback,status) == fallback { fallbackrc=wait_status_rc(status[0]) } } 430 ok=0; if fallbackrc == 0 { ok=1 } 431 fails=fails+tg_check(ok,"proc fallback closes high fd and preserves stdout",fallbackrc,0) 432 rc=sys_read(2048,out,1) 433 ok=0; if rc == 0 { ok=1 } 434 fails=fails+tg_check(ok,"parent descriptor remains owned and open",rc,0) 435 sys_close(2048) 436 sys_munmap(out,4096) 437 return fails 438} 439 440func tg_high_descriptor(self: *u8) -> i64 { 441 let saved: *i64=sys_mmap(16) as *i64 442 let requested: *i64=sys_mmap(16) as *i64 443 let queried: i64=nx_prlimit(0,TG_RLIMIT_NOFILE,0 as *u8,saved as *u8) 444 var fails: i64=0 445 if queried < 0 { 446 fails=tg_check(0,"descriptor fixture reads kernel limits",queried,0) 447 } else { 448 requested[0]=saved[0];requested[1]=saved[1] 449 if requested[0] <= TG_HIGH_FD_WITNESS { requested[0]=TG_HIGH_FD_WITNESS+1 } 450 if requested[0] > requested[1] { 451 fails=tg_check(0,"descriptor witness fits kernel hard limit",requested[1],requested[0]) 452 } else { 453 let raised: i64=nx_prlimit(0,TG_RLIMIT_NOFILE,requested as *u8,0 as *u8) 454 if raised < 0 { fails=tg_check(0,"descriptor fixture applies local soft limit",raised,0) } else { 455 fails=tg_high_descriptor_subject(self) 456 let restored: i64=nx_prlimit(0,TG_RLIMIT_NOFILE,saved as *u8,0 as *u8) 457 var ok: i64=0;if restored == 0 { ok=1 } 458 fails=fails+tg_check(ok,"descriptor fixture restores original limits",restored,0) 459 } 460 } 461 } 462 sys_munmap(saved as *u8,16);sys_munmap(requested as *u8,16) 463 return fails 464} 465 466// The fixture becomes a subreaper only inside an isolated child, so the gate 467// does not adopt unrelated workload descendants. 468const TG_LIFETIME_WAIT_MS: i64 = 10 469const TG_LIFETIME_ATTEMPTS: i64 = 50 470func tg_lifetime_fixture(late: i64) -> i64 { 471 if sys_prctl(NX_PR_SET_CHILD_SUBREAPER,1) != 0 { return 40 } 472 let state: *i64=sys_mmap_shared(4096) as *i64 473 let barrier: *i64=sys_mmap(16) as *i64 474 if sys_pipe2(barrier,0) != 0 { return 41 } 475 let rfd: i64=barrier[0] & 0xFFFFFFFF 476 let wfd: i64=(barrier[0] >> 32) & 0xFFFFFFFF 477 let owner: i64=sys_fork() 478 if owner < 0 { sys_close(rfd); sys_close(wfd); return 42 } 479 if owner == 0 { 480 let identity: i64=__syscall(TG_SYS_GETPID,0,0,0,0,0,0) 481 let worker: i64=sys_fork() 482 if worker < 0 { state[1]=0-1; sys_exit(43); return 43 } 483 if worker == 0 { 484 sys_close(wfd) 485 let byte: *u8=sys_mmap(16) 486 if sys_read(rfd,byte,1) != 1 { sys_exit(44); return 44 } 487 sys_close(rfd) 488 var rc: i64=0 489 if late != 2 { rc=sys_bind_parent_lifetime(identity,TR_SIGKILL) } 490 state[1]=rc 491 state[2]=1 492 if rc != 0 { sys_exit(45); return 45 } 493 sys_sleep_ms(TG_HANG_MS) 494 sys_exit(46); return 46 495 } 496 state[0]=worker 497 sys_close(rfd); sys_close(wfd) 498 if late == 1 { sys_exit(0); return 0 } 499 sys_sleep_ms(TG_HANG_MS) 500 sys_exit(47); return 47 501 } 502 sys_close(rfd) 503 let status: *i64=sys_mmap(16) as *i64 504 var result: i64=0 505 if late == 1 { 506 if tr_reap(owner,status) != owner { result=48 } 507 } 508 if sys_write(wfd,"R",1) != 1 { result=49 } 509 sys_close(wfd) 510 var spins: i64=0 511 while state[2] == 0 && spins < TG_LIFETIME_ATTEMPTS { 512 sys_sleep_ms(TG_LIFETIME_WAIT_MS); spins=spins+1 513 } 514 if state[2] == 0 { result=50 } 515 if late != 1 { 516 if state[1] != 0 { result=51 } 517 nx_kill(owner,TR_SIGKILL) 518 if tr_reap(owner,status) != owner { result=52 } 519 } else { 520 if state[1] != TR_ECHILD { result=53 } 521 } 522 let worker: i64=state[0] 523 if worker <= 1 { return 54 } 524 var reaped: i64=0 525 spins=0 526 while reaped == 0 && spins < TG_LIFETIME_ATTEMPTS { 527 reaped=sys_wait4(worker,status,1) 528 if reaped == 0 { sys_sleep_ms(TG_LIFETIME_WAIT_MS) } 529 spins=spins+1 530 } 531 if reaped == 0 { nx_kill(worker,TR_SIGKILL); tr_reap(worker,status); result=55 } 532 if reaped < 0 { result=56 } 533 if result == 0 { 534 if late == 0 { if wait_status_rc(status[0]) != 137 { result=57 } } 535 if late == 1 { if wait_status_rc(status[0]) != 45 { result=58 } } 536 } 537 if sys_wait4(0-1,status,1) != TR_ECHILD { result=59 } 538 sys_munmap(state as *u8,4096) 539 return result 540} 541func tg_lifetime_check(late: i64) -> i64 { 542 let child: i64=sys_fork() 543 if child == 0 { let rc: i64=tg_lifetime_fixture(late); sys_exit(rc); return rc } 544 if child < 0 { return 1 } 545 let status: *i64=sys_mmap(16) as *i64 546 if tr_reap(child,status) != child { return 1 } 547 let rc: i64=wait_status_rc(status[0]) 548 var ok: i64=0; if rc == 0 { ok=1 } 549 if late == 2 { ok=0; if rc == 55 { ok=1 }; return tg_check(ok,"unbound control survives parent, fixture kills and reaps it",rc,55) } 550 if late == 1 { return tg_check(ok,"parent already dead before binding is rejected",rc,0) } 551 return tg_check(ok,"bound child dies with parent and is reaped",rc,0) 552} 553 554// Read only this fixture owner's direct children; retain identities before killing it. 555func tg_owner_children(owner: i64, pids: *i64) -> i64 { 556 return osp_children(owner,pids,2) 557} 558func tg_capture_owner_crash(self: *u8, incomplete: i64) -> i64 { 559 if sys_prctl(NX_PR_SET_CHILD_SUBREAPER,1) != 0 { return 60 } 560 let owner: i64=sys_fork() 561 if owner < 0 { return 61 } 562 if owner == 0 { 563 let av: *i64=sys_mmap(32) as *i64 564 av[0]=self as i64; av[1]="x" as *u8 as i64; av[2]=0 565 let out: *u8=sys_mmap(4096) 566 let length: *i64=sys_mmap(16) as *i64 567 let rc: i64=tr_run_capture_to(self,av,out,4096,length,TG_FAST_BUDGET_MS) 568 sys_exit(62); return rc 569 } 570 let pids: *i64=sys_mmap(32) as *i64 571 let status: *i64=sys_mmap(16) as *i64 572 var count: i64=0 573 var attempt: i64=0 574 while count != 2 && attempt < TG_LIFETIME_ATTEMPTS { 575 count=tg_owner_children(owner,pids) 576 if count < 0 { break } 577 if count != 2 { sys_sleep_ms(TG_LIFETIME_WAIT_MS) } 578 attempt=attempt+1 579 } 580 nx_kill(owner,TR_SIGKILL) 581 if tr_reap(owner,status) != owner { return 63 } 582 // Deliberately discard an observation in the negative fixture. Cleanup must 583 // discover adopted children rather than trust the incomplete observation. 584 if incomplete == 1 { count=0 } 585 if count != 2 { 586 let fixture: i64=__syscall(TG_SYS_GETPID,0,0,0,0,0,0) 587 let adopted: i64=tg_owner_children(fixture,pids) 588 if adopted < 0 { return 69 } 589 var child: i64=0 590 while child < adopted { 591 // Unreaped direct children retain these identities until this wait. 592 nx_kill(pids[child],TR_SIGKILL) 593 if tr_reap(pids[child],status) != pids[child] { return 69 } 594 child=child+1 595 } 596 if sys_wait4(0-1,status,WNOHANG) != TR_ECHILD { return 69 } 597 return 64 598 } 599 var i: i64=0 600 var result: i64=0 601 while i < count { 602 var reaped: i64=0 603 attempt=0 604 while reaped == 0 && attempt < TG_LIFETIME_ATTEMPTS { 605 reaped=sys_wait4(pids[i],status,1) 606 if reaped == 0 { sys_sleep_ms(TG_LIFETIME_WAIT_MS) } 607 attempt=attempt+1 608 } 609 if reaped == 0 { nx_kill(pids[i],TR_SIGKILL); tr_reap(pids[i],status); result=65 } 610 if reaped < 0 { result=66 } 611 if reaped > 0 { 612 let rc: i64=wait_status_rc(status[0]) 613 if rc != 137 && rc != 126 { result=67 } 614 } 615 i=i+1 616 } 617 if sys_wait4(0-1,status,1) != TR_ECHILD { result=68 } 618 return result 619} 620func tg_capture_crash_check(self: *u8, incomplete: i64) -> i64 { 621 let child: i64=sys_fork() 622 if child == 0 { let rc: i64=tg_capture_owner_crash(self,incomplete); sys_exit(rc); return rc } 623 if child < 0 { return 1 } 624 let status: *i64=sys_mmap(16) as *i64 625 if tr_reap(child,status) != child { return 1 } 626 let rc: i64=wait_status_rc(status[0]) 627 var ok: i64=0; if rc == 0 { ok=1 } 628 if incomplete == 1 { 629 ok=0; if rc == 64 { ok=1 } 630 return tg_check(ok,"incomplete observation fails only after adopted children are reaped",rc,64) 631 } 632 return tg_check(ok,"actual capture owner crash closes worker and watchdog",rc,0) 633} 634 635// Focused host qualification: no build, network, persistent writes, or parallel cohort. 636func tg_lifecycle_gate(self: *u8) -> i64 { 637 let pid: i64=__syscall(TG_SYS_GETPID,0,0,0,0,0,0) 638 let before: i64=osp_fd_count(pid) 639 var fails: i64=tg_capture_crash_check(self,0) 640 fails=fails+tg_capture_crash_check(self,1) 641 fails=fails+tg_lifetime_check(0) 642 fails=fails+tg_lifetime_check(1) 643 fails=fails+tg_lifetime_check(2) 644 fails=fails+tg_high_descriptor(self) 645 fails=fails+tg_resource_finish(pid,before) 646 tg_puts("NX-LIFECYCLE checks=");tg_num(TG_COUNTER[1]);tg_puts(" fails="); tg_num(fails); tg_puts("\n") 647 let result: i64=gv_verdict("LIFECYCLE",TG_COUNTER,"owned process and descriptor regression suite");sys_exit(result);return result 648} 649func main(argc: i64, argv: *i64) -> i64 { 650 TG_COUNTER=gv_ctr() 651 if argc > 1 { if (argv[1] as *u8)[0] == 108 as u8 { return tg_lifecycle_gate(argv[0] as *u8) } } 652 if argc > 2 { if (argv[1] as *u8)[0] == 113 as u8 { return tg_parallel_producer((argv[2] as *u8)[0] as i64) } } 653 if argc > 1 { return tg_stream_child((argv[1] as *u8)[0] as i64) } 654 tg_puts("=== nx_tool_run_timeout_gate -- bounded exec vs a REAL hanging child ===\n" as *u8) 655 var fails: i64 = 0 656 let selfpid: i64=__syscall(TG_SYS_GETPID,0,0,0,0,0,0) 657 let fdBaseline: i64=osp_fd_count(selfpid) 658 let out: *u8 = sys_mmap(TG_OUTCAP) 659 let ol: *i64 = sys_mmap(16) as *i64 660 661 // ---- T1 + T2: the deadline fires, and it fires in real time ---- 662 let t0: i64 = sys_now_realtime_sec() 663 let r1: i64 = tg_run("10000" as *u8, TG_BUDGET_MS, out, ol) 664 let t1: i64 = sys_now_realtime_sec() 665 let elapsed: i64 = t1 - t0 666 667 var ok: i64 = 0; if r1 == TR_ERR_TIMEOUT { ok = 1 } 668 fails = fails + tg_check(ok, "T1 deadline fires (TR_ERR_TIMEOUT)" as *u8, r1, TR_ERR_TIMEOUT) 669 670 ok = 0; if elapsed < TG_MAX_ELAPSED_S { ok = 1 } 671 fails = fails + tg_check(ok, "T2 returned in real time, not after the full 10s sleep" as *u8, elapsed, TG_MAX_ELAPSED_S) 672 673 // ---- T3 + T4: NEG-CONTROL, a fast child must NOT be timed out and its output must survive ---- 674 ol[0] = 0 675 let r3: i64 = tg_run("50" as *u8, TG_FAST_BUDGET_MS, out, ol) 676 ok = 0; if r3 == 0 { ok = 1 } 677 fails = fails + tg_check(ok, "T3 neg-ctrl fast child exits 0 (no false timeout)" as *u8, r3, 0) 678 679 ok = tr_contains(out, ol[0], "SLEEPER-DONE" as *u8) 680 fails = fails + tg_check(ok, "T4 fast child's stdout captured" as *u8, ol[0], ol[0]) 681 682 // ---- T5: repeatable -- a second kill must behave identically (no leaked fd/pid/zombie) ---- 683 let r5: i64 = tg_run("10000" as *u8, TG_BUDGET_MS, out, ol) 684 ok = 0; if r5 == TR_ERR_TIMEOUT { ok = 1 } 685 fails = fails + tg_check(ok, "T5 second timeout retains correct behavior" as *u8, r5, TR_ERR_TIMEOUT) 686 687 // ---- T6: opt-out is explicit, never silent ---- 688 ol[0] = 0 689 let r6: i64 = tg_run("50" as *u8, 0, out, ol) 690 ok = 0; if r6 == 0 { ok = 1 } 691 fails = fails + tg_check(ok, "T6 timeout_ms<=0 delegates to unbounded path" as *u8, r6, 0) 692 693 fails=fails+tg_stream_check(argv[0] as *u8,"b" as *u8,0,1,TG_STREAM_CAP) 694 fails=fails+tg_stream_check(argv[0] as *u8,"b" as *u8,1,1,TG_STREAM_CAP) 695 fails=fails+tg_stream_check(argv[0] as *u8,"e" as *u8,1,0,TG_STREAM_CAP) 696 fails=fails+tg_stream_check(argv[0] as *u8,"s" as *u8,1,0,TG_STREAM_CAP-1) 697 fails=fails+tg_stream_timeout(argv[0] as *u8) 698 fails=fails+tg_invalid_capacity() 699 fails=fails+tg_descendant_deadline(argv[0] as *u8) 700 fails=fails+tg_lifecycle_exit(argv[0] as *u8,"k" as *u8,0,137) 701 fails=fails+tg_lifecycle_exit(argv[0] as *u8,"k" as *u8,TG_FAST_BUDGET_MS,137) 702 fails=fails+tg_lifecycle_exit(argv[0] as *u8,"x" as *u8,TG_BUDGET_MS,TR_ERR_TIMEOUT) 703 fails=fails+tg_wait_observe() 704 fails=fails+tg_cwd_check(argv[0] as *u8,"b",".",TG_STREAM_EXIT) 705 fails=fails+tg_cwd_check(argv[0] as *u8,"r",".",126) 706 fails=fails+tg_cwd_check(argv[0] as *u8,"k",".",137) 707 fails=fails+tg_cwd_check(argv[0] as *u8,"r","/dev/null",TR_ERR_CHDIR) 708 fails=fails+tg_cwd_check("nx_tool_run_timeout_gate.elf","r","_build",126) 709 fails=fails+tg_cwd_check(argv[0] as *u8,"x",".",TR_ERR_TIMEOUT) 710 fails=fails+tg_parallel_capture(argv[0] as *u8,TG_PARALLEL_SMALL) 711 fails=fails+tg_parallel_capture(argv[0] as *u8,TG_PARALLEL_LARGE) 712 fails=fails+tg_capture_crash_check(argv[0] as *u8,0) 713 fails=fails+tg_capture_crash_check(argv[0] as *u8,1) 714 fails=fails+tg_lifetime_check(0) 715 fails=fails+tg_lifetime_check(1) 716 fails=fails+tg_lifetime_check(2) 717 fails=fails+tg_high_descriptor(argv[0] as *u8) 718 fails=fails+tg_resource_finish(selfpid,fdBaseline) 719 tg_puts("NX-TOOL-RUN-TIMEOUT-GATE checks=");tg_num(TG_COUNTER[1]);tg_puts(" fails="); tg_num(fails) 720 tg_puts("\n") 721 let result: i64=gv_verdict("TOOL-RUN-TIMEOUT-GATE",TG_COUNTER,"executed assertions counted; process, capture and cleanup evidence") 722 sys_exit(result);return result 723}