code wiki / (root) / nx_clock_driver_toolrun_ar.nx

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}