code wiki / (root) / nx_waitrc_gate.nx

nx_waitrc_gate.nx source

↩ module page · 209 lines · 10353 B

1// nx_waitrc_gate.nx -- A CRASHED OR KILLED PROCESS MUST NOT READ AS A CLEAN ONE. 2// 3// THE DEFECT, MEASURED 2026-08-25 ON THE LIVE SURFACE: a gate that died mid-run was served by 4// /api/gate_run as exit_code 0, verdict GREEN. One line causes it. wait_exit_code is WEXITSTATUS, 5// bits 8-15 of the wait status, and a child killed by a SIGNAL has no exit status at all -- those 6// bits are ZERO. So a signal death and a clean exit 0 are THE SAME VALUE to any caller reading only 7// that accessor, and nx_tool_run's tr_run_capture family -- the exec primitive behind /api/gate_run, 8// /api/build and 51 other consumers -- read only that accessor. 9// 10// WHY SIGKILL AND NOT SIGSEGV, WHICH IS WHAT THIS GATE FIRST TESTED AND WHY THAT RUN WAS RED. 11// The first fixture segfaulted a child and asserted a signal death. It got termsig=0, exit=139, and 12// the fixture-reached-condition tooth FAILED -- correctly. This estate ships a DEFAULT-ON CRASH 13// GUARD: it catches SIGSEGV/SIGBUS/SIGILL/SIGFPE, prints the faulting instruction and address, and 14// ends the process with EXIT 139 rather than re-raising. So for a guarded NishiLang binary a segfault 15// ALREADY surfaces as a non-zero exit code and wait_exit_code is already right about it. 16// SIGKILL cannot be caught, blocked or handled by any guard, and it is the signal that actually bit 17// this estate: nx_gatekit_lib records two gates the 60 s watchdog KILLED journaling GREEN exit=0 18// ms=60443. A WATCHDOG-KILLED SUBJECT THAT RETURNS THE EXIT CODE OF A CLEAN ONE TURNS EVERY TIMEOUT 19// INTO A PASS. That is the class under test, so the fixture must die the way the real ones died. 20// THE FIRST RED WAS THE FIXTURE FAILING TO REACH THE CONDITION, NOT THE RULE FAILING -- WHICH IS 21// EXACTLY WHAT THAT TOOTH EXISTS TO SEPARATE, AND WHY THE ANSWER WAS TO FIX THE FIXTURE AND NEVER 22// TO WIDEN THE ASSERTION. 23// 24// The DISAGREE tooth is the discriminator and the reason this gate cannot go quiet: it feeds ONE 25// status word to the old accessor and the new ruler and demands they DISAGREE. If wait_status_rc 26// ever silently became wait_exit_code again, that tooth is what notices. 27 28import "nx_syscalls.nx" 29import "nx_gate_verdict.nx" 30import "nx_tool_run.nx" 31import "nx_ccbuild_lib.nx" 32 33const WR_SIGKILL: i64 = 9 34const WR_GUARD_EXIT: i64 = 139 // 128+SIGSEGV, what the crash guard exits with 35const WR_CHILD_EXIT: i64 = 7 36const WR_CAP: i64 = 65536 37const WR_DEADLINE_MS: i64 = 20000 38// A BOUND ON AN UNKNOWABLE WAIT, NAMED, AND ITS EXHAUSTION ANNOUNCES. The child parks here only 39// until the parent's SIGKILL lands -- microseconds in practice, since the pid exists from fork(). 40// If the kill never lands the child exits WR_CHILD_UNKILLED instead of dying, and the 41// fixture-reached tooth reports that distinguishable value rather than a silent wrong answer. 42const WR_CHILD_PARK_MS: i64 = 5000 43const WR_CHILD_UNKILLED: i64 = 3 44 45func wr_puts(s: *u8) -> i64 { 46 var n: i64 = 0 47 while s[n] != (0 as u8) { n = n + 1 } 48 sys_write(1, s, n) 49 return 0 50} 51 52func wr_putn(v: i64) -> i64 { 53 if v == 0 { wr_puts("0\x00" as *u8); return 0 } 54 var x: i64 = v 55 if x < 0 { wr_puts("-\x00" as *u8); x = 0 - x } 56 let t: *u8 = sys_mmap(32) 57 var n: i64 = 0 58 while x > 0 { t[n] = (48 + (x % 10)) as u8; n = n + 1; x = x / 10 } 59 let o: *u8 = sys_mmap(32) 60 var i: i64 = n 61 var j: i64 = 0 62 while i > 0 { i = i - 1; o[j] = t[i]; j = j + 1 } 63 o[j] = 0 as u8 64 wr_puts(o) 65 return 0 66} 67 68// TRUE SIGNAL DEATH. The parent kills, because getpid is BROKEN on this backend -- __syscall(39) 69// returns -25 for every process and rv64 172 is not in the translation table, so a self-kill would 70// send signal 9 to pid -25. The parent already holds the real pid from fork(), which needs no 71// syscall table at all. Returns the RAW wait status so both rulers can read the same word. 72func wr_status_of_sigkill() -> i64 { 73 let pid: i64 = sys_fork() 74 if pid == 0 { 75 sys_sleep_ms(WR_CHILD_PARK_MS) 76 sys_exit(WR_CHILD_UNKILLED) 77 } 78 nx_kill(pid, WR_SIGKILL) 79 let st: *i64 = sys_mmap(16) as *i64 80 sys_wait4(pid, st, 0) 81 return st[0] 82} 83 84func wr_opaque_zero(argc: i64) -> i64 { 85 if argc > 1000000 { return 1 } 86 return 0 87} 88 89func wr_status_of_segv(argc: i64) -> i64 { 90 let pid: i64 = sys_fork() 91 if pid == 0 { 92 let z: i64 = wr_opaque_zero(argc) 93 let p: *i64 = z as *i64 94 p[0] = 1 95 sys_exit(0) 96 } 97 let st: *i64 = sys_mmap(16) as *i64 98 sys_wait4(pid, st, 0) 99 return st[0] 100} 101 102func wr_status_of_exit(code: i64) -> i64 { 103 let pid: i64 = sys_fork() 104 if pid == 0 { sys_exit(code) } 105 let st: *i64 = sys_mmap(16) as *i64 106 sys_wait4(pid, st, 0) 107 return st[0] 108} 109 110func main(argc: i64, argv: *i64) -> i64 { 111 wr_puts("=== nx_waitrc_gate -- a killed process must not read as a clean one ===\n\x00" as *u8) 112 let ctr: *i64 = gv_ctr() 113 114 let st_kill: i64 = wr_status_of_sigkill() 115 let st_clean: i64 = wr_status_of_exit(0) 116 let st_seven: i64 = wr_status_of_exit(WR_CHILD_EXIT) 117 118 let sig: i64 = wait_term_signal(st_kill) 119 let old_reads: i64 = wait_exit_code(st_kill) 120 let new_reads: i64 = wait_status_rc(st_kill) 121 122 wr_puts(" sigkill status=\x00" as *u8); wr_putn(st_kill) 123 wr_puts(" termsig=\x00" as *u8); wr_putn(sig) 124 wr_puts(" wait_exit_code=\x00" as *u8); wr_putn(old_reads) 125 wr_puts(" wait_status_rc=\x00" as *u8); wr_putn(new_reads) 126 wr_puts("\n\x00" as *u8) 127 128 gv_check("fixture-reached-condition-the-child-really-died-by-SIGKILL\x00" as *u8, 129 (sig == WR_SIGKILL) as i64, ctr) 130 131 gv_check("killed-child-reads-128-plus-signal\x00" as *u8, 132 (new_reads == (128 + WR_SIGKILL)) as i64, ctr) 133 gv_check("killed-child-is-NON-ZERO-so-every-caller-branching-on-rc-sees-it\x00" as *u8, 134 (new_reads != 0) as i64, ctr) 135 136 gv_check("neg-control-the-OLD-accessor-reads-ZERO-on-that-very-status-the-defect\x00" as *u8, 137 (old_reads == 0) as i64, ctr) 138 gv_check("neg-control-the-two-rulers-DISAGREE-on-a-signal-death\x00" as *u8, 139 (old_reads != new_reads) as i64, ctr) 140 141 gv_check("positive-control-clean-exit-0-reads-0\x00" as *u8, 142 (wait_status_rc(st_clean) == 0) as i64, ctr) 143 gv_check("positive-control-ordinary-exit-7-reads-7\x00" as *u8, 144 (wait_status_rc(st_seven) == WR_CHILD_EXIT) as i64, ctr) 145 gv_check("positive-control-normal-exits-UNCHANGED-both-rulers-agree\x00" as *u8, 146 ((wait_status_rc(st_clean) == wait_exit_code(st_clean)) & (wait_status_rc(st_seven) == wait_exit_code(st_seven))) as i64, ctr) 147 148 // THE CRASH GUARD, MEASURED RATHER THAN ASSUMED. This is why SIGSEGV is NOT the dangerous case 149 // here and why the first version of this gate was RED: the guard converts a segfault into an 150 // ordinary exit, so wait_exit_code is already correct about it. Recording it as a tooth means 151 // the next reader does not have to rediscover it, and if the guard is ever turned off by 152 // default this tooth goes RED and tells them the ground moved. 153 let st_segv: i64 = wr_status_of_segv(argc) 154 wr_puts(" segv status=\x00" as *u8); wr_putn(st_segv) 155 wr_puts(" termsig=\x00" as *u8); wr_putn(wait_term_signal(st_segv)) 156 wr_puts(" exit=\x00" as *u8); wr_putn(wait_exit_code(st_segv)) 157 wr_puts("\n\x00" as *u8) 158 gv_check("crash-guard-converts-SIGSEGV-to-an-EXIT-not-a-signal-death\x00" as *u8, 159 ((wait_term_signal(st_segv) == 0) & (wait_exit_code(st_segv) == WR_GUARD_EXIT)) as i64, ctr) 160 gv_check("and-the-new-ruler-leaves-that-guarded-exit-EXACTLY-as-it-was\x00" as *u8, 161 (wait_status_rc(st_segv) == WR_GUARD_EXIT) as i64, ctr) 162 163 // END-TO-END THROUGH THE SHARED PRIMITIVE. The unit teeth prove the RULE; this proves the 164 // PRODUCTION PATH, because /api/gate_run reaches a gate through tr_run_capture_deadline and 165 // that function's answer becomes the published verdict. 166 sys_mkdir("/tmp/nx_waitrc_gate\x00" as *u8, 0x1ed) 167 var e2e_rc: i64 = 0 - 999 168 var e2e_built: i64 = 0 - 1 169 if cb_anchor_root() == 1 { 170 let src: *u8 = "/tmp/nx_waitrc_gate/crash.nx\x00" 171 let fd: i64 = sys_openat_wr(src, 0x1a4) 172 if fd >= 0 { 173 let body: *u8 = "func opq(a: i64) -> i64 {\n if a > 1000000 { return 1 }\n return 0\n}\n\nfunc main(argc: i64, argv: *i64) -> i64 {\n let z: i64 = opq(argc)\n let p: *i64 = z as *i64\n p[0] = 1\n return 0\n}\n\x00" 174 var bn: i64 = 0 175 while body[bn] != (0 as u8) { bn = bn + 1 } 176 sys_write(fd, body, bn) 177 sys_close(fd) 178 let devnull: i64 = sys_openat_wr("/dev/null\x00" as *u8, 0x1a4) 179 e2e_built = cb_build("_offc/nx_cc_sovereign.elf\x00" as *u8, src, 180 "/tmp/nx_waitrc_gate/crash.s\x00" as *u8, 181 "_build/nx_waitrc_crash.elf\x00" as *u8, 182 0 as *i64, devnull, 183 "_build/nx_waitrc_crash.tmpelf\x00" as *u8, 184 "/tmp/nx_waitrc_gate/crash.asmlog\x00" as *u8) 185 if e2e_built == 0 { 186 let ob: *u8 = sys_mmap(WR_CAP) 187 let ol: *i64 = sys_mmap(16) as *i64 188 let av: *i64 = sys_mmap(32) as *i64 189 av[0] = "_build/nx_waitrc_crash.elf\x00" as *u8 as i64 190 av[1] = 0 191 let wp: *i64 = sys_mmap(16) as *i64 192 let wr: *i64 = sys_mmap(16) as *i64 193 e2e_rc = tr_run_capture_deadline("_build/nx_waitrc_crash.elf\x00" as *u8, av, 194 ob, WR_CAP, ol, WR_DEADLINE_MS, wp, wr) 195 } 196 } 197 } 198 wr_puts(" e2e build=\x00" as *u8); wr_putn(e2e_built) 199 wr_puts(" tr_run_capture_deadline_rc=\x00" as *u8); wr_putn(e2e_rc) 200 wr_puts("\n\x00" as *u8) 201 202 gv_check("e2e-fixture-reached-condition-the-crashing-ELF-actually-BUILT\x00" as *u8, 203 (e2e_built == 0) as i64, ctr) 204 gv_check("e2e-the-SHARED-PRIMITIVE-reports-a-dying-child-as-NON-ZERO\x00" as *u8, 205 ((e2e_built == 0) & (e2e_rc != 0)) as i64, ctr) 206 207 return gv_verdict("NX-WAITRC-GATE\x00" as *u8, ctr, 208 "a signal death is reported as 128+signal by the shared exec primitive; normal exits and guarded crashes are untouched\x00" as *u8) 209}