code wiki / _hdl_build / nx_tool_run_timeout_gate.nx
nx_tool_run_timeout_gate.nx source
↩ module page · 110 lines · 5155 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 (no fd/pid/zombie leak
14// wedging the caller after the first kill)
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"
20
21const TG_STDOUT: i64 = 1
22const TG_SLEEPER: *u8 = "_build/nx_tr_sleeper.sov.elf"
23const TG_OUTCAP: i64 = 65536
24
25// A 10s sleeper against a 500ms budget. If the kill did not happen the call cannot return before 10s.
26const TG_HANG_MS: i64 = 10000
27const TG_BUDGET_MS: i64 = 500
28// Generous ceiling: anything under this proves we did not sit out the full sleep. Deliberately far from
29// both 0.5s and 10s so neither scheduler jitter nor a slow NAS can flip the verdict either way.
30const TG_MAX_ELAPSED_S: i64 = 5
31const TG_FAST_MS: i64 = 50
32const TG_FAST_BUDGET_MS: i64 = 5000
33
34func 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 }
35func tg_num(v: i64) -> i64 {
36 var m: i64 = v
37 if m < 0 { m = 0 - m; sys_write(TG_STDOUT, "-" as *u8, 1) }
38 let t: *u8 = sys_mmap(28)
39 var k: i64 = 0
40 if m == 0 { t[0] = 48 as u8; k = 1 }
41 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
42 let o: *u8 = sys_mmap(28)
43 var i: i64 = 0
44 while i < k { o[i] = t[k - 1 - i]; i = i + 1 }
45 sys_write(TG_STDOUT, o, k)
46 return 0
47}
48func tg_check(ok: i64, label: *u8, got: i64, want: i64) -> i64 {
49 if ok == 1 { tg_puts(" PASS " as *u8) } else { tg_puts(" FAIL " as *u8) }
50 tg_puts(label)
51 tg_puts(" got=" as *u8); tg_num(got)
52 tg_puts(" want=" as *u8); tg_num(want)
53 tg_puts("\n" as *u8)
54 if ok == 1 { return 0 }
55 return 1
56}
57
58// run the sleeper for ms with a budget; returns tr_run_capture_to's result
59func tg_run(ms: *u8, budget: i64, out: *u8, ol: *i64) -> i64 {
60 let av: *i64 = sys_mmap(32) as *i64
61 av[0] = TG_SLEEPER as i64
62 av[1] = ms as *u8 as i64
63 av[2] = 0
64 return tr_run_capture_to(TG_SLEEPER, av, out, TG_OUTCAP, ol, budget)
65}
66
67func main() -> i64 {
68 tg_puts("=== nx_tool_run_timeout_gate -- bounded exec vs a REAL hanging child ===\n" as *u8)
69 var fails: i64 = 0
70 let out: *u8 = sys_mmap(TG_OUTCAP)
71 let ol: *i64 = sys_mmap(16) as *i64
72
73 // ---- T1 + T2: the deadline fires, and it fires in real time ----
74 let t0: i64 = sys_now_realtime_sec()
75 let r1: i64 = tg_run("10000" as *u8, TG_BUDGET_MS, out, ol)
76 let t1: i64 = sys_now_realtime_sec()
77 let elapsed: i64 = t1 - t0
78
79 var ok: i64 = 0; if r1 == TR_ERR_TIMEOUT { ok = 1 }
80 fails = fails + tg_check(ok, "T1 deadline fires (TR_ERR_TIMEOUT)" as *u8, r1, TR_ERR_TIMEOUT)
81
82 ok = 0; if elapsed < TG_MAX_ELAPSED_S { ok = 1 }
83 fails = fails + tg_check(ok, "T2 returned in real time, not after the full 10s sleep" as *u8, elapsed, TG_MAX_ELAPSED_S)
84
85 // ---- T3 + T4: NEG-CONTROL, a fast child must NOT be timed out and its output must survive ----
86 ol[0] = 0
87 let r3: i64 = tg_run("50" as *u8, TG_FAST_BUDGET_MS, out, ol)
88 ok = 0; if r3 == 0 { ok = 1 }
89 fails = fails + tg_check(ok, "T3 neg-ctrl fast child exits 0 (no false timeout)" as *u8, r3, 0)
90
91 ok = tr_contains(out, ol[0], "SLEEPER-DONE" as *u8)
92 fails = fails + tg_check(ok, "T4 fast child's stdout captured" as *u8, ol[0], ol[0])
93
94 // ---- T5: repeatable -- a second kill must behave identically (no leaked fd/pid/zombie) ----
95 let r5: i64 = tg_run("10000" as *u8, TG_BUDGET_MS, out, ol)
96 ok = 0; if r5 == TR_ERR_TIMEOUT { ok = 1 }
97 fails = fails + tg_check(ok, "T5 second timeout identical (no leak after a kill)" as *u8, r5, TR_ERR_TIMEOUT)
98
99 // ---- T6: opt-out is explicit, never silent ----
100 ol[0] = 0
101 let r6: i64 = tg_run("50" as *u8, 0, out, ol)
102 ok = 0; if r6 == 0 { ok = 1 }
103 fails = fails + tg_check(ok, "T6 timeout_ms<=0 delegates to unbounded path" as *u8, r6, 0)
104
105 tg_puts("NX-TOOL-RUN-TIMEOUT-GATE checks=6 fails=" as *u8); tg_num(fails)
106 if fails == 0 { tg_puts(" verdict=GREEN\n" as *u8); sys_exit(0); return 0 }
107 tg_puts(" verdict=RED\n" as *u8)
108 sys_exit(1)
109 return 1
110}