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}