nx_clock_driver_toolrun_ar.nx source
↩ module page · 626 lines · 32853 B
1// nx_tool_run.nx -- R0 of the EXECUTABLE-API rung: the sovereign exec+capture primitive the ecosystem
2// is missing. Today nishifamily.com/api/tools + /mcp only LIST tools (a read-only registry) and MCP
3// tools/call returns a SAFE STUB ("invoked X (capability-authorized)") -- there is NO way to actually
4// RUN an organ and hand back its real stdout. This organ is that missing capability, built hardware-up
5// from raw syscalls (fork/pipe/dup3/execve/wait4), reusing the EXACT proven wrappers from nx_hostctl's
6// hc_dfork_exec so it inherits the same never-brick discipline. NO /bin/sh, NO shell string, NO PATH
7// search -- callers pass an ABSOLUTE ELF path (the allowlist layer that maps tool-name -> path is R1,
8// nx_tool_registry). Synchronous (wait4), unlike hc_dfork_exec's detach -- because an API tools/call
9// needs the child's OUTPUT and EXIT CODE, not a fire-and-forget daemon.
10// license_tier: ORIGINAL
11import "nx_syscalls.nx"
12import "nx_clock_driver_boundary_ar.nx"
13
14// EINTR is -4. sys_read returns -errno, so a NEGATIVE result is an ERROR and r==0 alone is EOF.
15const TR_EINTR: i64 = 0 - 4
16// bounded so a genuinely unreadable fd cannot spin forever (same shape as sys_sleep_ms's guard)
17const TR_EINTR_MAX: i64 = 4096
18
19// ---- EXEC FAILURE MUST SPEAK (2026-08-28, 503 lane) ----------------------------------------------
20// WHAT THIS DELETES. Both capture primitives below used to end the child with a bare sys_exit(127)
21// when execve returned, writing NOTHING to the pipe -- and the header above records that as acceptable
22// (a bad path can only produce a 127 exit + empty capture). But the tools daemon's async lane writes
23// that empty capture to _jobs/job_<id>.out, and its own receipt tells every caller that an EMPTY
24// ARTIFACT MEANS THE JOB IS STILL RUNNING. So a tool that never started and a tool still working are
25// THE SAME OBSERVATION, forever: the caller either waits without bound or concludes the work landed.
26// A PROCESS THAT DIES BEFORE IT CAN SPEAK IS INDISTINGUISHABLE FROM ONE THAT IS STILL THINKING, AND
27// THE SILENCE IS READ AS THE MORE FLATTERING OF THE TWO.
28// MEASURED 2026-08-28 with a control pair: a 130000-byte single argument writes its file and returns
29// OK, while a 132000-byte one produced an empty artifact and no diagnostic anywhere. The CAUSE is not
30// ours -- Linux caps ONE argv element at MAX_ARG_STRLEN = 32 pages = 131072 bytes, a PER-ARGUMENT
31// limit no larger total-argv budget relaxes -- but the SILENCE was ours, and that is what this fixes.
32// The child is already past dup3 here, so fd 1 IS the capture pipe: the parent drains this text and the
33// caller reads a named cause instead of nothing. ONE message, TWO call sites, so they cannot drift.
34const TR_EXIT_EXECFAIL: i64 = 127
35const TR_MODE_0644: i64 = 420
36
37func tr_exec_failed(path: *u8) -> i64 {
38 let m1: *u8 = "NX-EXEC-FAILED rc=127 path=" as *u8
39 var n1: i64 = 0
40 while m1[n1] != (0 as u8) { n1 = n1 + 1 }
41 sys_write(1, m1, n1)
42 var pn: i64 = 0
43 while path[pn] != (0 as u8) { pn = pn + 1 }
44 sys_write(1, path, pn)
45 let m2: *u8 = " -- execve RETURNED instead of replacing this process, so the tool NEVER RAN. This capture is empty BY CAUSE, not because work is still in flight: do NOT read it as RUNNING. Two causes produce it here. (1) The binary is missing, not executable, or not an ELF -- check nx_catalog <name>, and nx_offc_install <name> promoted if a runner forks the _offc mirror. (2) A SINGLE argument exceeds the kernel MAX_ARG_STRLEN of 32 pages = 131072 bytes; that cap is PER-ARGUMENT and no total-size budget relaxes it -- split the payload, or push a large source as anchored edits rather than one whole-file argument. MEASURED 2026-08-28: a 130000-byte argument succeeds and a 132000-byte argument lands here.\n" as *u8
46 var n2: i64 = 0
47 while m2[n2] != (0 as u8) { n2 = n2 + 1 }
48 sys_write(1, m2, n2)
49 sys_exit(TR_EXIT_EXECFAIL)
50 return TR_EXIT_EXECFAIL
51}
52
53// tr_run_capture: fork -> child wires its stdout(+stderr) to a pipe and execve's `path` with `argv`
54// (a NUL-terminated *i64 array of *u8-as-i64, argv[0] conventionally = path) -> parent closes the write
55// end, drains the pipe into out[0..cap), wait4's the child, and returns wait_exit_code (0..255), or a
56// negative sentinel on a harness failure. On child execve failure the child exits 127 (captured as such).
57// *outlen (if non-null) receives the number of bytes captured.
58//
59// never-brick: path is an absolute ELF chosen by the caller's allowlist; a bad path can only produce a
60// 127 exit + empty capture, never a shell injection and never a write to persistent hardware state.
61// RUN A TOOL WHOSE STDOUT IS ITS PRODUCT, NOT ITS COMMENTARY.
62//
63// WHY THIS EXISTS BESIDE tr_run_capture RATHER THAN INSTEAD OF IT. Every capture helper in this lib
64// merges the child's stdout and stderr onto ONE pipe, which is exactly right when the output is a
65// report to be scanned: a diagnostic and a result belong in the same buffer and the caller reads
66// both. It is exactly WRONG when stdout carries an ARTIFACT. The sovereign compiler writes assembly
67// to stdout and progress to stderr, so a caller that captures it merged and writes the buffer to a
68// .s file produces a file with diagnostics glued to the front -- which the assembler then rejects
69// with an error about the SOURCE, sending the reader at the compiler instead of at the plumbing.
70// Measured 2026-09-04: that mistake cost a gate five failing teeth and read exactly like a broken
71// compiler while the compiler was correct.
72//
73// THE ESTATE ALREADY HAD THIS AND COULD NOT REACH IT. `sbr_run` inside nx_sov_build_run does fork +
74// redirect + execve correctly, but it lives inside a PROGRAM, so every other consumer must either
75// duplicate it or work around it -- the duplicate-ruler defect in its most common form. This is the
76// same mechanism lifted into the lib both consumers already import, so there is one of it.
77//
78// Contract: stdout -> out_path (created/truncated), stderr -> err_path when non-zero, else the
79// child's stderr is left on the parent's. Returns the child's exit status, or 128+signal if it died
80// to one -- because a SEGFAULTED tool whose status decodes as 0 is a silent fake success, which is
81// the defect the shell convention exists to prevent. Negative returns are the same named plumbing
82// failures tr_run_capture uses, so a caller can tell "the tool failed" from "I could not run it".
83func tr_run_redirect(path: *u8, argv: *i64, out_path: *u8, err_path: *u8) -> i64 {
84 let ofd: i64 = sys_openat_wr(out_path, TR_MODE_0644)
85 if ofd < 0 { return 0 - 5 } // TR_ERR_OUTOPEN
86 var efd: i64 = 0 - 1
87 if (err_path as i64) != 0 {
88 efd = sys_openat_wr(err_path, TR_MODE_0644)
89 if efd < 0 { sys_close(ofd); return 0 - 6 } // TR_ERR_ERROPEN
90 }
91 let pid: i64 = sys_fork()
92 if pid < 0 {
93 sys_close(ofd)
94 if efd >= 0 { sys_close(efd) }
95 return 0 - 3
96 }
97 if pid == 0 {
98 sys_dup3(ofd, 1, 0)
99 if efd >= 0 { sys_dup3(efd, 2, 0) }
100 sys_close(ofd)
101 if efd >= 0 { sys_close(efd) }
102 var fdc: i64 = 3
103 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 }
104 let envp: *i64 = sys_mmap(16) as *i64
105 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64
106 envp[1] = 0
107 sys_execve_clean(path, argv, envp)
108 // execve failed. There is no pipe to name the cause on here, so exit with the shell's
109 // not-executable status and let the caller's own existence check say which file it was.
110 sys_exit(127)
111 return 0
112 }
113 sys_close(ofd)
114 if efd >= 0 { sys_close(efd) }
115 let stp: *i64 = sys_mmap(16) as *i64
116 let w: i64 = sys_wait4(pid, stp, 0)
117 if w < 0 { return 0 - 4 }
118 return wait_status_rc(stp[0])
119}
120
121func tr_run_capture(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64) -> i64 {
122 if (outlen as i64) != 0 { outlen[0] = 0 }
123 let fds: *i64 = sys_mmap(16) as *i64
124 if sys_pipe2(fds, 0) != 0 { return 0 - 2 } // TR_ERR_PIPE
125 // pipe2 writes int[2] (TWO 32-bit fds) into the first 8 bytes -> read end = low 32 bits of fds[0],
126 // write end = high 32 bits. Reading them as two i64 slots leaves the write fd un-tracked (never closed),
127 // so the reader never sees EOF and blocks forever. Unpack the 32-bit fds explicitly.
128 let packed: i64 = fds[0]
129 let rfd: i64 = packed & 0xFFFFFFFF
130 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF
131
132 let pid: i64 = sys_fork()
133 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 3 } // TR_ERR_FORK
134 if pid == 0 {
135 // ---- CHILD ---- wire stdout(1) + stderr(2) to the pipe write end, close both raw ends, exec.
136 sys_dup3(wfd, 1, 0)
137 sys_dup3(wfd, 2, 0)
138 sys_close(rfd)
139 sys_close(wfd)
140 var fdc: i64 = 3
141 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 }
142 let envp: *i64 = sys_mmap(16) as *i64
143 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64
144 envp[1] = 0
145 sys_execve_clean(path, argv, envp)
146 tr_exec_failed(path) // execve failed -> NAMED cause on the pipe, then 127
147 return 0
148 }
149
150 // ---- PARENT ---- close the write end (so read() sees EOF when the child exits), drain the pipe.
151 sys_close(wfd)
152 var total: i64 = 0
153 var run: i64 = 1
154 var eintr: i64 = 0
155 while run == 1 {
156 if total >= cap { run = 0 } else {
157 let r: i64 = sys_read(rfd, ((out as i64) + total) as *u8, cap - total)
158 // r < 0 is an ERROR, NOT EOF. `r <= 0` conflated them: EINTR -- overwhelmingly SIGCHLD from a
159 // reaped child, which THIS function creates -- ended the drain, returning a SHORT capture that
160 // outlen then reported as COMPLETE. That is how one organ's output arrives truncated at a
161 // DIFFERENT point every run and reads as a flaky gate (MEASURED 2026-08-08: 11 runs of one
162 // nx_coa_gate binary -> 4 different prefixes, ZERO FAIL lines). RESUME on EINTR exactly as
163 // sys_sleep_ms does for clock_nanosleep, whose comment already names this cause.
164 if r > 0 { total = total + r } else {
165 if r == 0 { run = 0 } else {
166 if r == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 }
167 }
168 }
169 }
170 }
171 sys_close(rfd)
172 if (outlen as i64) != 0 { outlen[0] = total }
173
174 let stp: *i64 = sys_mmap(16) as *i64
175 let w: i64 = sys_wait4(pid, stp, 0)
176 if w < 0 { return 0 - 4 } // TR_ERR_WAIT
177 return wait_status_rc(stp[0])
178}
179
180// ---- BOUNDED EXEC (seq1412) ----------------------------------------------------------------
181// tr_run_capture has NO timeout: the parent blocks in the read() drain until EOF. A child that never
182// exits -- or that forks something holding stdout open -- hangs its caller forever. That primitive has
183// 51+ call sites including nx_seat (every session boot), nx_gate_rollup, and the tools-daemon exec path
184// (tea_run), so ONE hanging organ can wedge tools/call for every MCP client.
185//
186// ADDITIVE ON PURPOSE: tr_run_capture's signature and behaviour are untouched, so none of those 51
187// callers change. New/critical callers opt in here.
188//
189// WHY A WATCHDOG FORK AND NOT A POLL LOOP: bounding the drain would need O_NONBLOCK on the read end,
190// and there is no sys_fcntl in nx_syscalls -- so the non-blocking design is not buildable. Instead a
191// second child sleeps the deadline and SIGKILLs the worker; the dying worker drops the last write end,
192// the parent's blocking read() gets its EOF naturally, and no new syscall is required.
193//
194// ORDERING IS LOAD-BEARING: the parent MUST close wfd BEFORE forking the watchdog. Fork it first and
195// the watchdog inherits the write end, so the pipe never reaches EOF even after the worker dies -- the
196// exact hang this exists to remove, reintroduced by the fix.
197const TR_ERR_TIMEOUT: i64 = 0 - 5
198const TR_SIGKILL: i64 = 9
199
200// timeout_ms <= 0 -> delegates to the unbounded tr_run_capture (explicit opt-out, never a silent one).
201// Returns the child's exit code, TR_ERR_TIMEOUT if the deadline fired, or the -2/-3/-4 harness sentinels.
202func tr_run_capture_to(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64) -> i64 {
203 if timeout_ms <= 0 { return tr_run_capture(path, argv, out, cap, outlen) }
204 if (outlen as i64) != 0 { outlen[0] = 0 }
205 let fds: *i64 = sys_mmap(16) as *i64
206 if sys_pipe2(fds, 0) != 0 { return 0 - 2 }
207 let packed: i64 = fds[0]
208 let rfd: i64 = packed & 0xFFFFFFFF
209 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF
210
211 let pid: i64 = sys_fork()
212 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 3 }
213 if pid == 0 {
214 // seq1463: clean signal slate for the child -- SIG_IGN is inherited across fork AND
215 // execve, so a gate launched by a SIGPIPE-ignoring daemon cannot observe its own
216 // disease-control. A harness must not change the state it verifies.
217 sys_default_signal(13)
218 sys_dup3(wfd, 1, 0)
219 sys_dup3(wfd, 2, 0)
220 sys_close(rfd)
221 sys_close(wfd)
222 var fdc: i64 = 3
223 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 }
224 let envp: *i64 = sys_mmap(16) as *i64
225 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64
226 envp[1] = 0
227 sys_execve_clean(path, argv, envp)
228 tr_exec_failed(path)
229 return 0
230 }
231
232 // the worker must be the ONLY holder of the write end before the watchdog exists (see above).
233 sys_close(wfd)
234
235 let wd: i64 = sys_fork()
236 if wd == 0 {
237 sys_close(rfd) // never hold the read end either
238 sys_sleep_ms(timeout_ms)
239 nx_kill(pid, TR_SIGKILL)
240 sys_exit(0)
241 return 0
242 }
243
244 var total: i64 = 0
245 var run: i64 = 1
246 var eintr: i64 = 0
247 while run == 1 {
248 if total >= cap { run = 0 } else {
249 let r: i64 = sys_read(rfd, ((out as i64) + total) as *u8, cap - total)
250 // r < 0 is an ERROR, NOT EOF. `r <= 0` conflated them: EINTR -- overwhelmingly SIGCHLD from a
251 // reaped child, which THIS function creates -- ended the drain, returning a SHORT capture that
252 // outlen then reported as COMPLETE. That is how one organ's output arrives truncated at a
253 // DIFFERENT point every run and reads as a flaky gate (MEASURED 2026-08-08: 11 runs of one
254 // nx_coa_gate binary -> 4 different prefixes, ZERO FAIL lines). RESUME on EINTR exactly as
255 // sys_sleep_ms does for clock_nanosleep, whose comment already names this cause.
256 if r > 0 { total = total + r } else {
257 if r == 0 { run = 0 } else {
258 if r == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 }
259 }
260 }
261 }
262 }
263 sys_close(rfd)
264 if (outlen as i64) != 0 { outlen[0] = total }
265
266 let stp: *i64 = sys_mmap(16) as *i64
267 let w: i64 = sys_wait4(pid, stp, 0)
268
269 // Retire the watchdog whether or not it fired. Leaving it asleep would let it SIGKILL a RECYCLED
270 // pid later -- a timeout that kills an unrelated process is worse than no timeout at all.
271 if wd > 0 {
272 nx_kill(wd, TR_SIGKILL)
273 let wstp: *i64 = sys_mmap(16) as *i64
274 sys_wait4(wd, wstp, 0)
275 }
276
277 if w < 0 { return 0 - 4 }
278 // low 7 bits of the status word = terminating signal. SIGKILL here means the watchdog fired.
279 // (An external SIGKILL -- e.g. the OOM killer -- also reads as TIMEOUT; both mean "did not finish".)
280 let termsig: i64 = stp[0] & 0x7F
281 if termsig == TR_SIGKILL { return TR_ERR_TIMEOUT }
282 return wait_status_rc(stp[0])
283}
284
285// ---- BOUNDED, PROMOTABLE DRAIN (2026-08-22) -----------------------------------------------------
286// tr_run_capture_to's header above records that bounding the drain "would need O_NONBLOCK on the read
287// end, and there is no sys_fcntl in nx_syscalls -- so the non-blocking design is not buildable." That
288// premise is FALSE, and it cost the estate every synchronous-lane 503: poll(2) needs no O_NONBLOCK, and
289// sys_poll ships in nx_syscalls with 50 call sites (corpus_complete=1). A retrieval failure was written
290// into the source as an impossibility, and the watchdog+SIGKILL below it DESTROYED THE ANSWER of every
291// call that outran the window while the work itself ran on and landed.
292// ★A LAW RECORDED IN A HEADER IS STILL A HYPOTHESIS -- THIS ONE WAS REFUTED BY ONE GREP.
293// This function is the repair, and it is ADDITIVE: tr_run_capture / tr_run_capture_to and their 51
294// consumers are byte-untouched (rule 19).
295// * deadline_ms is a WHOLE-CALL budget, not a per-read idle timer -- the distinction the edge's
296// SO_RCVTIMEO gets wrong. remaining = deadline_ms - (now - t0), recomputed every pass.
297// * poll > 0 -> read (POLLIN is set, so it cannot block); the r>0 / r==0 / EINTR discipline is
298// copied from tr_run_capture_to verbatim, truncation-at-a-different-point bug included.
299// * poll == 0 -> THE DEADLINE. DO NOT KILL. The caller receives TR_PROMOTE plus the live worker pid and
300// the live read end, and now owns a worker that will finish and a pipe that will carry its
301// tail. No watchdog fork exists on this path: one FEWER process per sync call.
302// * deadline_ms <= 0 -> delegates to the unbounded tr_run_capture -- the explicit opt-out contract
303// tr_run_capture_to already has, never a silent one.
304// pollfd is the kernel ABI struct { i32 fd; i16 events; i16 revents } = 8 bytes.
305// ⚠INCUMBENT NAMED, NOT HIDDEN: nx_ts_drain_lib.nx carries tsd_pollfd_set / tsd_pollfd_ready for this
306// same struct. It is deliberately NOT imported here: nx_tool_run sits in the closure of 51 consumers
307// and that lib transitively imports nx_resmon_lib + nx_itoa_lib, so a symbol collision in any ONE of
308// those closures would break a build nobody in this lane can see. The consolidation is nx_oo_extract
309// lifting BOTH copies into a tiny nx_pollfd_lib -- that is the named remedy; this is the named debt.
310const TR_PROMOTE: i64 = 0 - 7 // distinct from TR_ERR_TIMEOUT (-5) and TR_ERR_CHDIR (-6)
311const TR_POLLFD_BYTES: i64 = 8 // sizeof(struct pollfd)
312const TR_POLLIN: i64 = 1 // POLLIN
313const TR_PF_EV_OFF: i64 = 4 // offsetof(struct pollfd, events)
314const TR_PF_RE_OFF: i64 = 6 // offsetof(struct pollfd, revents)
315const TR_PF_FD_BYTES: i64 = 4 // sizeof(i32 fd)
316const TR_BITS_PER_BYTE: i64 = 8
317const TR_BYTE_MASK: i64 = 0xff
318func tr_pollfd_set(p: *u8, fd: i64) -> i64 {
319 var k: i64 = 0
320 while k < TR_PF_FD_BYTES { p[k] = ((fd >> (k * TR_BITS_PER_BYTE)) & TR_BYTE_MASK) as u8; k = k + 1 }
321 p[TR_PF_EV_OFF] = TR_POLLIN as u8
322 p[TR_PF_EV_OFF + 1] = 0 as u8
323 p[TR_PF_RE_OFF] = 0 as u8
324 p[TR_PF_RE_OFF + 1] = 0 as u8
325 return 0
326}
327// Returns the child's exit code if it finished inside the deadline, TR_PROMOTE (with *out_pid / *out_rfd
328// filled) if the deadline fired first, or the -2/-3/-4 harness sentinels. On TR_PROMOTE the caller OWNS
329// the worker and the read end: it must drain rfd to EOF (the tail of the answer) or close it.
330func tr_run_capture_deadline(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, deadline_ms: i64, out_pid: *i64, out_rfd: *i64) -> i64 {
331 if (out_pid as i64) != 0 { out_pid[0] = 0 }
332 if (out_rfd as i64) != 0 { out_rfd[0] = 0 - 1 }
333 if deadline_ms <= 0 { return tr_run_capture(path, argv, out, cap, outlen) }
334 if (outlen as i64) != 0 { outlen[0] = 0 }
335 let fds: *i64 = sys_mmap(16) as *i64
336 if sys_pipe2(fds, 0) != 0 { return 0 - 2 }
337 let packed: i64 = fds[0]
338 let rfd: i64 = packed & 0xFFFFFFFF
339 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF
340 let t0: i64 = sys_now_ms()
341 let pid: i64 = sys_fork()
342 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 3 }
343 if pid == 0 {
344 sys_default_signal(13)
345 sys_dup3(wfd, 1, 0)
346 sys_dup3(wfd, 2, 0)
347 sys_close(rfd)
348 sys_close(wfd)
349 var fdc: i64 = 3
350 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 }
351 let envp: *i64 = sys_mmap(16) as *i64
352 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64
353 envp[1] = 0
354 sys_execve_clean(path, argv, envp)
355 tr_exec_failed(path)
356 return 0
357 }
358 sys_close(wfd)
359 let pfd: *u8 = sys_mmap(TR_POLLFD_BYTES)
360 var total: i64 = 0
361 var run: i64 = 1
362 var eintr: i64 = 0
363 var promoted: i64 = 0
364 while run == 1 {
365 if total >= cap { run = 0 } else {
366 var remaining: i64 = deadline_ms - (sys_now_ms() - t0)
367 if remaining < 0 { remaining = 0 }
368 tr_pollfd_set(pfd, rfd)
369 let pr: i64 = sys_poll(pfd, 1, remaining)
370 if pr > 0 {
371 let r: i64 = sys_read(rfd, ((out as i64) + total) as *u8, cap - total)
372 if r > 0 { total = total + r } else {
373 if r == 0 { run = 0 } else {
374 if r == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 }
375 }
376 }
377 } else {
378 if pr == 0 { promoted = 1; run = 0 } else {
379 if pr == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 }
380 }
381 }
382 }
383 }
384 if (outlen as i64) != 0 { outlen[0] = total }
385 if promoted == 1 {
386 if (out_pid as i64) != 0 { out_pid[0] = pid }
387 if (out_rfd as i64) != 0 { out_rfd[0] = rfd }
388 return TR_PROMOTE
389 }
390 sys_close(rfd)
391 let stp: *i64 = sys_mmap(16) as *i64
392 let w: i64 = sys_wait4(pid, stp, 0)
393 if w < 0 { return 0 - 4 }
394 return wait_status_rc(stp[0])
395}
396
397// ---- SANDBOXED RUN (2026-08-06) ---------------------------------------------------------------
398// ***CONTAINMENT BEATS ENUMERATION.*** On 2026-08-06 a coverage sweep ran nx_cap_grant_e2e_gate -- a
399// destructive end-to-end test that exercises the REAL capability system in place. It rewrote
400// nishihost/tool_allowlist.conf with a 52-byte fixture (750 GREEN rows lost) and left
401// tools_cap_secret.key ABSENT, so nx_tools_api fell back to its forgeable placeholder and EVERY
402// capability in the estate was denied. A denylist of such gates was written afterwards, and a denylist
403// is an ENUMERATION -- it only ever protects against the instances somebody already thought of, and its
404// detector is a source-literal proxy, so it is a FLOOR not a total.
405// This is the containment: run the child with its OWN cwd, so a gate reaching for ../<production-file>
406// lands inside a scratch tree instead of the live one. It cannot stop an ABSOLUTE path -- nothing short
407// of a namespace can -- so it composes with the denylist rather than replacing it. Defence in depth,
408// stated honestly, because a containment that oversells itself is how the next one gets skipped.
409// ***FAIL-CLOSED: if the chdir does not take, the child EXITS rather than running in the wrong tree.***
410// Running the subject in the directory you were trying to protect is the exact failure this prevents,
411// so "could not chdir, so proceeded" must never be reachable.
412// ⚠<path> is resolved AFTER the chdir -- pass it absolute, or relative to <cwd> (e.g. ../_build/x.elf).
413const TR_ERR_CHDIR: i64 = 0 - 6
414func tr_run_capture_cwd(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64, cwd: *u8) -> i64 {
415 if (outlen as i64) != 0 { outlen[0] = 0 }
416 let fds: *i64 = sys_mmap(16) as *i64
417 if sys_pipe2(fds, 0) != 0 { return 0 - 2 }
418 let packed: i64 = fds[0]
419 let rfd: i64 = packed & 0xFFFFFFFF
420 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF
421 let pid: i64 = sys_fork()
422 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 3 }
423 if pid == 0 {
424 sys_default_signal(13)
425 sys_dup3(wfd, 1, 0)
426 sys_dup3(wfd, 2, 0)
427 sys_close(rfd)
428 sys_close(wfd)
429 var fdc: i64 = 3
430 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 }
431 if sys_chdir(cwd) != 0 { sys_exit(126) }
432 let envp: *i64 = sys_mmap(16) as *i64
433 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64
434 envp[1] = 0
435 sys_execve_clean(path, argv, envp)
436 tr_exec_failed(path)
437 return 0
438 }
439 sys_close(wfd)
440 var wd: i64 = 0
441 if timeout_ms > 0 {
442 if dfi_watchdog_denied()==1 { wd=DFI_EAGAIN } else { wd = sys_fork() }
443 if wd == 0 {
444 sys_close(rfd)
445 sys_sleep_ms(timeout_ms)
446 nx_kill(pid, TR_SIGKILL)
447 sys_exit(0)
448 return 0
449 }
450 }
451 // TEST ONLY: child preflight refuses to enter the scheduler without this fork receipt.
452 if wd > 0 { dfi_mark("witness.watchdog-ready" as *u8) }
453 else { dfi_mark("witness.watchdog-refused" as *u8) }
454 var total: i64 = 0
455 var run: i64 = 1
456 var eintr: i64 = 0
457 while run == 1 {
458 if total >= cap { run = 0 } else {
459 let r: i64 = sys_read(rfd, ((out as i64) + total) as *u8, cap - total)
460 // r < 0 is an ERROR, NOT EOF. `r <= 0` conflated them: EINTR -- overwhelmingly SIGCHLD from a
461 // reaped child, which THIS function creates -- ended the drain, returning a SHORT capture that
462 // outlen then reported as COMPLETE. That is how one organ's output arrives truncated at a
463 // DIFFERENT point every run and reads as a flaky gate (MEASURED 2026-08-08: 11 runs of one
464 // nx_coa_gate binary -> 4 different prefixes, ZERO FAIL lines). RESUME on EINTR exactly as
465 // sys_sleep_ms does for clock_nanosleep, whose comment already names this cause.
466 if r > 0 { total = total + r } else {
467 if r == 0 { run = 0 } else {
468 if r == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 }
469 }
470 }
471 }
472 }
473 sys_close(rfd)
474 if (outlen as i64) != 0 { outlen[0] = total }
475 let stp: *i64 = sys_mmap(16) as *i64
476 let w: i64 = sys_wait4(pid, stp, 0)
477 if wd > 0 {
478 nx_kill(wd, TR_SIGKILL)
479 let wstp: *i64 = sys_mmap(16) as *i64
480 sys_wait4(wd, wstp, 0)
481 }
482 if w < 0 { return 0 - 4 }
483 let termsig: i64 = stp[0] & 0x7F
484 if termsig == TR_SIGKILL { return TR_ERR_TIMEOUT }
485 let ec: i64 = wait_status_rc(stp[0])
486 if ec == 126 { return TR_ERR_CHDIR }
487 return ec
488}
489
490// ---- TRUNCATION-AWARE CAPTURE (2026-08-08, debt 1786235483) ---------------------------------
491// ***THE THREE DRAIN LOOPS ABOVE CANNOT SAY "I WAS CUT OFF".*** Each exits on `total >= cap` and
492// returns the child's exit code with outlen[0] == cap -- byte-for-byte indistinguishable from a
493// child that emitted EXACTLY cap bytes. With 51+ call sites that makes every buffer constant in the
494// estate a SILENT SAMPLING DECISION made by whoever picked it. Measured instance: nx_debtmine read
495// 1048576 of a 4921787-byte plane and reported truncated=0 (debt 1786235467).
496//
497// ADDITIVE BY CONSTRUCTION: the three existing functions are NOT touched, so all 51 callers compile
498// byte-identical and none needs rebuilding to stay correct. Measurement callers opt in here. Same
499// precedent as tr_run_capture_to in this file. FOLLOW-ON (deliberately not done in the same change):
500// fold those three loops onto tr_drain_tr so the estate has ONE drain, once this is proven in prod.
501//
502// THE DISCRIMINATOR: once the buffer is full, attempt ONE more read. >0 means the child had more to
503// say (CUT); EOF means it fit exactly. That byte is already unrecoverable -- the capture is over cap
504// either way -- and consuming it lets the child reach EOF instead of blocking on a full pipe.
505// ***A FIXTURE SMALLER THAN cap CANNOT EXERCISE THIS AT ALL***, which is why the gate straddles the
506// boundary in BOTH directions: cap-1 (must report CUT) and exactly-cap (must report FIT).
507const TR_FIT: i64 = 0
508const TR_CUT: i64 = 1
509func tr_drain_tr(rfd: i64, out: *u8, cap: i64, trunc: *i64) -> i64 {
510 if (trunc as i64) != 0 { trunc[0] = TR_FIT }
511 var total: i64 = 0
512 var run: i64 = 1
513 var eintr: i64 = 0
514 while run == 1 {
515 if total >= cap { run = 0 } else {
516 let r: i64 = sys_read(rfd, ((out as i64) + total) as *u8, cap - total)
517 // r < 0 is an ERROR, NOT EOF. `r <= 0` conflated them: EINTR -- overwhelmingly SIGCHLD from a
518 // reaped child, which THIS function creates -- ended the drain, returning a SHORT capture that
519 // outlen then reported as COMPLETE. That is how one organ's output arrives truncated at a
520 // DIFFERENT point every run and reads as a flaky gate (MEASURED 2026-08-08: 11 runs of one
521 // nx_coa_gate binary -> 4 different prefixes, ZERO FAIL lines). RESUME on EINTR exactly as
522 // sys_sleep_ms does for clock_nanosleep, whose comment already names this cause.
523 if r > 0 { total = total + r } else {
524 if r == 0 { run = 0 } else {
525 if r == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 }
526 }
527 }
528 }
529 }
530 if total >= cap {
531 let probe: *u8 = sys_mmap(16)
532 let extra: i64 = sys_read(rfd, probe, 1)
533 if extra > 0 { if (trunc as i64) != 0 { trunc[0] = TR_CUT } }
534 }
535 return total
536}
537
538// tr_run_capture_tr: bounded capture that REPORTS whether it was cut off. trunc may be null.
539func tr_run_capture_tr(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64, trunc: *i64) -> i64 {
540 if (outlen as i64) != 0 { outlen[0] = 0 }
541 if (trunc as i64) != 0 { trunc[0] = TR_FIT }
542 let fds: *i64 = sys_mmap(16) as *i64
543 if sys_pipe2(fds, 0) != 0 { return 0 - 2 }
544 let packed: i64 = fds[0]
545 let rfd: i64 = packed & 0xFFFFFFFF
546 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF
547 let pid: i64 = sys_fork()
548 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 3 }
549 if pid == 0 {
550 sys_default_signal(13)
551 sys_dup3(wfd, 1, 0)
552 sys_dup3(wfd, 2, 0)
553 sys_close(rfd)
554 sys_close(wfd)
555 var fdc: i64 = 3
556 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 }
557 let envp: *i64 = sys_mmap(16) as *i64
558 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64
559 envp[1] = 0
560 sys_execve_clean(path, argv, envp)
561 tr_exec_failed(path)
562 return 0
563 }
564 // ORDERING IS LOAD-BEARING (same law as tr_run_capture_to): close wfd BEFORE forking the
565 // watchdog, or the watchdog holds the write end and the pipe never reaches EOF.
566 sys_close(wfd)
567 var wd: i64 = 0
568 if timeout_ms > 0 {
569 wd = sys_fork()
570 if wd == 0 {
571 sys_close(rfd)
572 sys_sleep_ms(timeout_ms)
573 nx_kill(pid, TR_SIGKILL)
574 sys_exit(0)
575 return 0
576 }
577 }
578 let total: i64 = tr_drain_tr(rfd, out, cap, trunc)
579 sys_close(rfd)
580 if (outlen as i64) != 0 { outlen[0] = total }
581 let stp: *i64 = sys_mmap(16) as *i64
582 let w: i64 = sys_wait4(pid, stp, 0)
583 if wd > 0 {
584 nx_kill(wd, TR_SIGKILL)
585 let wstp: *i64 = sys_mmap(16) as *i64
586 sys_wait4(wd, wstp, 0)
587 }
588 if w < 0 { return 0 - 4 }
589 let termsig: i64 = stp[0] & 0x7F
590 if termsig == TR_SIGKILL { return TR_ERR_TIMEOUT }
591 return wait_status_rc(stp[0])
592}
593
594// tr_run1: convenience for the common "run ELF with a single string arg" case. Builds argv = [path, arg, 0].
595// arg may be null -> argv = [path, 0].
596func tr_run1(path: *u8, arg: *u8, out: *u8, cap: i64, outlen: *i64) -> i64 {
597 let argv: *i64 = sys_mmap(32) as *i64
598 argv[0] = path as i64
599 if (arg as i64) == 0 { argv[1] = 0 } else { argv[1] = arg as i64; argv[2] = 0 }
600 return tr_run_capture(path, argv, out, cap, outlen)
601}
602
603// tr_run1_to: bounded twin of tr_run1. Same argv shaping, with a deadline.
604func tr_run1_to(path: *u8, arg: *u8, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64) -> i64 {
605 let argv: *i64 = sys_mmap(32) as *i64
606 argv[0] = path as i64
607 if (arg as i64) == 0 { argv[1] = 0 } else { argv[1] = arg as i64; argv[2] = 0 }
608 return tr_run_capture_to(path, argv, out, cap, outlen, timeout_ms)
609}
610
611// tr_contains: 1 if the NUL-terminated needle occurs in buf[0..n), else 0. For gates asserting on captured stdout.
612func tr_contains(buf: *u8, n: i64, needle: *u8) -> i64 {
613 var nl: i64 = 0
614 while needle[nl] != (0 as u8) { nl = nl + 1 }
615 if nl == 0 { return 1 }
616 if n < nl { return 0 }
617 var i: i64 = 0
618 while i <= n - nl {
619 var m: i64 = 1
620 var c: i64 = 0
621 while c < nl { if buf[i + c] != needle[c] { m = 0; c = nl } else { c = c + 1 } }
622 if m == 1 { return 1 }
623 i = i + 1
624 }
625 return 0
626}