nx_guarded_run.nx source
↩ module page · 122 lines · 5786 B
1// nx_guarded_run.nx -- the team's HANG-HANDLING capability (operator: "make
2// sure the team is getting built to handle issues like this hang", 2026-06-10).
3//
4// module: nishi-core.exec.guarded_run
5// capability: CORE_COMPUTE + GATE
6//
7// WHY: nx_sov_build_run's sbr_run forks + wait4(blocking) with NO deadline, so a
8// child that hangs in a syscall (e.g. the validated-HTTPS read loop wedging on a
9// host like en.wikipedia.org that never cleanly EOFs) wedges the runner -- and
10// therefore the GATE -- forever. This session proved two things the hard way:
11// (1) such hangs are REAL (example.com fetches 868B fine; wikipedia hangs 220s+);
12// (2) plain `timeout` (SIGTERM) does NOT reap them -- only SIGKILL does.
13// nx_guarded_run makes ANY run hang-proof: fork the child, poll wait4(WNOHANG)
14// against a wall-clock DEADLINE, and on breach SIGKILL it and report a distinct
15// TIMEOUT verdict the caller files as a defect instead of hanging. Sovereign:
16// pure sys_fork/execve/wait4/nx_kill -- no GNU `timeout`, no shell.
17//
18// This is the substrate the Doctor/Engineer wrap around every crawl/fetch/build
19// gate so a network or miscompile hang becomes a deterministic, filed TIMEOUT --
20// never a wedged session. Pairs with nx_progress_watchdog (in-process stall
21// detection); this is the out-of-process KILL the watchdog lacks.
22import "nx_syscalls.nx"
23const NX_MAGIC_5000: i64 = 5000
24const NX_MAGIC_1500: i64 = 1500
25
26const NX_GR_TIMEOUT: i64 = 124 // GNU-timeout convention: child exceeded deadline (then SIGKILLed)
27const NX_GR_SPAWN_FAIL: i64 = 125 // wait4 returned an error
28const NX_GR_POLL_MS: i64 = 50 // wall-clock poll granularity
29const NX_GR_SIGKILL: i64 = 9 // SIGTERM is NOT enough for these hangs (2026-06-10 lesson)
30
31func gr_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
32func gr_putn(v: i64) -> i64 {
33 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
34 var m: i64 = v; if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
35 let t: *u8 = sys_mmap(28); var k: i64 = 0
36 while m > 0 { t[k] = 0x30 + (m - (m/10)*10); m = m/10; k = k + 1 }
37 while k > 0 { k = k - 1; sys_write(1, (((t as i64)+k) as *u8), 1) }
38 return 0
39}
40
41// Run `path` with argv/envp under a hard wall-clock deadline (ms). Optional
42// stdout/stderr redirect fds (>=0). Returns:
43// 0..127 child WEXITSTATUS
44// 128+sig child died to a signal
45// 124 DEADLINE breached -> child SIGKILLed and reaped (NX_GR_TIMEOUT)
46// 125 wait4 error (NX_GR_SPAWN_FAIL)
47func nx_guarded_run(path: *u8, argv: *i64, envp: *i64, deadline_ms: i64,
48 redir_out: i64, redir_err: i64) -> i64 {
49 let pid: i64 = sys_fork()
50 if pid == 0 {
51 if redir_out >= 0 { sys_dup3(redir_out, 1, 0) }
52 if redir_err >= 0 { sys_dup3(redir_err, 2, 0) }
53 sys_execve(path, argv, envp)
54 sys_exit(127)
55 }
56 let st: *i64 = sys_mmap(16) as *i64
57 let start: i64 = sys_now_ms()
58 var rc: i64 = 0 - 1
59 while rc < 0 {
60 let w: i64 = sys_wait4(pid, st, 1) // WNOHANG: 0=still running, pid=reaped, <0=error
61 if w == pid {
62 let sig: i64 = st[0] & 0x7f
63 if sig != 0 { rc = 128 + sig } else { rc = (st[0] >> 8) & 0xff }
64 }
65 if w < 0 { rc = NX_GR_SPAWN_FAIL }
66 if rc < 0 {
67 let elapsed: i64 = sys_now_ms() - start
68 if elapsed >= deadline_ms {
69 nx_kill(pid, NX_GR_SIGKILL) // hard kill -- the only thing that reaps these
70 sys_wait4(pid, st, 0) // reap the corpse (now dead, blocking ok)
71 rc = NX_GR_TIMEOUT
72 } else {
73 sys_sleep_ms(NX_GR_POLL_MS)
74 }
75 }
76 }
77 return rc
78}
79
80// ---- self-test gate: prove the guard PASSES a fast child and KILLS a hang ----
81func gr_argv2(a0: *u8, a1: *u8) -> *i64 {
82 let v: *i64 = sys_mmap(8 * 4) as *i64
83 v[0] = a0 as i64; v[1] = a1 as i64; v[2] = 0
84 return v
85}
86
87func main() -> i64 {
88 let envp: *i64 = sys_mmap(8 * 4) as *i64
89 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64; envp[1] = 0
90 let devnull: i64 = sys_openat_wr("/dev/null" as *u8, 0x1a4)
91
92 gr_puts("=== nx_guarded_run self-test (sovereign hang-handler) ===\n")
93
94 // CASE 1: a fast child finishes well within the deadline -> its real exit.
95 let fast: *i64 = gr_argv2("/bin/true" as *u8, 0 as *u8)
96 let t1a: i64 = sys_now_ms()
97 let rc1: i64 = nx_guarded_run("/bin/true" as *u8, fast, envp, NX_MAGIC_5000, devnull, devnull)
98 let d1: i64 = sys_now_ms() - t1a
99 gr_puts("CASE1 fast /bin/true: rc="); gr_putn(rc1); gr_puts(" ms="); gr_putn(d1)
100 var p1: i64 = 0
101 if rc1 == 0 { p1 = 1 }
102 if p1 == 1 { gr_puts(" PASS\n") } else { gr_puts(" FAIL (expected 0)\n") }
103
104 // CASE 2: a child that HANGS (sleep 60) under a 1500ms deadline -> the guard
105 // must return TIMEOUT and do so FAST (proving it actually killed the hang,
106 // not waited 60s). This is exactly the wikipedia-read-hang shape.
107 let hang: *i64 = gr_argv2("/bin/sleep" as *u8, "60" as *u8)
108 let t2a: i64 = sys_now_ms()
109 let rc2: i64 = nx_guarded_run("/bin/sleep" as *u8, hang, envp, NX_MAGIC_1500, devnull, devnull)
110 let d2: i64 = sys_now_ms() - t2a
111 gr_puts("CASE2 hang /bin/sleep 60 @1500ms: rc="); gr_putn(rc2); gr_puts(" ms="); gr_putn(d2)
112 var p2: i64 = 0
113 if rc2 == NX_GR_TIMEOUT { if d2 < NX_MAGIC_5000 { p2 = 1 } } // killed, and killed promptly
114 if p2 == 1 { gr_puts(" PASS\n") } else { gr_puts(" FAIL (expected 124 within 5s)\n") }
115
116 if p1 == 1 { if p2 == 1 {
117 gr_puts("GATE: nx_guarded_run PASS -- the team can bound any run and reap any hang\n")
118 return 0
119 } }
120 gr_puts("GATE: nx_guarded_run FAIL\n")
121 return 1
122}