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}