code wiki / (root) / nx_guarded_run.nx

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}