code wiki / (root) / nx_capture_owned_gate_expanded_t218.nx

nx_capture_owned_gate_expanded_t218.nx source

↩ module page · 3983 lines · 204898 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 11// syscalls.nx -- thin __syscall wrappers used across modules. 12// 13// Sovereign path: no libc. Every memory allocation, file op, and 14// clock read in the rest of the runtime routes through one of these 15// helpers. Numbers match Linux RV64; NishiOS uses the same set. 16// 17// Extracted from runtime.nx and ir.nx's copy-pasted helpers so the 18// module-import build doesn't produce duplicate symbols. 19 20// Tier aliases (nx_size / nx_idx / nx_fd / ...) ride along with the 21// syscall shelf: 141 runtime files use `as nx_size` etc. and only 22// compiled historically because the old parser silently void-cast 23// unknown type names (T#nx-int-alias-size-0 closed that hole LOUDLY, 24// which exposed the missing import). nx_tier.nx is pure type 25// aliases (0 funcs); prepass_register_aliases skips duplicates, so 26// modules that also import it directly stay fine. 27// nx_tier.nx -- substrate-wide tier configuration. 28// 29// Single point of edit for scale-agnostic substrate. Per user 30// directive 2026-05-13: "with the i64 it looks hardcoded everywhere 31// if we really want this dynamic dont we want that to be a changeable 32// value everywhere so it can switch to i128 and i256 etc." 33// 34// Per cardinals: 35// - feedback-numeric-tier-ladder.md (N0..N9 swap) 36// - feedback-scale-agnostic-substrate.md (MCU..HPC swap) 37// - feedback-substrate-additive-not-restrictive.md (declare cost) 38// 39// SEMANTIC ALIASES (not all should swap simultaneously): 40// 41// nx_int -- DEFAULT ARITHMETIC integer. Swappable across the 42// numeric tier ladder. Swap this to i128 to make the 43// entire substrate compute in 128-bit integers. 44// 45// nx_size -- MEMORY-SIZE integer. Always platform-pointer-width. 46// Used for buffer sizes, mmap byte counts, struct 47// sizes. Does NOT swap with nx_int -- changing this 48// would break pointer arithmetic. Stays i64 on RV64. 49// 50// nx_idx -- ARRAY-INDEX integer. Same width as nx_size on 51// flat-memory targets. Distinct alias so future 52// GPU/distributed targets can change indexing without 53// touching arithmetic. 54// 55// nx_byte -- The byte type. Stays u8. Distinct alias so MCU 56// targets that emulate u16-byte memory could rebind. 57// 58// HARDWARE-TIER BUFFER SIZES (declare cost, don't restrict): 59// 60// NX_BUF_TINY -- 64 B (MCU-friendly; stack-safe) 61// NX_BUF_SMALL -- 256 B (MCU heap-friendly) 62// NX_BUF_MEDIUM -- 4096 B (page-size; workstation default) 63// NX_BUF_LARGE -- 64 KiB (server-friendly) 64// NX_BUF_HUGE -- 1 MiB (HPC; assumes virtual memory) 65// 66// Use these instead of `sys_mmap(4096)` etc. so the substrate 67// announces its memory footprint and tier-incompatible code can 68// be flagged by audit. 69// 70// HARDWARE TIER (informational; downstream code may branch): 71// 72// NX_TIER_MCU = 0 -- microcontroller, kilobytes RAM 73// NX_TIER_SOVEREIGN_CHIP = 1 -- custom silicon, ~MB RAM 74// NX_TIER_FAMILY_DEVICE = 2 -- phone/router, ~GB RAM 75// NX_TIER_WORKSTATION = 3 -- laptop/desktop, ~10-100 GB RAM 76// NX_TIER_SERVER = 4 -- server-class, ~TB RAM 77// NX_TIER_HPC = 5 -- cluster, distributed 78// 79// COMPILE-TIME SWAP for nx_int (uncomment exactly one line): 80 81// THIS FILE IS THE SINGLE DEFINITION SITE for substrate-wide types. 82// Per user directive 2026-05-13: only this file (and platform-ABI 83// definition files like nx_syscalls.nx) should declare bare i64. 84// Every other substrate module uses the aliases below. 85 86// ===== arithmetic-tier aliases (swappable per nx_int tier ladder) ===== 87 88type nx_int = i64 // N1 -- default; 9 quintillion, fits all physical scales 89// type nx_int = i32 // N0 -- MCU / embedded 90// type nx_int = i128 // N2 -- queued; needs nx_i128 backend ops 91// type nx_int = i256 // N3 -- shipped (nx_i256.nx); cosmology / crypto 92 93// ===== platform-width aliases (stay at pointer width) ================= 94 95type nx_size = i64 // memory-size / byte-count 96type nx_idx = i64 // array-index 97type nx_byte = u8 // single-byte unit 98 99// ===== POSIX/Linux platform-ABI aliases (mandated 64-bit on RV64) ==== 100// 101// Each is a 64-bit integer by Linux RV64 ABI. Renamed here so substrate 102// code never writes bare `i64` for these semantic types. 103 104type nx_fd = i64 // file descriptor (kernel-mandated width) 105type nx_exit = i64 // exit / status code (main() return) 106type nx_pid = i64 // process id 107type nx_uid = i64 // user id 108type nx_gid = i64 // group id 109type nx_syscall_num = i64 // Linux syscall number 110type nx_off = i64 // file offset (off_t) 111type nx_errno = i64 // errno (negative on syscall failure) 112 113// ===== SEMANTIC TYPE GENEALOGY (added 2026-05-20) ====================== 114// 115// Per cardinal [[feedback-type-genealogy-math-cardinal-not-script]] 116// AND its immediate refinement (same session): every alias collapsing 117// to i64 is "y2k incestuous" -- relabeling, not genealogy. Real 118// semantic types pick the APPROPRIATE underlying width based on 119// the physics of the values they represent: 120// 121// - Small sealed enums (15 outcomes, 18 probe kinds) -> u8 122// - Display pixel coords (~32M max realistic) -> i32 123// - Q10 / Q14 fixed-point (values * 1024 / 16384) -> i32 124// - 32-bit color packs (RGBA8888) -> u32 125// - Q20 fixed-point (values * 1048576) -> i64 126// - Wide color packs (RGBA16161616, PRESERVE_ALL) -> u64 127// - Timestamps (ns / us / ms / cycles) -> i64 (2038 Y2K38) 128// - 64-bit hash digests -> u64 129// - Cryptographic hashes (SHA-256, SHA-512) -> STRUCT (multi-word; queued) 130// - Virtual addresses on 64-bit ISA -> u64 131// 132// Each type is a child of its PHYSICALLY-APPROPRIATE parent 133// (i8/u8/i32/u32/i64/u64), not blanket-i64. This breaks the 134// y2k-incestuous trap where renaming i64 N ways pretends to be 135// type discipline while every value silently shares one width. 136 137// ----- TIME family (all i64; ns/us/ms/cycles legitimately need it) ----- 138// 2038 Y2K38 lurks for 32-bit time_t; i64 is the substrate-honest 139// choice. ms/us/ns + cycles all i64. s_q14 needs only i32 range 140// (val*16384 fits comfortably in i32 for typical second scales) but 141// we stay at i64 to compose cleanly with the i64 time arithmetic 142// across the substrate. 143type nx_ns = i64 // nanoseconds (since boot, monotonic) 144type nx_us = i64 // microseconds (since boot, monotonic) 145type nx_ms = i64 // milliseconds (since epoch, wall) 146type nx_s_q14 = i64 // seconds in Q14 fixed-point 147type nx_cycles = i64 // CPU cycle count 148 149// ----- HASH family (non-cryptographic 64-bit; crypto = STRUCT) ----- 150// FNV-1a / xxhash digest is u64 by spec. SHA-256 / SHA-512 / BLAKE 151// hashes are MULTI-WORD; they're declared as structs in 152// nx_sha256.nx / nx_sha512.nx / nx_blake2b.nx (each carries its own 153// fixed-size byte array; NOT i64). 154type nx_hash64 = u64 // FNV-1a / xxhash / truncated SHA -- 64-bit digest 155 156// ----- ETG family (sealed enums; small value space -> u8) ----- 157// nx_outcome_id sealed enum has 11 values; u8 fits 256 158// nx_probe_kind sealed enum has 18 values; u8 fits 256 159// nx_claim_source sealed enum has 13 values; u8 fits 256 160// nx_silicon_serial is a content-addressed identity HASH; u64. 161type nx_outcome_id = u8 // NX_ETG_OUTCOME_* (11 values; u8 fits) 162type nx_probe_kind = u8 // NX_ETG_PROBE_* (18 values; u8 fits) 163type nx_claim_source = u8 // NX_ETG_CLAIM_* (13 values; u8 fits) 164type nx_silicon_serial = u64 // per-die identity hash (cryptographic-strength width) 165 166// ----- PERF family (sealed enums) ----- 167type nx_pathology_id = u8 // NX_PERF_PATH_* (15 values; u8 fits) 168type nx_flow_state_id = u8 // NX_FLOW_STATE_* (6 values; u8 fits) 169 170// ----- FIXED-POINT family (width chosen by precision*range) ----- 171// Q10: value * 1024. Typical seed values are 0..255 so q10 max is 172// ~261K; i32 holds up to ~2.1B -> plenty of headroom. 173// Q14: value * 16384. Typical max around 16K of seed -> q14 ~ 2.6e8; 174// i32 holds up to 2.1e9 -> headroom for a few decimal seconds. 175// Q20: value * 1048576. Wider precision; needs i64 to avoid wrap. 176type nx_q10 = i32 // val * 1024; ~0.001 precision 177type nx_q14 = i32 // val * 16384; ~6e-5 precision 178type nx_q20 = i64 // val * 1048576; ~1e-6 precision 179 180// ----- GRAPHICS family (display coords + color packs at real widths) ----- 181// Modern displays are well within 32-bit pixel addressing. 182// 8K display = 7680x4320 pixels. i32 holds 2.1B -> plenty. 183// nx_color_rgba8 = 32-bit packed RGBA (the common case) 184// nx_color_rgba16 = 64-bit packed RGBA16161616 (HDR / wide gamut) 185type nx_pixel_x = i32 // screen X in pixels 186type nx_pixel_y = i32 // screen Y in pixels 187type nx_color_rgba8 = u32 // RGBA8888 packed 188type nx_color_rgba16 = u64 // RGBA16161616 packed (HDR / preserve-all) 189 190// ----- PERCEPTUAL family (sealed enum; small value space) ----- 191// nx_perceptual_profile has ~40 declared values up through 192// NX_PERCEPT_PRESERVE_ALL = 9999. Sentinel value 9999 needs i16, 193// not u8. i16 fits -32768..32767 with room for sentinels. 194type nx_perceptual_profile = i16 // NX_PERCEPT_* (~40 values + 9999 sentinel) 195 196// ----- ADDRESS family (virtual addresses on 64-bit ISA) ----- 197// Pointer-width is u64 on all our supported 64-bit targets 198// (RV64 / x86_64 / AArch64 / ppc64le / loongarch64 / mips64 / 199// s390x / RV32 uses u32 -- TODO: tier-conditional). 200type nx_addr = u64 // raw virtual address (caller casts to *u8) 201 202// nx_capability_manifest: 203// variant_class: tier_config 204// variant_id: tier_config_v1_global 205// requires_isa: [rv32i, rv32imac, rv64imac, rv64imacv, x86_64, aarch64, armv7a, cortex_m, avr, xtensa, wasm32] 206// requires_syscalls: [] 207// requires_ram_min_b: 0 // pure-const + typedef module, no runtime cost 208// tier_floor: NX_TIER_MCU 209// tier_ceiling: NX_TIER_HPC 210// cost_model: 211// flops_per_n: 0.0 212// bytes_per_n: 0.0 213// syscalls_per_n: 0.0 214// adversary_class: THREAT_OPPORTUNISTIC 215// 216// Note: This file is the substrate's TIER ENUM SOURCE OF TRUTH. It 217// has no variants by design (it IS the variant_class taxonomy that 218// other primitives' tier_floor / tier_ceiling reference). Manifest 219// declared for hygiene completeness; selector will skip it. 220 221// ---- buffer-size constants (use instead of bare numbers) ------- 222 223const NX_BUF_TINY: nx_size = 64 224const NX_BUF_SMALL: nx_size = 256 225const NX_BUF_MEDIUM: nx_size = 4096 226const NX_BUF_LARGE: nx_size = 65536 227const NX_BUF_HUGE: nx_size = 1048576 228 229// ---- hardware tier sentinels ----------------------------------- 230 231const NX_TIER_MCU: nx_int = 0 232const NX_TIER_SOVEREIGN_CHIP: nx_int = 1 233const NX_TIER_FAMILY_DEVICE: nx_int = 2 234const NX_TIER_WORKSTATION: nx_int = 3 235const NX_TIER_SERVER: nx_int = 4 236const NX_TIER_HPC: nx_int = 5 237 238// ---- numeric tier sentinels (informational) -------------------- 239 240const NX_NUM_N0_I32: nx_int = 0 241const NX_NUM_N1_I64: nx_int = 1 242const NX_NUM_N2_I128: nx_int = 2 243const NX_NUM_N3_I256: nx_int = 3 244const NX_NUM_N4_I512: nx_int = 4 245const NX_NUM_N5_BIGINT: nx_int = 5 246 247// ---- byte-width of substrate types (replace bare `8` / `4`) ---- 248// 249// Use these wherever you need the byte count of a substrate type -- 250// e.g., sys_mmap(N * NX_SIZEOF_NX_SIZE) to allocate N nx_size slots. 251// Swap nx_int's underlying type and ONLY this constant changes. 252 253const NX_SIZEOF_NX_INT: nx_size = 8 // nx_int currently i64 -> 8 bytes 254const NX_SIZEOF_NX_SIZE: nx_size = 8 // nx_size always pointer-width 255const NX_SIZEOF_NX_IDX: nx_size = 8 // nx_idx alias of nx_size 256 257// ---- POSIX stdio file descriptors (replace bare 0/1/2) --------- 258 259const NX_FD_STDIN: nx_fd = 0 260const NX_FD_STDOUT: nx_fd = 1 261const NX_FD_STDERR: nx_fd = 2 262 263const SYS_MAGIC_1024: i64 = 1024 264const SYS_MAGIC_1000000: i64 = 1000000 265const SYS_MAGIC_4294967296: i64 = 4294967296 266// first read window for a size-UNKNOWABLE file (lseek END <= 0); doubles while it fills -- see sys_read_file 267const SYS_READ_GROW_INIT: i64 = 65536 268const SYS_MAGIC_100000: i64 = 100000 269 270// ---- syscall numbers (per-target) ---- 271// 272// Cross-target via the macro processor (cardinal landed 2026-05-20: 273// feedback-hardware-agnostic-is-robustness -- the substrate must 274// compile + run on every silicon we point it at). Default path 275// (TARGET_X86_64 not defined) carries Linux RV64 numbers used by 276// qemu-RV64 + NishiOS. When nxc2 is invoked with --target x86_64 277// main.c pre-defines @macro TARGET_X86_64 1 so this file resolves 278// to x86_64 Linux ABI numbers. 279// 280// nx_syscalls_x86_64.nx remains the dedicated x86_64-only mirror 281// for files that want explicit single-target imports (e.g., bench 282// smokes built only for x86_64). This block makes nx_syscalls.nx 283// itself dual-target so substrate primitives compile portably. 284 285@ifdef TARGET_X86_64 286const SYS_READ: i64 = 0 287const SYS_WRITE: i64 = 1 288const SYS_CLOSE: i64 = 3 289const SYS_LSEEK: i64 = 8 290const SYS_OPENAT: i64 = 257 291const SYS_EXIT: i64 = 60 292const SYS_MMAP: i64 = 9 293const SYS_CLOCK_GETTIME: i64 = 228 294const SYS_IOCTL: i64 = 16 295const SYS_CLOCK_NANOSLEEP: i64 = 230 296// Namespace/container family, x86 branch (debt 1785528831). Moved here from 297// nx_syscalls_x86_64.nx so ONE module owns the wrapper set -- a TU reaching both 298// modules used to hold every wrapper TWICE, resolved silently by definition ORDER. 299const SYS_CHROOT: i64 = 161 300const SYS_MOUNT: i64 = 165 301const SYS_UNSHARE: i64 = 272 302const SYS_GETUID: i64 = 102 303const SYS_GETGID: i64 = 104 304const SYS_POLL: i64 = 7 305@endif 306 307@ifndef TARGET_X86_64 308const SYS_READ: i64 = 63 309const SYS_WRITE: i64 = 64 310const SYS_CLOSE: i64 = 57 311const SYS_LSEEK: i64 = 62 312const SYS_OPENAT: i64 = 56 313const SYS_EXIT: i64 = 93 314const SYS_MMAP: i64 = 222 315const SYS_CLOCK_GETTIME: i64 = 113 316const SYS_IOCTL: i64 = 29 317const SYS_CLOCK_NANOSLEEP: i64 = 115 318// Namespace/container family, RV64 branch (debt 1785528831). This is the branch actually 319// KEPT (TARGET_X86_64 is hard-pinned undefined), so these are the numbers the x86 backend 320// translates at emit: 51->161 chroot, 40->165 mount, 97->272 unshare, 174->102 getuid, 321// 176->104 getgid. The 40 and 51 rows were added to x86ctx_rv64_to_x86_64_syscall and 322// shipped FIRST -- without them both would pass through to the WRONG x86 syscall 323// (sendfile / getsockname), silently, because that translator's default is `return num`. 324const SYS_CHROOT: i64 = 51 325const SYS_MOUNT: i64 = 40 326const SYS_UNSHARE: i64 = 97 327const SYS_GETUID: i64 = 174 328const SYS_GETGID: i64 = 176 329const SYS_POLL: i64 = 73 330@endif 331 332func sys_ioctl(fd: i64, request: i64, arg: i64) -> i64 { 333 return __syscall(SYS_IOCTL, fd, request, arg, 0, 0, 0) 334} 335 336// poll(2): wait for events on fds. fds points to an array of `nfds` 337// struct pollfd { i32 fd; i16 events; i16 revents } (8 bytes each). 338// timeout_ms < 0 = block forever, 0 = return immediately. Returns the 339// count of ready fds (>0), 0 on timeout, or -errno. Used by the 340// substrate's own network diagnostics (bounded non-blocking connect) 341// instead of reaching for external tools. (rv64 const = ppoll; this 342// wrapper only runs on the x86_64 target.) 343func sys_poll(fds: *u8, nfds: i64, timeout_ms: i64) -> i64 { 344 return __syscall(SYS_POLL, fds, nfds, timeout_ms, 0, 0, 0) 345} 346 347// ---- core wrappers ---- 348 349func sys_write(fd: i64, buf: *u8, count: i64) -> i64 { 350 return __syscall(SYS_WRITE, fd, buf, count, 0, 0, 0) 351} 352 353func sys_read(fd: i64, buf: *u8, count: i64) -> i64 { 354 return __syscall(SYS_READ, fd, buf, count, 0, 0, 0) 355} 356 357func sys_close(fd: i64) -> i64 { 358 return __syscall(SYS_CLOSE, fd, 0, 0, 0, 0, 0) 359} 360 361// chdir. The compiler only rv64->x86 translates CONSTANT syscall numbers (x86ctx_emit_syscall: 362// VK_CONST_INT); chdir is absent from that table, so a constant 49 falls through to x86_64 bind and a 363// constant 80 is mapped to fstat -- BOTH gave EBADF (PROBE-PROVEN by test_chdir). The documented escape 364// (nx_x86_64_ctx.nx:1004 "Runtime-computed syscall number -- load as-is") is to make op0 RUNTIME: a memory 365// load can't be folded to VK_CONST_INT, so the raw x86_64 number 80 passes through untranslated = real 366// chdir. Used by the supervisor to set a spawned daemon's CWD before execve. 0 on success, -errno on fail. 367func sys_chdir(path: *u8) -> i64 { 368 let nbox: *i64 = sys_mmap(16) as *i64 369 nbox[0] = 80 // x86_64 chdir, forced runtime so the rv64->x86 xlate is skipped 370 return __syscall(nbox[0], path as i64, 0, 0, 0, 0, 0) 371} 372 373// getcwd -- SAME runtime-number escape as sys_chdir directly above, for the same documented reason: the 374// rv64->x86 translator only rewrites CONSTANT syscall numbers, and getcwd is absent from that table, so a 375// constant would be mangled exactly as chdir's was. A memory load cannot be folded to VK_CONST_INT, so the 376// raw x86_64 number passes through untranslated. 377// WHY THIS EXISTS (2026-08-14): the shim had sys_chdir but NOTHING to ask where we are. Every organ that 378// resolves a path against the CWD could therefore only print a RELATIVE path -- a claim whose truth depends 379// on invisible state. Three separate working-directory faults in one session stayed invisible until they 380// bit, and in each the reader could not tell "the file is missing" from "I am standing somewhere else". 381// ★★★AN ORGAN THAT CANNOT REPORT WHERE IT IS CANNOT WRITE AN HONEST PATH. 382// Returns the byte length written INCLUDING the terminator, or -errno (notably -ERANGE if cap is short). 383// SYS_PATH_MAX is exported so a caller never hand-writes the size: the FIRST consumer of sys_getcwd (this 384// author, minutes after adding it) wrote `sys_mmap(4096)` and `sys_getcwd(buf, 4096)` on consecutive 385// lines -- a bare literal AND a duplicate-authored pair, the exact shape being removed elsewhere the same 386// day. ★★A NEW PRIMITIVE THAT DOES NOT EXPORT ITS OWN SIZE INVITES EVERY CALLER TO INVENT ONE. 387const SYS_PATH_MAX: i64 = 4096 // Linux PATH_MAX; getcwd returns -ERANGE below it 388// The DIRECTORY sibling of MODE_0644, added on the same evidence: `0x1ed` appears at 569 sites in 389// buildroot/runtime (nx_shelltool, corpus_complete=1), i.e. the estate scatters TWO file-mode constants, 390// not one. Named here so the pair lives together and a reader meets both at the same place. 391const MODE_0755: i64 = 0x1ed // rwxr-xr-x : default mode for a created directory 392func sys_getcwd(buf: *u8, cap: i64) -> i64 { 393 let nbox: *i64 = sys_mmap(16) as *i64 394 nbox[0] = 79 // x86_64 getcwd, forced runtime so the rv64->x86 xlate is skipped 395 return __syscall(nbox[0], buf as i64, cap, 0, 0, 0, 0) 396} 397 398// ⚠AT_FDCWD MOVED UP 2026-07-20 -- IT WAS A LIVE MISCOMPILE. This const was declared ~60 lines BELOW 399// (in the openat block) while sys_unlinkat and sys_fchmodat immediately below REFERENCE it. A module 400// const referenced ABOVE its declaration does not resolve, and nx_cc silently substituted CONSTANT 0 401// -- so both wrappers passed dirfd=0 (stdin) instead of -100. Absolute paths survive that (openat 402// ignores dirfd when the path is absolute), RELATIVE paths do not, which is exactly why unlinkat was 403// long recorded as flaky and "passing only by luck". Surfaced by the new unknown-identifier 404// diagnostic, which turned a silent 0 into a compile error. LAW (already banked, now enforced): 405// module-wide consts/statics go ABOVE every possible reader. 406const AT_FDCWD: i64 = -100 407 408// unlinkat(AT_FDCWD, path, 0) -- delete a file. x86_64 263 is a PROVEN pass-through (not an rv64 key), 409// but this is THE canonical home: 5+ organs hand-rolled `__syscall(263,...)` before this landed (DRY, 410// 2026-07-20). 0 on success, -errno on fail. 411func sys_unlinkat(path: *u8) -> i64 { 412 return __syscall(263, AT_FDCWD, path as i64, 0, 0, 0, 0) 413} 414 415// fchmodat(AT_FDCWD, path, mode) -- chmod by path. ⚠a CONSTANT 268 gets rv64->x86 TRANSLATED to the 416// wrong syscall (silent no-op chmod -- cost a vacuous-permission-test debug cycle, 2026-07-20), so the 417// number is forced RUNTIME via the sys_chdir nbox pattern. 0 on success, -errno on fail. 418func sys_fchmodat(path: *u8, mode: i64) -> i64 { 419 let nbox: *i64 = sys_mmap(16) as *i64 420 nbox[0] = 268 // x86_64 fchmodat, forced runtime so the xlate is skipped 421 return __syscall(nbox[0], AT_FDCWD, path as i64, mode, 0, 0, 0) 422} 423 424// exit_group(2) -- terminate ALL tasks in the thread group. Raw x86_64 231 425// (231 is NOT an rv64 key in the compiler's swap table, so it passes through 426// untranslated -- the munmap-11 precedent). THE explicit program-exit call 427// once a process holds live nx_thread_pool workers: CLONE_VM tasks are 428// separate PIDs, so plain sys_exit (93 -> x86 60, single task) leaves them 429// running, holding stdout open and wedging any pipeline that waits for EOF 430// (found 2026-07-07: the shared-pool matmul dispatcher hung the build lane 431// this way). Return-from-main already exit_groups via the _start trampoline; 432// use THIS for explicit early program exit. Per-THREAD exit stays sys_exit 433// (see nx_thread_exit). 434func sys_exit_group(code: i64) -> i64 { 435 return __syscall(231, code, 0, 0, 0, 0, 0) 436} 437 438// setpriority(PRIO_PROCESS=0, who=0 -> SELF, prio) -- x86_64 syscall 141. 439// Lower priority = larger nice value; 19 is the maximum yield. 440// WHY A WRAPPER AND NOT AN OPERATOR STEP (measured 2026-07-30): a bulk media 441// migration walk saturated the NAS; every forked organ queued behind its I/O so 442// EVERY agent MCP call 503'd for minutes -- the control plane went blind while a 443// background job did exactly what it was told. `renice 19` on the running pid 444// restored interactive service at once. 445// LAW: a long-running BULK job must yield to the interactive control plane BY 446// CONSTRUCTION at its own launch, not when an operator notices. Bind it to the 447// one act every bulk job performs (its startup) and nothing has to remember it. 448// WARN: `ionice` does NOT exist on the Synology busybox, so the I/O-class lever 449// is unavailable; CPU nice sufficed because the walk is SHA-256-bound over 450// cached reads (state R, not D, once niced). 451func sys_setpriority(prio: i64) -> i64 { 452 return __syscall(141, 0, 0, prio, 0, 0, 0) 453} 454 455// ADDITIVE TWIN 2026-08-04 (nx_resgov): re-nice ANOTHER process by pid. The incumbent above pins 456// who=0 = "me", so it cannot deprioritise a runaway -- and a governor that can only slow ITSELF has 457// no graceful rung between "observe" and "kill". PRIO_PROCESS=0, who=pid. Existing callers untouched 458// (rule 19: add the new entry point, never re-shape the one in service). 459func sys_setpriority_of(pid: i64, prio: i64) -> i64 { 460 return __syscall(141, 0, pid, prio, 0, 0, 0) 461} 462 463// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 464// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 465// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 466// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 467// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 468// ===== SMALL-ALLOCATION BUMP ARENA (2026-08-06, debt 1785516350 / 1786055008) ===================== 469// MEASURED FIRST, THEN BUILT. nx_arena_probe: 20,000 x sys_mmap(32) -> VmSize 80,172 kB, 470// VmRSS 80,024 kB. 640 KB of requested data cost 78 MB of RESIDENT memory -- 4096 bytes per 32-byte 471// request, exactly one page and one kernel VMA each. Across the corpus nx_mmapbal deep counts 17,157 472// functions / 43,498 sites that allocate and never return, so this multiplier is the actual shape of 473// the leak: the call sites are not individually wrong so much as individually EXPENSIVE. 474// 475// One VMA per call is also a HARD CORRECTNESS CEILING, not just a memory cost: vm.max_map_count 476// defaults to 65530, after which mmap returns -ENOMEM and callers write through the failed pointer. 477// That is precisely the dmesg-proven nx_hostctl SEGFAULT at 0xfffffffffffffff4 described below. 478// 479// SO: requests <= NXA_SMALL_MAX are bump-allocated out of a 256 KiB chunk (one VMA per ~5,400 small 480// allocations instead of one per allocation). Larger requests take the ORIGINAL path untouched -- 481// they are the ones plausibly relying on page alignment, and they are not where the leak lives. 482// 483// THE ZEROING CONTRACT IS LOAD-BEARING AND IS PRESERVED BY NEVER RECYCLING. Callers rely on mmap 484// returning zeroed memory (nx_mmapbal: "mmap zeroes, so an untouched slot reads empty with no init 485// loop"). Bytes handed out here come from a freshly mmapped chunk and are NEVER handed out twice, so 486// every region is zero-filled exactly as before. LIFO give-back on munmap was deliberately REJECTED: 487// it would recover memory but hand back dirty bytes, silently breaking every caller that trusts the 488// zero -- a correctness regression traded for a memory win, which is the wrong trade. 489// 490// KNOWN TRADE-OFF, stated rather than hidden: small allocations are now ADJACENT within a chunk 491// instead of isolated in their own pages. An overrun that today walks off the end of a page and 492// SIGSEGVs loudly may instead corrupt a neighbouring allocation quietly. NXA_GAP puts slack between 493// allocations and NXA_SMALL_MAX is kept deliberately low to bound the exposure, but the risk is real 494// and is the reason this starts at 256 rather than a page. 495// ---- MEMORY ORDERING, THE ONE DEFINITION ------------------------------------------------------- 496// Moved here from nx_atom.nx on 2026-08-25 and DELETED from its two other copies 497// (nx_atomic_intrinsic_test, nx_simd_i32x8_test). Measured before the move, corpus_complete=1: 498// THREE files each declared NX_MO_SEQ_CST = 5 independently. A constant written in three places is 499// three rulers that agree until one of them does not. 500// 501// They live at THIS layer because the arena allocator below needs an ordering value for its own 502// lock, and this file cannot import nx_atom.nx -- nx_atom imports THIS file, so that direction is a 503// cycle. Everything that had these constants still has them: nx_atom.nx imports this file, and so 504// does every consumer of nx_atom. 505// 506// The __atomic_* forms these feed are COMPILER INTRINSICS, not library calls, so this file can use 507// them with no import at all. Verified in nx_x86_64_ctx rather than assumed: __atomic_cas_i64 emits 508// `lock cmpxchgq`, __atomic_faa_i64 emits `lock xaddq`, __atomic_fence emits `mfence`. On x86-64 the 509// ordering operand is not consulted by the emitter because those instructions are full barriers 510// regardless; it is carried for the RV64A backend, where it selects the aq/rl bits. 511const NX_MO_RELAXED: i64 = 0 512const NX_MO_CONSUME: i64 = 1 513const NX_MO_ACQUIRE: i64 = 2 514const NX_MO_RELEASE: i64 = 3 515const NX_MO_ACQ_REL: i64 = 4 516const NX_MO_SEQ_CST: i64 = 5 517 518const NXA_SMALL_MAX: i64 = 256 519const NXA_CHUNK: i64 = 262144 520const NXA_ALIGN: i64 = 16 521const NXA_GAP: i64 = 16 522const NXA_STATE: i64 = 4096 523// RING CANARY (temporary diagnostic): the single-slot canary checked only the immediately 524// previous allocation and reported ZERO overruns -- but the bisection proved the write is 525// DELAYED, landing after later allocations have been served. Track the last NXA_RING 526// allocations and re-verify every one of them on each call. Lives at i64 slot NXA_RBASE in 527// the state page; the reporter borrows bytes 64/128, so 512 is clear of it. 528const NXA_RING: i64 = 128 529const NXA_RBASE: i64 = 64 530// ---- ARENA MARK/RESET (2026-08-12, additive; the durable fix for bump-without-reset). The arena 531// abandons a full chunk on rollover, so a long-running accept loop accumulates chunks into one giant 532// coalesced VMA (hub_gw MEASURED 3.4GB over 64k requests). A daemon marks the arena AFTER startup and 533// resets at its accept-loop's quiescent point; reset munmaps every chunk allocated since the mark and 534// zeroes the marked chunk's reclaimed tail, so per-request small allocations reuse a bounded slab. 535// State slots (state page is 512 i64): [3]=chunk_count [4]=mark_valid [5]=mark_bump [6]=mark_chunk_end 536// [7]=mark_chunk_count; the chunk-base list lives at slots NXA_CHUNKBASE..+NXA_CHUNKMAX (clear of the 537// ring at 64..320 and the reporter scratch below 64). CONTRACT: the caller guarantees NO arena 538// allocation made after the mark is still referenced at reset (the accept-loop top, where the previous 539// request's frames have all returned -- the same quiescent point ss_cache_reap already uses). LARGE 540// (>NXA_SMALL_MAX) allocations take their own VMA and are NOT tracked here; a per-request large mmap 541// still needs its own munmap. Untracked-overflow (>NXA_CHUNKMAX chunks between resets) degrades to the 542// old leak for the excess, never corrupts. 543// ---- ARENA MUTUAL EXCLUSION (2026-08-25) ------------------------------------------------------- 544// THE DEFECT: the bump-pointer advance below was a plain read-modify-write -- 545// let p: i64 = nxa_st[0] 546// nxa_st[0] = p + need 547// -- so two threads that read nxa_st[0] before either wrote it BOTH RECEIVE THE SAME POINTER and 548// then write over each other. The chunk refill, the ring-canary scan and the nxa_st[2] counter have 549// the same shape. MEASURED while shipping structured concurrency: eight pool workers calling a 550// helper that allocates a 16-byte timespec raced this cursor and produced ARENA-OVERRUN 551// prev_alloc_size=16 followed by SIGSEGV. It generalises to EVERY small allocation from more than 552// one thread, which is why the scoped-spawn child body was written to allocate nothing at all. 553// 554// WHY A LOCK AND NOT A LOCK-FREE BUMP. A fetch-and-add on the cursor fixes only the fast path; two 555// threads can still both observe the chunk exhausted and both refill, and the canary ring and the 556// counter would still race. One lock over the whole mutable region is correct by inspection, which 557// on the allocator that every organ in the estate calls is worth more than a clever fast path. 558// THE COST IS NOT THE DOMINANT COST HERE: this function ALREADY walks all NXA_RING canary slots on 559// every allocation, so one uncontended `lock cmpxchgq` is far below the noise of work already done. 560// 561// SLOT 4 IS FREE BY THE LAYOUT ABOVE: [0] cursor, [1] limit, [2] ring counter, [3] chunk count, and 562// the ring starts at NXA_RBASE=64. It is also clear of the byte-64 and byte-128 scratch that 563// nxa_report_overrun formats digits into (slots 8 and 16), which slot 4 (bytes 32-39) does not touch. 564const NXA_LOCK: i64 = 4 565// A BOUND ON AN UNKNOWABLE WAIT, DERIVED RATHER THAN PICKED, AND ITS EXHAUSTION ANNOUNCES. The 566// longest thing the critical section can do is the NXA_RING canary scan plus one mmap, so a spin far 567// beyond that is not contention -- it is a holder that is never coming back. Eight times the ring 568// gives an order of magnitude of headroom over the longest legitimate hold; on reaching it the 569// allocator SAYS SO on stderr once and keeps waiting, because hanging visibly is recoverable and 570// corrupting silently is not, and dying inside the allocator would take down a process that may be 571// merely slow. 572const NXA_LOCK_WARN: i64 = NXA_RING * 8 573// Slot 5: "the contention hint has already been printed by this process". Also free by the layout 574// above and clear of every scratch region. It is a FLAG, not a counter, and it is set through a CAS 575// so the once-ness is itself race-free rather than depending on the lock it reports about. 576const NXA_LOCK_WARNED: i64 = 5 577 578const NXA_CHUNKBASE: i64 = 320 579const NXA_CHUNKMAX: i64 = 192 580 581// [0] = next free byte, [1] = one past the end of the current chunk. A static POINTER to a real 582// mmapped page rather than scalar statics, matching the idiom the corpus already proves; the state 583// page is taken through __syscall directly so this can never recurse into itself. 584static nxa_st: *i64 585 586// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 587// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 588// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 589// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 590// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 591// 592// A small len means the region came from the bump arena above, because sys_mmap routes by the SAME 593// threshold. Unmapping an interior pointer would tear a hole in a chunk still holding other callers' 594// live allocations, so it is a no-op here. Balanced small callers therefore no longer return memory -- 595// but they now cost ~48 bytes instead of 4096, so the arena wins by two orders of magnitude even 596// against code that was already correct. 597// Matching release for sys_mmap_try and other whole kernel mappings. 598// Never pass an arena allocation from sys_mmap: its small pointers may be interior. 599// Preserve the requested mapping length; the kernel applies its page rounding. 600const NXA_MAP_INVALID:i64=0-22 // Linux EINVAL, a protocol value rather than a resource budget. 601func sys_munmap_direct(addr:*u8,len:i64)->i64{ 602 if (addr as i64)<=0||len<=0{return NXA_MAP_INVALID} 603 return __syscall(11,addr as i64,len,0,0,0,0) 604} 605 606func sys_munmap(addr: *u8, len: i64) -> i64 { 607 if len <= NXA_SMALL_MAX { return 0 } 608 return __syscall(11, addr as i64, len, 0, 0, 0, 0) 609} 610 611// Seek within a file. whence: 0=SEEK_SET, 1=SEEK_CUR, 2=SEEK_END. 612// Returns new file offset on success, -errno on failure. 613func sys_lseek(fd: i64, offset: i64, whence: i64) -> i64 { 614 return __syscall(SYS_LSEEK, fd, offset, whence, 0, 0, 0) 615} 616 617// ---- FILESYSTEM SPACE: THE AXIS THE ESTATE DID NOT HAVE (2026-08-28) ----------------------------- 618// WHY THIS IS HERE AND NOT LEFT WHERE IT WAS. On 2026-08-28 a 100%-FULL DISK truncated a sibling seat's 619// MEMORY.md to 0 bytes -- open(path,"w") truncates before it writes, so a full volume does not refuse a 620// write, it DESTROYS the file. Nothing in the estate saw it coming: nx_resmon is "the resource axis 621// nx_health lacks" for MEMORY and SWAP, and a search for the disk primitive returned matches=0 for BOTH 622// sys_statfs and statvfs with corpus_complete=1. nx_res_census records the same absence in its own header. 623// The capability was not missing, it was DARK: nx_system_triage.tr_free_gb has read filesystem space since 624// 2026-06-10, in an _hdl_build organ that is NOT REGISTERED (nx_job_run refuses it as "not an unpinned 625// GREEN tool"), so the one instrument that could have warned was unreachable by any caller. 626// A CAPABILITY THAT EXISTS IN ONE UNREACHABLE ORGAN IS INDISTINGUISHABLE FROM ONE NOBODY BUILT. 627// 628// WHY THE RAW 137 AND NOT A SYS_ CONST. This file's dual-arch blocks are gated on TARGET_X86_64, which is 629// HARD-PINNED UNDEFINED, so the RV64 branch is what compiles and the x86 backend translates each number at 630// emit through x86ctx_rv64_to_x86_64_syscall -- whose default is `return num`. There is NO row for RV64 43 631// (statfs), so a SYS_STATFS=43 const would pass through unmapped to x86_64 43 = ACCEPT: a different 632// syscall, silently, on a path pointer. That is not a hypothesis -- nx_system_triage PROBE-PROVED it on 633// 2026-06-10: "rv64 43 returns -9 through the translation table; 137 raw matches df exactly." So 137 is 634// the MEASURED-CORRECT number for the target we actually emit, and it is named here ONCE instead of 635// sitting as a bare literal at each call site. 636// ⚠NAMED FOLLOW-UP, conflict-checked and deliberately NOT taken here: adding `if num == 43 { return 137 }` 637// to x86ctx_rv64_to_x86_64_syscall would make the arch-correct const work too. Nothing passes 43 as an x86 638// number (43 appears only as a translation TARGET, from RV64 202 accept), so the row is safe -- but it is a 639// COMPILER change that activates only on the next nx_cc self-host rebuild, and the working path needs none. 640// 641// struct statfs (x86_64) as i64 slots: 0 f_type, 1 f_bsize, 2 f_blocks, 3 f_bfree, 4 f_bavail, 5 f_files. 642// f_bavail (not f_bfree) is the honest number for "will my write succeed": it excludes the root reserve, so 643// it reports FULLER than root would see. Wrong in the safe direction, and said out loud rather than implied. 644// ⚠THE IMPRECISION, MEASURED AND NAMED SO NOBODY LATER "FIXES" IT INTO AGREEING WITH df: this permil is 645// NOT df's Use%. df computes Used/(Used+Available), which EXCLUDES the root-reserved blocks from its 646// denominator; this computes (blocks-bavail)/blocks, which counts the reserve as used. VERIFIED against df 647// on 2026-08-28: avail_bytes came back 958449582080, which is EXACTLY df's Available of 935985920 KiB, while 648// the same volume read 113 permil here and 7% there -- both correct, measuring different things. Both reach 649// their maximum at the SAME event (bavail = 0), so a threshold calibrated against THIS metric alarms at the 650// same moment a writer actually hits the wall; it simply sits higher below that. Calibrate thresholds to 651// this definition, and do not import a df-derived number as if it were the same quantity. 652const SYS_STATFS_X86_MEASURED: i64 = 137 653const STATFS_BUF_BYTES: i64 = 144 654const STATFS_I_BSIZE: i64 = 1 655const STATFS_I_BLOCKS: i64 = 2 656const STATFS_I_BAVAIL: i64 = 4 657const STATFS_PERMIL: i64 = 1000 658const STATFS_ERR: i64 = 0 - 1 659 660// raw statfs into a caller-supplied 144-byte buffer. 0 = ok, non-zero = the kernel's negative errno. 661func sys_statfs(path: *u8, buf: *i64) -> i64 { 662 return __syscall(SYS_STATFS_X86_MEASURED, path, buf, 0, 0, 0, 0) 663} 664 665// bytes available to a non-root writer on the filesystem holding `path`; STATFS_ERR if statfs failed. 666func sys_fs_avail_bytes(path: *u8) -> i64 { 667 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 668 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 669 return buf[STATFS_I_BSIZE] * buf[STATFS_I_BAVAIL] 670} 671 672// USED per-mille of the filesystem holding `path`, counted against what a non-root writer can reach: 673// (blocks - bavail) * 1000 / blocks. STATFS_ERR if statfs failed or the volume reports zero blocks -- 674// an UNMEASURABLE volume must never read as 0 permil used, which is the most flattering possible lie. 675func sys_fs_used_permil(path: *u8) -> i64 { 676 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 677 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 678 let blocks: i64 = buf[STATFS_I_BLOCKS] 679 if blocks <= 0 { return STATFS_ERR } 680 let avail: i64 = buf[STATFS_I_BAVAIL] 681 return ((blocks - avail) * STATFS_PERMIL) / blocks 682} 683 684func sys_exit(code: i64) -> i64 { 685 return __syscall(SYS_EXIT, code, 0, 0, 0, 0, 0) 686} 687 688// mmap anonymous R/W memory; returns raw bytes. Fixed flags: 689// PROT_READ|PROT_WRITE = 3, MAP_PRIVATE|MAP_ANONYMOUS = 0x22, fd=-1. 690// FAIL-CLOSED ON A REFUSED MAPPING (2026-08-07). MEASURED: the corpus has 90,817 sys_mmap call sites 691// and SIX of them check the result -- all six in test probes whose response is sys_exit anyway. So 692// 90,811 sites take whatever this returns and write through it. When the kernel refuses, that value is 693// -errno, and the write lands at 0xfffffffffffffff4 (-12, ENOMEM). That is not a hypothetical: dmesg 694// on this host recorded it hourly in nx_web_shard_compact, and 18 times in nx_web_crawl_step. 695// Returning a poisoned pointer to 90,811 unguarded callers is the defect. Dying here is strictly safer 696// than dying there: the process ends either way, but this way there is no memory corruption first and 697// the failure is NAMED instead of arriving as a bare segfault address an operator has to decode. 698// This is the never-brick shape -- fail-safe BY CONSTRUCTION, not by every caller remembering. 699// KNOWN COST, stated: nx_mmap_probe / test_munmap deliberately provoke a refusal to observe it. They 700// now exit here with code 12 rather than printing their own verdict. Six probes lose a diagnostic; 701// 90,811 sites stop corrupting memory. 702// ===== TEMPORARY DIAGNOSTIC -- ARENA OVERRUN CANARY (2026-08-07) ===================================== 703// ⛔DO NOT BLESS A COMPILER BUILT WITH THIS. The canary writes 0xC7 into the NXA_GAP slack that a 704// caller could otherwise legitimately read as zeros, so it changes observable behaviour for any code 705// that reads past its declared size -- which is precisely the code being hunted. 706// PURPOSE: at NXA_SMALL_MAX=256 the compiler produces 14 SPURIOUS type diagnostics (it reports 707// `arg 2 is an INTEGER but the parameter is a POINTER` against a parameter DECLARED `j: *u8`), i.e. 708// something writes past its allocation and corrupts the parser's type table. At threshold 64 the same 709// requests each get a 4096-byte page whose slack absorbs it. Reading the source found nothing: the 710// two obvious suspects (nx_ir.nx:70 sys_mmap(104), nx_parse.nx:868 sys_mmap(256)) are both correctly 711// sized and bounded. So stop reading and MEASURE: stamp each small allocation's gap, verify the 712// PREVIOUS one on the next call, and print the size of whichever allocation was overrun. 713// Writes to fd 2 without allocating -- it borrows scratch inside the arena state page, because a 714// reporter that called sys_mmap would recurse into the thing it is instrumenting. 715// Dump n bytes at src to fd 2, unprintables as '.', using scratch at state+256 (the ring starts at 716// state+512 and the decimal scratch sits at +64/+128, so this cannot collide with either). n is 717// capped by callers at 48 so the buffer stays clear of the ring. 718func nxa_dump_printable(src: i64, n: i64) -> i64 { 719 let o: *u8 = ((nxa_st as i64) + 256) as *u8 720 var i: i64 = 0 721 while i < n { 722 let sp: *u8 = (src + i) as *u8 723 var c: i64 = sp[0] as i64 724 if c < 32 { c = 46 } 725 if c > 126 { c = 46 } 726 o[i] = c as u8 727 i = i + 1 728 } 729 o[n] = 10 as u8 730 sys_write(2, o, n + 1) 731 return 0 732} 733 734// FINGERPRINT (2026-08-12): the size alone + all-zeros byte dump never named the site. The ring already 735// records each allocation's REQUESTED size in counter order, so the recent size SEQUENCE fingerprints the 736// code path that was running when the overrun landed (a distinctive run of sizes is near-unique to a 737// function). Writes to fd 2 borrowing state-page scratch at bytes 320/340 (clear of the ring at byte 512, 738// the reporter decimals at 64/128, and the byte-dump at 256). No allocation -- must not recurse into sys_mmap. 739func nxa_dump_sizes() -> i64 { 740 sys_write(2, " ring_sizes(old->recent): " as *u8, 27) 741 let scr: *u8 = ((nxa_st as i64) + 320) as *u8 742 let out2: *u8 = ((nxa_st as i64) + 340) as *u8 743 let cnt: i64 = nxa_st[2] 744 var start: i64 = cnt - 32 745 if start < 0 { start = 0 } 746 var idx: i64 = start 747 while idx < cnt { 748 let slot: i64 = idx % NXA_RING 749 let szv: i64 = nxa_st[NXA_RBASE + slot * 2 + 1] 750 var m: i64 = szv 751 var k: i64 = 0 752 if m == 0 { scr[0] = 48 as u8; k = 1 } 753 while m > 0 { scr[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 754 var j: i64 = 0 755 while j < k { out2[j] = scr[k - 1 - j]; j = j + 1 } 756 out2[k] = 44 as u8 757 sys_write(2, out2, k + 1) 758 idx = idx + 1 759 } 760 sys_write(2, "\n" as *u8, 1) 761 return 0 762} 763 764func nxa_report_overrun(sz: i64, gs: i64) -> i64 { 765 let msg: *u8 = "ARENA-OVERRUN prev_alloc_size=" as *u8 766 var n: i64 = 0 767 while msg[n] != (0 as u8) { n = n + 1 } 768 sys_write(2, msg, n) 769 let b: *u8 = ((nxa_st as i64) + 64) as *u8 770 let o: *u8 = ((nxa_st as i64) + 128) as *u8 771 var m: i64 = sz 772 var k: i64 = 0 773 if m == 0 { b[0] = 48 as u8; k = 1 } 774 while m > 0 { b[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 775 var i: i64 = 0 776 while i < k { o[i] = b[k - 1 - i]; i = i + 1 } 777 o[k] = 10 as u8 778 sys_write(2, o, k + 1) 779 // The SIZE alone did not name the site (four 80-byte victims, and the two unbounded 80-byte 780 // buffers in nx_parse.nx were sized from their inputs with no effect). So show the DATA: the 781 // victim's own bytes identify the buffer, and the bytes written past its end identify the WRITER. 782 let algn: i64 = (sz + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN 783 let base: i64 = gs - algn 784 var dn: i64 = sz 785 if dn > 48 { dn = 48 } 786 sys_write(2, " own : " as *u8, 8) 787 nxa_dump_printable(base, dn) 788 sys_write(2, " over: " as *u8, 8) 789 nxa_dump_printable(gs, 16) 790 nxa_dump_sizes() 791 return 0 792} 793 794func nxa_die(msg: *u8) -> i64 { 795 var n: i64 = 0 796 while msg[n] != (0 as u8) { n = n + 1 } 797 sys_write(2, msg, n) 798 sys_exit(12) 799 return 0 800} 801 802// Address of the arena lock word. Valid only once nxa_st exists; every caller below has already 803// ensured that, and the state-page creation itself is discussed at the take site. 804func nxa_lock_addr() -> *i64 { 805 return ((nxa_st as i64) + NXA_LOCK * 8) as *i64 806} 807 808// __atomic_cas_i64 returns 1 when it wrote and 0 when it did not, so the spin condition is == 0. 809// It is a COMPILER INTRINSIC, not a call into nx_atom -- that module imports THIS file, so importing 810// it back would be a cycle. Verified in nx_x86_64_ctx rather than assumed: it lowers to a genuine 811// `lock cmpxchgq` followed by sete, which is a full barrier on x86-64 whatever ordering is passed. 812func nxa_lock_take() -> i64 { 813 var spins: i64 = 0 814 while __atomic_cas_i64(nxa_lock_addr(), 0, 1, NX_MO_ACQUIRE) == 0 { 815 spins = spins + 1 816 // Fires EXACTLY ONCE, on equality rather than on exceeding, so a genuinely long wait reports 817 // itself without turning the allocator into a log generator. 818 if spins == NXA_LOCK_WARN { 819 // ONCE PER PROCESS, not once per acquisition. MEASURED 2026-08-25 and this is a 820 // correction to the first cut of this very function: it fired on equality per CALL, and 821 // eight workers contending LEGITIMATELY produced hundreds of identical lines in a single 822 // gate run. A DIAGNOSTIC THAT FIRES CONSTANTLY IS ONE EVERY READER LEARNS TO IGNORE, and 823 // this one writes to the stderr of every organ in the estate. 824 // The threshold was derived from the longest the critical section can run, which bounds 825 // ONE hold and says nothing about QUEUE DEPTH: with N threads waiting, a legitimate wait 826 // is N holds and can exceed any per-section derivation. So this is a NOISE FLOOR for a 827 // hint, never a correctness bound -- it never fails, never delays, and never repeats. 828 // The flag is set through a CAS so the once-ness cannot itself race. 829 let wflag: *i64 = ((nxa_st as i64) + NXA_LOCK_WARNED * 8) as *i64 830 if __atomic_cas_i64(wflag, 0, 1, NX_MO_ACQ_REL) == 1 { 831 let m: *u8 = "ARENA-LOCK: sustained allocator contention seen (reported once per process; a hint, not an error -- allocation proceeds normally).\n" as *u8 832 var mn: i64 = 0 833 while m[mn] != (0 as u8) { mn = mn + 1 } 834 sys_write(2, m, mn) 835 } 836 } 837 } 838 return 0 839} 840 841func nxa_lock_give() -> i64 { 842 // nx_cc refuses a bare intrinsic statement ("computes a value and never uses it") and an atomic 843 // store has no result worth using, so it is bound and discarded -- the same shape nx_atom uses 844 // for exactly this reason. The contract is unchanged: this returns 0 either way. 845 let discarded: i64 = __atomic_store_i64(nxa_lock_addr(), 0, NX_MO_RELEASE) 846 if discarded != 0 { return 0 } 847 return 0 848} 849 850// Optional mapping for request boundaries that must report allocation refusal. 851// Unlike sys_mmap, this never aborts the process and never consumes arena storage. 852// Release successful mappings with sys_munmap_direct, not the arena-aware sys_munmap. 853// A successful reservation can still fail on later physical-memory pressure; callers 854// must not describe virtual address admission as guaranteed resident RAM. 855func sys_mmap_try(size:i64)->*u8 { 856 if size<=0 { return 0 as *u8 } 857 let mapped:i64=__syscall(SYS_MMAP,0,size,3,0x22,-1,0) 858 if mapped<=0 { return 0 as *u8 } 859 return mapped as *u8 860} 861 862func sys_mmap(size: i64) -> *u8 { 863 // Large requests keep the EXACT original behaviour, byte for byte: page-aligned, own VMA. Any 864 // caller that depends on page alignment is allocating at least a page, so the arena cannot reach 865 // it. Every failure path below also falls back to this same call, so an exhausted arena degrades 866 // to the old allocator rather than returning a bad pointer. 867 if size > NXA_SMALL_MAX { 868 let big: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 869 if big <= 0 { nxa_die("FATAL sys_mmap: kernel refused a large mapping (ENOMEM). Refusing to return a poisoned pointer -- a write through it would corrupt memory.\n" as *u8) } 870 return big as *u8 871 } 872 if (nxa_st as i64) == 0 { 873 let s: i64 = __syscall(SYS_MMAP, 0, NXA_STATE, 3, 0x22, -1, 0) 874 if s <= 0 { 875 // arena state page refused -- degrade to the plain allocator, and only die if THAT fails too 876 let f1: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 877 if f1 <= 0 { nxa_die("FATAL sys_mmap: kernel refused the arena state page AND the fallback mapping (ENOMEM).\n" as *u8) } 878 return f1 as *u8 879 } 880 nxa_st = s as *i64 881 } 882 // EVERYTHING FROM HERE TO THE RETURN TOUCHES SHARED STATE: the cursor, the limit, the chunk 883 // table, the canary ring and the ring counter. It is ONE critical section because the refill 884 // decision and the bump that depends on it cannot be separated without reintroducing the race. 885 // The state page itself is created ABOVE this point, unlocked: two threads arriving there 886 // together would each map a page and one would win the static, leaking the other's 4 KiB but 887 // corrupting nothing, and in practice the arena is warm long before any thread is spawned 888 // because spawning one allocates. That residual is NAMED here rather than papered over. 889 nxa_lock_take() 890 var need: i64 = size 891 if need <= 0 { need = 1 } 892 need = (need + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN + NXA_GAP 893 if nxa_st[0] + need > nxa_st[1] { 894 let c: i64 = __syscall(SYS_MMAP, 0, NXA_CHUNK, 3, 0x22, -1, 0) 895 if c <= 0 { 896 // chunk refused -- degrade to the plain allocator, and only die if THAT fails too. 897 // RELEASE FIRST: this is the one path that leaves the critical section early, and a lock 898 // held across a degraded return would wedge every other allocator in the process. 899 nxa_lock_give() 900 let f2: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 901 if f2 <= 0 { nxa_die("FATAL sys_mmap: kernel refused an arena chunk AND the fallback mapping (ENOMEM).\n" as *u8) } 902 return f2 as *u8 903 } 904 nxa_st[0] = c 905 nxa_st[1] = c + NXA_CHUNK 906 // track the chunk base so arena_reset can munmap post-mark chunks (additive; guarded at cap). 907 if nxa_st[3] < NXA_CHUNKMAX { nxa_st[NXA_CHUNKBASE + nxa_st[3]] = c; nxa_st[3] = nxa_st[3] + 1 } 908 } 909 // ---- RING CANARY (temporary diagnostic) ---- 910 var rk: i64 = 0 911 while rk < NXA_RING { 912 let gs0: i64 = nxa_st[NXA_RBASE + rk * 2] 913 if gs0 != 0 { 914 var bi: i64 = 0 915 var bad: i64 = 0 916 while bi < 8 { 917 let bp: *u8 = (gs0 + bi) as *u8 918 if bp[0] != (199 as u8) { bad = 1; bi = 8 } else { bi = bi + 1 } 919 } 920 if bad == 1 { 921 nxa_report_overrun(nxa_st[NXA_RBASE + rk * 2 + 1], gs0) 922 nxa_st[NXA_RBASE + rk * 2] = 0 923 } 924 } 925 rk = rk + 1 926 } 927 let p: i64 = nxa_st[0] 928 nxa_st[0] = p + need 929 let gs: i64 = p + need - NXA_GAP 930 var gj: i64 = 0 931 while gj < NXA_GAP { let q: *u8 = (gs + gj) as *u8; q[0] = 199 as u8; gj = gj + 1 } 932 let slot: i64 = nxa_st[2] % NXA_RING 933 nxa_st[NXA_RBASE + slot * 2] = gs 934 nxa_st[NXA_RBASE + slot * 2 + 1] = size 935 nxa_st[2] = nxa_st[2] + 1 936 // The ONLY other exit from the critical section is the degraded chunk-refill path above, which 937 // releases before it returns. Every shared write is now behind this pair. 938 nxa_lock_give() 939 return p as *u8 940} 941 942// arena_mark: force the arena warm (so a first chunk + state page exist), then record the current 943// position as the reset barrier. Returns 1. A daemon calls this ONCE after startup, before its loop. 944func sys_arena_mark() -> i64 { 945 let warm: *u8 = sys_mmap(1) // ensures nxa_st + chunk[0] exist; the 1 byte is itself arena scratch 946 if (warm as i64) == 0 { return 0 } 947 nxa_st[4] = 1 948 nxa_st[5] = nxa_st[0] 949 nxa_st[6] = nxa_st[1] 950 nxa_st[7] = nxa_st[3] 951 return 1 952} 953 954// arena_reset: reclaim everything allocated since the mark. munmap post-mark chunks, restore the bump 955// to the mark, ZERO the marked chunk's reclaimed tail (preserves the mmap-returns-zeroed contract for 956// recycled bytes), and CLEAR the ring canary (its stamps may point into a just-munmap'd chunk, and a 957// stale deref on the next alloc would SEGV). Returns 1 on reset, 0 if no mark was set. 958func sys_arena_reset() -> i64 { 959 if (nxa_st as i64) == 0 { return 0 } 960 if nxa_st[4] != 1 { return 0 } 961 var i: i64 = nxa_st[7] 962 while i < nxa_st[3] { 963 let cb: i64 = nxa_st[NXA_CHUNKBASE + i] 964 if cb != 0 { __syscall(11, cb, NXA_CHUNK, 0, 0, 0, 0); nxa_st[NXA_CHUNKBASE + i] = 0 } 965 i = i + 1 966 } 967 nxa_st[3] = nxa_st[7] 968 nxa_st[0] = nxa_st[5] 969 nxa_st[1] = nxa_st[6] 970 var z: i64 = nxa_st[0] 971 while z < nxa_st[1] { let q: *u8 = z as *u8; q[0] = 0 as u8; z = z + 1 } 972 var r: i64 = 0 973 while r < NXA_RING * 2 { nxa_st[NXA_RBASE + r] = 0; r = r + 1 } 974 nxa_st[2] = 0 975 return 1 976} 977 978// mmap anonymous SHARED R/W memory -- ONE region that survives fork() so all 979// children see each other's writes (MAP_SHARED|MAP_ANONYMOUS = 0x21). Allocate 980// in the PARENT before fork. Foundation for the fork-per-connection video relay 981// (peers in separate children share the per-room frame table). 982func sys_mmap_shared(size: i64) -> *u8 { 983 let r: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x21, -1, 0) 984 return r as *u8 985} 986 987// madvise(2) -- prefetch/advice hints for mapped ranges. MADV_WILLNEED=3 batches page-ins so a 988// serial fault loop over a cold file-backed mmap becomes parallel disk readahead (the dp-web-pub 989// stage-2 p95 fix, 2026-08-12). RAW x86_64 NUMBER 28 ON PURPOSE (sys_exit_group's raw-231 pattern): 990// the portable rv64/asm-generic number is 233 and x86ctx_rv64_to_x86_64_syscall has no 233 row in 991// the DEPLOYED compiler, so a portable const would emit x86_64 233 = epoll_ctl (the wrong-syscall- 992// not-an-error class; see the setpgid/flock rows). The 233->28 row is staged in nx_x86_64_ctx.nx and 993// activates on the next nx_cc self-host rebuild; flip this to the portable const AFTER that lands. 994// Signature bite-proven by nx_madvise_probe (0 / -12 ENOMEM / -22 EINVAL). Advisory contract: callers 995// may ignore the return value -- a failed hint costs nothing but the cold-read behaviour it hints away. 996func sys_madvise(addr: *u8, len: i64, advice: i64) -> i64 { 997 return __syscall(28, addr, len, advice, 0, 0, 0) 998} 999 1000// openat flavors used by the compiler driver. AT_FDCWD = -100 (declared ABOVE, next to its first 1001// reader -- see the miscompile note there; do NOT move it back down). 1002// O_RDONLY = 0; O_CREAT|O_WRONLY|O_TRUNC = 0x241 on Linux RV64. 1003const O_RDONLY: i64 = 0 1004const O_WRONLY_CT: i64 = 0x241 // O_CREAT | O_WRONLY | O_TRUNC 1005const O_WRONLY_CA: i64 = 0x441 // O_CREAT | O_WRONLY | O_APPEND 1006 1007func sys_openat_rd(path: *u8) -> i64 { 1008 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY, 0, 0, 0) 1009} 1010 1011// O_RDWR|O_CREAT (NO truncate) -- for offset-addressed persistent files like the metrics ring TSDB 1012// (create if missing, then lseek+read/write records in place, never truncating existing history). 1013const O_RDWR_CREATE: i64 = 0x42 1014func sys_openat_rdwr(path: *u8, mode: i64) -> i64 { 1015 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDWR_CREATE, mode, 0, 0) 1016} 1017 1018// ★★★THE FILE MODE IS THE HALF OF THIS INTERFACE THAT WAS NEVER NAMED. The O_ flags above are named 1019// consts in hex WITH a decoding comment; the mode passed beside them is a bare literal at every call 1020// site. MEASURED 2026-08-14 (coverage_complete=1 corpus_complete=1 over 23,053 files): 1021// - 29 organs passed the mode as a bare DECIMAL literal, which no reader decodes as rw-r--r-- 1022// without stopping to convert it. ⚠THE FIRST COUNT PUBLISHED HERE WAS 26: the scan was scoped to 1023// runtime/_hdl_build/ and the SUBDIRECTORY's count was published as the estate figure -- three 1024// more (nx_forge_rag, nx_gpu_export, nx_bvhfk) sat one level up in runtime/. 1025// ★A COUNT INHERITS THE SCOPE OF ITS SCAN, AND THE SCOPE IS THE PART NOBODY PRINTS BESIDE IT. 1026// ⚠The offending call is deliberately NOT spelled out literally in this comment: prose is source 1027// bytes, so writing the pattern here would make every future grep for it match this note; 1028// - 10 MORE each define their OWN private 0644 const (IP_ VR_ VP_ LIVE_ FD_ FP_ WL_ PUB_ REG_ HFF_), 1029// nine written 0x1a4 and one written 420 -- THE SAME CONSTANT IN TWO DIFFERENT BASES. 1030// Ten seats each solved this privately and none put the answer where the next one would look. That is 1031// the duplicate-ruler defect precisely: changing the estate's default artifact mode today means finding 1032// 39 sites in two notations and hoping none was missed. One name, in the shim every organ already 1033// imports, is the entire fix -- and it belongs HERE, beside the flags, not in a 40th private copy. 1034const MODE_0644: i64 = 0x1a4 // rw-r--r-- : default mode for a generated artifact 1035// rwxr-xr-x : default mode for a created DIRECTORY. A directory without the execute bit cannot be 1036// traversed, so MODE_0644 is not merely stricter here -- it is wrong, and the failure surfaces later 1037// as an unopenable path rather than as a refused mkdir. Named beside its sibling so the choice is a 1038// lookup rather than a recollection; the estate otherwise spells this as a raw 0x1ed at every site. 1039const MODE_0755: i64 = 0x1ed 1040// Seconds of ZERO PROGRESS on one socket operation before an accepted connection is abandoned. 1041// A single-threaded accept-loop daemon that loop-reads to Content-Length can be starved FOREVER by one 1042// peer that declares a body it never finishes sending -- a one-request DoS, hostile OR merely buggy. 1043// nx_dos_timeout_scan supervises the class and named 16 daemons carrying no timeout at all; the cure is 1044// sys_set_socket_timeout(cfd, ACCEPT_TMO_S) folded in right after accept. 1045// WHY 30 AND NOT THE 5 THE LOGIN DAEMONS USE: this bound must be wrong in the direction of SERVING, not 1046// of dropping. The attack is an UNBOUNDED wait, so ANY finite bound closes it; a short one additionally 1047// risks aborting a legitimate slow client. 30s of zero progress on a single recv/send is pathological 1048// for every daemon in the class -- including the streaming ones, where data is flowing and the timer 1049// never approaches its bound -- while still converting an infinite starvation into a bounded one. 1050// It is the calibration nx_galx_bridge already uses for an accepted cfd; named here rather than copied 1051// into a 16th private literal, exactly as MODE_0644 above. 1052const ACCEPT_TMO_S: i64 = 30 1053func sys_openat_wr(path: *u8, mode: i64) -> i64 { 1054 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CT, mode, 0, 0) 1055} 1056 1057// Linux O_WRONLY | O_CREAT | O_EXCL. An existing final component, including 1058// a symlink, is a conflict; callers acquire ownership only on success. 1059const O_WRONLY_CREATE_EXCLUSIVE: i64 = 0x1 | 0x40 | 0x80 1060func sys_openat_exclusive(path: *u8, mode: i64) -> i64 { 1061 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CREATE_EXCLUSIVE, mode, 0, 0) 1062} 1063 1064// Linux O_DIRECTORY: require a directory, rather than merely an openable node. 1065const O_DIRECTORY: i64 = 0x10000 1066func sys_openat_directory(path: *u8) -> i64 { 1067 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY | O_DIRECTORY, 0, 0, 0) 1068} 1069 1070// Open path for append (create if missing). Used by append-only 1071// journals such as .race_telemetry.tsv. RV64 syscall numbers; the 1072// x86_64 mirror lives in nx_syscalls_x86_64.nx. 1073func sys_openat_append(path: *u8, mode: i64) -> i64 { 1074 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA, mode, 0, 0) 1075} 1076 1077// Linux open ABI flags: acquire close-on-exec atomically and refuse a final 1078// symlink. Nonblocking also prevents an unexpected FIFO from stalling admission. 1079const O_CLOEXEC: i64 = 0x80000 1080const O_NOFOLLOW: i64 = 0x20000 1081const O_NONBLOCK: i64 = 0x800 1082const MODE_0600: i64 = 0x180 1083func sys_openat_lock(path: *u8) -> i64 { 1084 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK, MODE_0600, 0, 0) 1085} 1086 1087// symlinkat(target, AT_FDCWD, linkpath) -- raw x86_64 266 forced RUNTIME (the chdir escape, same as 1088// readlinkat below). THE atomic-repoint primitive for release management: create releases/current.new -> 1089// sys_renameat over releases/current = an atomic symlink swap (golive/rollback are instant + crash-safe). 1090// 0 on success, -errno (notably -EEXIST=-17 if linkpath exists -- create the .new name, then rename). 1091func sys_symlinkat(target: *u8, linkpath: *u8) -> i64 { 1092 let nbox: *i64 = sys_mmap(16) as *i64 1093 nbox[0] = 266 1094 let r: i64 = __syscall(nbox[0], target as i64, AT_FDCWD, linkpath as i64, 0, 0, 0) 1095 sys_munmap(nbox as *u8, 16) 1096 return r 1097} 1098 1099// readlinkat(AT_FDCWD, path, buf, cap) -- raw x86_64 267 forced RUNTIME (the chdir escape: keep the 1100// number out of the rv64->x86 constant-translate path). Returns link length (NO NUL appended), -errno 1101// on fail. nbox is munmap'd before return: the daemon supervisor calls this hundreds of times PER CYCLE 1102// (exe-identity sweeps), and a leaked page per call is exactly the VSZ-balloon class that broke fork. 1103func sys_readlinkat(path: *u8, buf: *u8, cap: i64) -> i64 { 1104 let nbox: *i64 = sys_mmap(16) as *i64 1105 nbox[0] = 267 1106 let r: i64 = __syscall(nbox[0], AT_FDCWD, path as i64, buf as i64, cap, 0, 0) 1107 sys_munmap(nbox as *u8, 16) 1108 return r 1109} 1110 1111// Atomically replace newpath with oldpath (rename(2) on one filesystem: a concurrent reader sees the 1112// whole old file or the whole new file, never a torn read). The S-class content-publish primitive: 1113// write the new page to a temp file, then sys_renameat(tmp, live) -> hot-swap, NO rm+ln race. 1114// renameat2: rv64=276, x86_64=316, flags=0. The known-good compiler translates most rv64 syscall 1115// numbers to the x86_64 target but its table MISSES 276 -- verified 2026-06-14 via nx_rename_probe: 1116// raw 276 -> -EINVAL (lands on x86_64 `tee`), raw 316 -> renames OK. That silently broke every 1117// cst_write_atomic publish (page.html.new written, never swapped in). Try the x86_64 number first 1118// (works on every x86_64 build incl. known-good); fall back to the rv64 number for native-rv64 or 1119// translating compilers that do map it. flags=0 so renameat2 == renameat semantics. 1120func sys_renameat(oldpath: *u8, newpath: *u8) -> i64 { 1121 let r: i64 = __syscall(316, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 1122 if r == 0 { return 0 } 1123 return __syscall(276, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 1124} 1125 1126// fsync(2): flush file (or directory) data+metadata to stable storage. 1127// PROBE-PROVEN 2026-06-10 (_fsync_probe): rv64 82 is NOT in the compiler's 1128// translation table (lands on x86 rename -> -EFAULT both ways); direct 1129// x86_64 74 passes through raw (the unlinkat-263 precedent) and behaves as 1130// fsync (0 on a valid fd, -9 EBADF on a bad one). Storage commit points 1131// fsync the data files AND their directory around rename(2) so a committed 1132// segment survives power loss, not just process death. 1133func sys_fsync(fd: i64) -> i64 { 1134 return __syscall(74, fd, 0, 0, 0, 0, 0) 1135} 1136 1137// flock(2): BSD-style whole-file ADVISORY lock. rv64 32 -> x86_64 73 via the compiler's 1138// x86ctx_rv64_to_x86_64_syscall table (nx_x86_64_ctx.nx:961, PROVEN LIVE in flock_deploy.log). 1139// op: SYS_LOCK_SH=1 / SYS_LOCK_EX=2 / SYS_LOCK_NB=4 (OR) / SYS_LOCK_UN=8. Returns 0 on success, 1140// -errno on failure. Used by the framed-append durability floor to serialize the write-until- 1141// complete loop so a partial/short write under contention can NEVER misalign a concurrent appender 1142// (O_APPEND single-write atomicity is necessary but not sufficient on every fs -- the lock makes 1143// the whole framed record write atomic against other lockers). Additive: no existing caller in 1144// this file changes. NOTE: nx_flock.nx is a separate organ importing the LEGACY "syscalls.nx" 1145// name; this wrapper lives HERE so organs already on nx_syscalls.nx (e.g. nx_framed_append) get 1146// flock without a second import (double-import rc=6 trap). 1147const SYS_LOCK_SH: i64 = 1 1148const SYS_LOCK_EX: i64 = 2 1149const SYS_LOCK_NB: i64 = 4 1150const SYS_LOCK_UN: i64 = 8 1151func sys_flock(fd: i64, op: i64) -> i64 { 1152 return __syscall(32, fd, op, 0, 0, 0, 0) 1153} 1154 1155// newfstatat(2): stat `path` into a 144-byte x86-64 struct stat at `statbuf`. x86_64 nr 262 is passed 1156// DIRECTLY (the unlinkat-263 / fsync-74 precedent: stat-family rv64 numbers aren't in the compiler's 1157// translation table, so a raw x86_64 number passes through untranslated). Returns 0 on success, <0 1158// (e.g. -2 ENOENT) on error. st_mtim.tv_sec @ offset 88, st_mtim.tv_nsec @ 96 (the freshness channel). 1159func sys_fstatat(path: *u8, statbuf: *u8) -> i64 { 1160 return __syscall(262, AT_FDCWD, path, statbuf, 0, 0, 0) 1161} 1162 1163// utimensat(2): set `path` atime+mtime from `times` (a struct timespec[2] = [atime.sec,atime.nsec, 1164// mtime.sec,mtime.nsec]). x86_64 nr 280 passed DIRECTLY. A sovereign `touch`; also makes freshness 1165// tests deterministic. Returns 0 on success, <0 on error. 1166func sys_utimensat(path: *u8, times: *i64) -> i64 { 1167 return __syscall(280, AT_FDCWD, path, times as i64, 0, 0, 0) 1168} 1169 1170// ---- sovereign host control-plane syscalls (x86_64; single unconditional consts, 1171// per the known-good-compiler @ifdef finding). The Nishi supervisor uses these to 1172// manage the daemon lifecycle WITHOUT any shell (no pkill / mkdir / chmod glue). ---- 1173 1174// COMPILER NOTE: the known-good compiler BAKES whole function bodies by NAME for some syscalls 1175// (proven via emitted .s: a function literally named sys_kill emits number 8, sys_chmod emits 155 1176// -- both wrong, regardless of the const referenced). So these wrappers use NON-baked names 1177// (nx_kill / nx_chmod). sys_mkdir / sys_renameat are not baked, so those keep the sys_ name. 1178 1179// DESIGN: __syscall takes the RV64/generic number; the compiler's x86ctx_rv64_to_x86_64_syscall table 1180// (nx_x86_64_ctx.nx) translates it to the build target. So pass the RV64 number. These four were added 1181// to that sovereign table 2026-06-06 (kill 129->62, mkdirat 34->258, fchmodat 53->268, renameat2 1182// 276->316); x86 kill(62) had collided with rv64 lseek(62), x86 fchmodat(268) with rv64 pivot_root(268). 1183 1184// kill(pid, sig) -- rv64 129 -> x86_64 62. SIGTERM=15 / SIGKILL=9. Host control plane. 1185func nx_kill(pid: i64, sig: i64) -> i64 { return __syscall(129, pid, sig, 0, 0, 0, 0) } 1186 1187// setpgid(pid, pgid) -- put a process in its own PROCESS GROUP so a killer can reach its whole 1188// subtree. nx_kill(0 - pgid, sig) signals every member, not just the one process you forked. 1189// A BOUND THAT ONLY REACHES THE PROCESS YOU FORKED IS NOT A BOUND ON THE WORK IT STARTED. 1190// Per-target const, NOT a bare generic number: x86ctx_rv64_to_x86_64_syscall translates only the 1191// numbers it knows and FALLS THROUGH for the rest. MEASURED on the laptop lane 2026-08-10: a bare 1192// generic 154 reached x86_64 as 154 and returned -38 (ENOSYS), silently -- and a fix built on it 1193// reproduced the original bug exactly. Callers must treat setpgid as BEST-EFFORT. 1194@ifdef TARGET_X86_64 1195const SYS_SETPGID: i64 = 109 1196@endif 1197@ifndef TARGET_X86_64 1198const SYS_SETPGID: i64 = 154 1199@endif 1200func sys_setpgid(pid: i64, pgid: i64) -> i64 { return __syscall(SYS_SETPGID, pid, pgid, 0, 0, 0, 0) } 1201 1202// prlimit64(pid, resource, new_limit, old_limit) -- the Linux RESOURCE-LIMIT primitive = 1203// the Job-Object ActiveProcessLimit / memory-limit analog for the sovereign supervisor (M5). 1204// x86_64 prlimit64 = 302 (PASSED DIRECTLY, the unlinkat-263 / fsync-74 / fstatat-262 1205// precedent: a raw x86_64 number not in the compiler's rv64->x86 swap table passes through 1206// untranslated). NOTE: rv64 prlimit64 IS 261 but x86_64 261 = futimesat -- so the naive 1207// "261 is the same on both" is WRONG (PROBE-PROVEN: 261 returned EFAULT/EINVAL because it 1208// hit futimesat); the build target here is x86_64, so we emit 302 directly. pid=0 => the 1209// calling process (a forked child caps ITSELF before running its payload). new_limit / 1210// old_limit each point at a struct rlimit64 { rlim_cur: i64, rlim_max: i64 } (16 bytes); 1211// pass 0 for old_limit to skip read-back. Returns 0 on success, -errno (e.g. -1 EPERM if 1212// raising a hard limit unprivileged) on failure. NON-baked name (the compiler bakes some 1213// sys_* bodies by name; the nx_ prefix avoids that trap). 1214func nx_prlimit(pid: i64, resource: i64, new_limit: *u8, old_limit: *u8) -> i64 { 1215 return __syscall(302, pid, resource, new_limit as i64, old_limit as i64, 0, 0) 1216} 1217 1218// RLIMIT resource ids (Linux generic; identical rv64/x86_64). RLIMIT_AS = address-space 1219// (virtual memory) cap -- the cleanest userspace-settable "memory budget" for a supervised 1220// job. RLIMIT_CPU = CPU-seconds cap. WNOHANG=1 = wait4 non-blocking liveness poll option. 1221const RLIMIT_CPU: i64 = 0 1222const RLIMIT_AS: i64 = 9 1223const WNOHANG: i64 = 1 1224 1225// mkdirat -- rv64 34 -> x86_64 258. Create a doc-root directory. mode e.g. 0x1ed (0755). 1226func sys_mkdir(path: *u8, mode: i64) -> i64 { return __syscall(34, AT_FDCWD, path, mode, 0, 0, 0) } 1227 1228// fchmodat -- rv64 53 -> x86_64 268. +x a freshly-deployed daemon binary (mode 0x1ed). flags=0. 1229func nx_chmod(path: *u8, mode: i64) -> i64 { return __syscall(53, AT_FDCWD, path, mode, 0, 0, 0) } 1230 1231// setsid -- x86_64 = 112 (not in the rv64->x86 table, so the literal passes through). Detach a forked 1232// process into a NEW session so it survives the SSH/parent close -- sovereign daemonization (no shell setsid). 1233func nx_setsid() -> i64 { return __syscall(112, 0, 0, 0, 0, 0, 0) } 1234 1235// CLOCK_MONOTONIC = 1. ts is 16 bytes {sec: i64, nsec: i64}. 1236// Returns 0 / -errno. 1237func sys_clock_gettime_mono(ts: *i64) -> i64 { 1238 return __syscall(SYS_CLOCK_GETTIME, 1, ts, 0, 0, 0, 0) 1239} 1240 1241// CLOCK_REALTIME = 0 -- wall-clock seconds since the Unix epoch. Use 1242// this (NOT monotonic) for anything that must match calendar time: 1243// X.509 notBefore/notAfter, logs, TLS timestamps. Monotonic returns 1244// time-since-boot, which encodes as ~1970 when (mis)used as an epoch. 1245func sys_clock_gettime_real(ts: *i64) -> i64 { 1246 return __syscall(SYS_CLOCK_GETTIME, 0, ts, 0, 0, 0, 0) 1247} 1248 1249// Wall-clock seconds since the Unix epoch. 1250func sys_now_realtime_sec() -> i64 { 1251 let ts: *i64 = sys_mmap(16) as *i64 1252 sys_clock_gettime_real(ts) 1253 return ts[0] 1254} 1255 1256// Wall-clock milliseconds since the Unix epoch. 1257func sys_now_realtime_ms() -> i64 { 1258 let ts: *i64 = sys_mmap(16) as *i64 1259 sys_clock_gettime_real(ts) 1260 return ts[0] * 1000 + ts[1] / SYS_MAGIC_1000000 1261} 1262 1263// Wall-clock MICROSECONDS since the Unix epoch -- the CROSS-MACHINE stamp. 1264// ★ Use this, never sys_now_us(), for any value one machine writes and ANOTHER machine judges 1265// (fleet beats, lease expiry, telemetry rows). Monotonic counts from each machine's OWN boot, so 1266// subtracting one node's monotonic stamp from another's monotonic now yields the difference of two 1267// unrelated boot epochs -- the remote row then reads as ancient (or future-forged) and a freshness 1268// guard rejects every honest remote node while looking like it is working. 1269func sys_now_realtime_us() -> i64 { 1270 let ts: *i64 = sys_mmap(16) as *i64 1271 sys_clock_gettime_real(ts) 1272 return ts[0] * SYS_MAGIC_1000000 + ts[1] / 1000 1273} 1274 1275// Convenience: monotonic time in milliseconds. Caller does not own 1276// the timespec buffer -- it is mmap'd once per call (cheap; the 1277// underlying syscall already costs more than the page fault). 1278func sys_now_ms() -> i64 { 1279 let ts: *i64 = sys_mmap(16) as *i64 1280 sys_clock_gettime_mono(ts) 1281 let sec_part: i64 = ts[0] * 1000 1282 let nsec_part: i64 = ts[1] / SYS_MAGIC_1000000 1283 return sec_part + nsec_part 1284} 1285 1286// Convenience: monotonic time in microseconds. Used by per-request 1287// elapsed-time tracking in search engines + benches where ms is too 1288// coarse. Same caller-ownership rules as sys_now_ms. 1289func sys_now_us() -> i64 { 1290 let ts: *i64 = sys_mmap(16) as *i64 1291 sys_clock_gettime_mono(ts) 1292 let sec_part: i64 = ts[0] * SYS_MAGIC_1000000 1293 let nsec_part: i64 = ts[1] / 1000 1294 return sec_part + nsec_part 1295} 1296 1297// Alias used by nx_search_onsite_engine etc. Matches `_us` naming 1298// convention. Substrate-canonical name is sys_now_us; this alias 1299// preserves existing call sites without churn. 1300func sys_clock_now_us() -> i64 { 1301 return sys_now_us() 1302} 1303 1304// Read the entire file at `path` into a fresh mmap'd buffer. Returns 1305// a null-terminated *u8 plus writes the byte count to *out_len. On 1306// error (open failure, oversize) returns null and leaves out_len = 0. 1307// Uses a fixed 1 MiB buffer for the first pass; larger sources need a 1308// growth loop. 1309// ---- process control (Linux RV64) ---------------------------- 1310// 1311// Lets NishiLang programs spawn other processes -- prerequisite 1312// for replacing shell scripts (f6_gate.sh) with .nx equivalents. 1313// NishiOS will expose a different process model (capability-based); 1314// these wrappers are the Linux-host compatibility layer. 1315 1316@ifdef TARGET_X86_64 1317const SYS_CLONE: i64 = 56 1318const SYS_EXECVE: i64 = 59 1319const SYS_WAIT4: i64 = 61 1320const SYS_PIPE2: i64 = 293 1321const SYS_DUP3: i64 = 292 1322@endif 1323 1324@ifndef TARGET_X86_64 1325const SYS_CLONE: i64 = 220 1326const SYS_EXECVE: i64 = 221 1327const SYS_WAIT4: i64 = 260 1328const SYS_PIPE2: i64 = 59 1329const SYS_DUP3: i64 = 24 1330@endif 1331 1332// Clone flags (subset). CLONE_VFORK blocks parent until child 1333// exec's or exits, matching fork() semantics closely enough for 1334// our spawn-then-wait patterns. 1335const CLONE_VM: i64 = 0x00000100 1336const CLONE_VFORK: i64 = 0x00004000 1337const SIGCHLD: i64 = 17 1338 1339// Create a child process via Linux clone(). Returns: 1340// > 0 in the parent: child PID 1341// == 0 in the child: child should exec or exit 1342// < 0 on error: -errno 1343// Uses SIGCHLD as the signal that parent receives on child exit 1344// (the libc fork() default); no shared memory or thread flags. 1345// ---- namespace / container family (debt 1785528831) ---------------- 1346// Moved here from nx_syscalls_x86_64.nx so ONE module owns the wrapper set. Their 1347// absence here is why nx_container.nx had to import that module as a SECOND syscall 1348// layer, which put every wrapper in the TU twice and let definition ORDER pick the 1349// winner, silently, until the duplicate-definition guard made it fail closed. 1350func sys_unshare(flags: i64) -> i64 { 1351 return __syscall(SYS_UNSHARE, flags, 0, 0, 0, 0, 0) 1352} 1353func sys_mount(source: *u8, target: *u8, fs_type: *u8, mountflags: i64, data: *u8) -> i64 { 1354 return __syscall(SYS_MOUNT, source, target, fs_type, mountflags, data, 0) 1355} 1356func sys_chroot(path: *u8) -> i64 { 1357 return __syscall(SYS_CHROOT, path, 0, 0, 0, 0, 0) 1358} 1359func sys_getuid() -> i64 { 1360 return __syscall(SYS_GETUID, 0, 0, 0, 0, 0, 0) 1361} 1362func sys_getgid() -> i64 { 1363 return __syscall(SYS_GETGID, 0, 0, 0, 0, 0, 0) 1364} 1365 1366func sys_fork() -> i64 { 1367 return __syscall(SYS_CLONE, SIGCHLD, 0, 0, 0, 0, 0) 1368} 1369 1370// Replace the current process image. `path` is the executable 1371// (absolute or in $PATH if the child first does a fresh clone). 1372// `argv` is a null-terminated array of *u8 (already-marshalled). 1373// `envp` same shape, or null for "inherit parent's env". 1374// Only returns on failure (-errno). 1375// EXEC WITH A CLEAN FD TABLE (seq1785451144). A child inherits every fd its parent held, INCLUDING 1376// listen sockets, across fork AND execve. That is how nx_opaque_login came to hold mgmt s :18098 1377// alongside mgmt itself -- two listeners on one port, connections split between them, a VALID route 1378// answering 404 on some requests. There is no error anywhere in that state, which is why it was 1379// filed as a transport flake for months. 1380// ADDITIVE ON PURPOSE: sys_execve is left byte-identical (910 call sites across 719 files -- a 1381// global change there is unverifiable in one session). Spawners opt in by calling THIS instead. 1382// AUDIT THAT MAKES IT SAFE: zero call sites in the tree dup3 to a target fd above 2, so no exec d 1383// child is deliberately handed a high fd; 0/1/2 are preserved untouched. 1384// Linux child lifetime binding: call in the freshly forked child, before exec. 1385// The expected parent PID is captured before fork, closing the pre-arm death race. 1386// Kernel semantics bind to the creating thread; privileged exec can clear this. 1387const NX_SYS_PRCTL: i64 = 167 1388const NX_PR_SET_PDEATHSIG: i64 = 1 1389const NX_PR_SET_CHILD_SUBREAPER: i64 = 36 1390func sys_prctl(option: i64, arg: i64) -> i64 { 1391 return __syscall(NX_SYS_PRCTL,option,arg,0,0,0,0) 1392} 1393func sys_bind_parent_lifetime(expected_parent: i64, signal: i64) -> i64 { 1394 if expected_parent <= 0 || signal <= 0 { return 0-22 } 1395 let armed: i64=sys_prctl(NX_PR_SET_PDEATHSIG,signal) 1396 if armed < 0 { return armed } 1397 let parent: i64=__syscall(173,0,0,0,0,0,0) 1398 if parent != expected_parent { return 0-10 } 1399 return 0 1400} 1401 1402// Linux waitid observes termination without releasing the child's PID when WNOWAIT is set. 1403// Portable syscall 95 requires the matching x86 backend translation to 247. 1404const SYS_WAITID_PORTABLE: i64 = 95 1405const NX_WAIT_P_PID: i64 = 1 1406const NX_WAIT_EXITED: i64 = 4 1407const NX_WAIT_NOWAIT: i64 = 0x01000000 1408const NX_WAIT_SIGINFO_BYTES: i64 = 128 1409func sys_waitid(idtype: i64, id: i64, info: *u8, options: i64) -> i64 { 1410 return __syscall(SYS_WAITID_PORTABLE,idtype,id,info as i64,options,0,0) 1411} 1412 1413// Post-fork only: the child owns its descriptor table. The buffer bounds a 1414// getdents batch, never the descriptor numbers or number of open handles. 1415const NX_FD_DENT_BUFFER: i64 = 4096 1416const NX_SYS_CLOSE_RANGE: i64 = 436 // Linux x86_64 and asm-generic ABI 1417const NX_FD_UINT_MAX: i64 = 4294967295 1418func sys_close_inherited_proc(first: i64) -> i64 { 1419 let directory: i64=sys_openat_rd("/proc/self/fd") 1420 if directory < 0 { return directory } 1421 let buf: *u8=sys_mmap(NX_FD_DENT_BUFFER) 1422 var result: i64=0 1423 var running: i64=1 1424 while running == 1 { 1425 let n: i64=sys_getdents64(directory,buf,NX_FD_DENT_BUFFER) 1426 if n == (0-4) { continue } 1427 if n <= 0 { result=n; break } 1428 var off: i64=0 1429 while off < n { 1430 if n-off < 20 { result=0-5; running=0; break } 1431 let rec: *u8=buf+off 1432 let size: i64=dirent_reclen(rec) 1433 if size < 20 || size > n-off { result=0-5; running=0; break } 1434 var i: i64=19 1435 var fd: i64=0 1436 var valid: i64=1 1437 while i < size { 1438 let c: i64=rec[i] as i64 1439 if c == 0 { break } 1440 if c < 48 || c > 57 { valid=0; break } 1441 if fd > (2147483647-(c-48))/10 { valid=0; break } 1442 fd=fd*10+c-48; i=i+1 1443 } 1444 if i == 19 || i == size { valid=0 } 1445 if valid == 1 && fd >= first && fd != directory { 1446 // Linux releases the descriptor even when close reports a late 1447 // I/O error; never retry close and risk a reused descriptor. 1448 let closed: i64=sys_close(fd) 1449 if closed < 0 && closed != (0-9) { result=closed; running=0; break } 1450 } 1451 off=off+size 1452 } 1453 } 1454 let closedir: i64=sys_close(directory) 1455 sys_munmap(buf,NX_FD_DENT_BUFFER) 1456 if result == 0 && closedir < 0 { result=closedir } 1457 return result 1458} 1459func sys_close_inherited(first: i64) -> i64 { 1460 if first < 0 { return 0-22 } 1461 let rc: i64=__syscall(NX_SYS_CLOSE_RANGE,first,NX_FD_UINT_MAX,0,0,0,0) 1462 if rc == (0-38) { return sys_close_inherited_proc(first) } 1463 return rc 1464} 1465func sys_execve_clean(path: *u8, argv: *i64, envp: *i64) -> i64 { 1466 let rc: i64=sys_close_inherited(3) 1467 if rc < 0 { return rc } 1468 return sys_execve(path,argv,envp) 1469} 1470 1471func sys_execve(path: *u8, argv: *i64, envp: *i64) -> i64 { 1472 return __syscall(SYS_EXECVE, path, argv, envp, 0, 0, 0) 1473} 1474 1475// Wait for a child to exit. `pid` = -1 waits for ANY child, 1476// otherwise waits for that specific PID. `status` is a caller- 1477// mmapped i64 slot: on exit the low 16 bits carry Linux's w* status 1478// flags (WIFEXITED / WEXITSTATUS). Returns the reaped child's PID 1479// or -errno. 1480func sys_wait4(pid: i64, status: *i64, options: i64) -> i64 { 1481 return __syscall(SYS_WAIT4, pid, status, options, 0, 0, 0) 1482} 1483 1484// Extract exit code from a wait4 status word. Matches the glibc 1485// WEXITSTATUS macro: bits 8-15 of the low 16. 1486func wait_exit_code(status: i64) -> i64 { 1487 return (status >> 8) & 0xFF 1488} 1489 1490// Terminating signal from a wait4 status (0 when the child exited normally). Sibling of 1491// wait_exit_code; RESTORED 2026-07-30 after a stale whole-tree push erased both it and 1492// sys_ignore_sigpipe below, while three files still CALLED them (nx_http_server, nx_sigpipe_gate, 1493// nx_tools_api_serve) -- so the tree could not build until they came back. 1494func wait_term_signal(status: i64) -> i64 { 1495 return status & 0x7f 1496} 1497 1498// THE ONE RULER for "what result code did this process actually produce". Use this, not 1499// wait_exit_code, anywhere the answer becomes a VERDICT. 1500// 1501// WHY IT EXISTS, MEASURED 2026-08-25. wait_exit_code is WEXITSTATUS and is correctly named: 1502// bits 8-15 of the status word. But a child KILLED BY A SIGNAL has no exit status at all, and 1503// those bits are ZERO -- so a SEGFAULTING process is indistinguishable from a clean exit 0 to 1504// every caller that reads only wait_exit_code. Measured live: a gate that SIGSEGV'd mid-run was 1505// served by /api/gate_run as exit_code 0, verdict GREEN. A CRASHED GATE WORE A PASS. 1506// 1507// This is not a new discovery in this estate -- and that is the point. nx_gatekit_lib's 1508// gk_wait_code already carried exactly this rule, with its own measurement recorded (two gates 1509// the 60 s watchdog KILLED journaled `GREEN exit=0 ms=60443`). It was fixed THERE in August and 1510// left unfixed in nx_tool_run, which is the shared exec primitive sitting behind /api/gate_run, 1511// /api/build and 51 other consumers. A LAW APPLIED IN ONE ORGAN AND NOT ITS SIBLING IS HALF A 1512// LAW, AND THE HALF LEFT UNDONE IS THE ONE ON THE PRODUCTION PATH. So the rule now lives HERE, 1513// beside the two accessors it is composed of, and gk_wait_code delegates to it: one ruler. 1514// 1515// Shell convention 128+signal (137 SIGKILL, 139 SIGSEGV) is deliberate: it makes the death both 1516// VISIBLE and NON-ZERO, so every existing caller that branches on rc != 0 sees it with no change. 1517// wait_exit_code is left EXACTLY as it was -- 85 call sites across the corpus (corpus_complete=1) 1518// read it, and silently redefining WEXITSTATUS under them would be the cure being worse. 1519func wait_status_rc(status: i64) -> i64 { 1520 let sig: i64 = wait_term_signal(status) 1521 if sig != 0 { return 128 + sig } 1522 return wait_exit_code(status) 1523} 1524 1525// Ignore SIGPIPE process-wide, so writing to a socket the peer already closed returns -EPIPE 1526// instead of KILLING the process. SIGPIPE default action is TERMINATE, which for a daemon means 1527// every client that walks away mid-response is an outage -- this one call at the listen primitive 1528// is inherited by all 52 consumers of nx_http_server_listen. 1529// rt_sigaction(SIGPIPE, {handler=SIG_IGN}, NULL, 8): syscall 13 on x86-64, which happens to equal 1530// the signal number. SA_RESTORER is deliberately NOT set -- the kernel consults it only when it 1531// DELIVERS a handler frame, and SIG_IGN never delivers one. 1532// PROVEN, not asserted: nx_sigpipe_gate forks a child that writes to a closed pipe and demands 1533// death-by-signal-13 WITHOUT this call and a clean -EPIPE WITH it. 1534// Restore a signal to its DEFAULT disposition. THE INVERSE OF sys_ignore_sigpipe, and it exists 1535// because SIG_IGN is inherited across BOTH fork and execve: a daemon that ignores SIGPIPE hands 1536// that ignore to every child it spawns, FOREVER. That silently corrupted verification -- the 1537// sigpipe gate reported 4/5 RED under /api/gate_run and 5/5 GREEN under a shell, same binary, 1538// same minute, because its DISEASE control (writing to a closed peer must KILL) could not be 1539// observed inside an environment where the kill was already disabled (seq1463). A harness must 1540// not change the state it is verifying; where it must, it has to hand back a clean slate. 1541// ⚠the same inheritance can also produce a FALSE GREEN, which is the far more dangerous half. 1542func sys_default_signal(sig: i64) -> i64 { 1543 let act: *i64 = sys_mmap(64) as *i64 1544 act[0] = 0 1545 act[1] = 0 1546 act[2] = 0 1547 act[3] = 0 1548 return __syscall(13, sig, act as i64, 0, 8, 0, 0) 1549} 1550 1551func sys_ignore_sigpipe() -> i64 { 1552 let act: *i64 = sys_mmap(64) as *i64 1553 act[0] = 1 1554 act[1] = 0 1555 act[2] = 0 1556 act[3] = 0 1557 return __syscall(13, 13, act as i64, 0, 8, 0, 0) 1558} 1559 1560// Create a pipe. `fds` must point at 8+ writable bytes; the kernel 1561// packs BOTH int32 fds into fds[0]: read end = low 32 bits, write end 1562// = HIGH 32 bits (fds[1] is never written -- the old comment claiming 1563// fds[1]=write-end caused a false-pass KAT + a hung gate, 2026-07-16). 1564// Extract: rfd = fds[0] & 0xffffffff; wfd = (fds[0] / 4294967296) & 1565// 0xffffffff. Returns 0 on success, -errno on failure. 1566func sys_pipe2(fds: *i64, flags: i64) -> i64 { 1567 return __syscall(SYS_PIPE2, fds, flags, 0, 0, 0, 0) 1568} 1569 1570// Duplicate `oldfd` onto `newfd`, closing `newfd` first if open. 1571// Used to wire child stdout to a pipe: dup3(pipe_write_end, 1). 1572func sys_dup3(oldfd: i64, newfd: i64, flags: i64) -> i64 { 1573 return __syscall(SYS_DUP3, oldfd, newfd, flags, 0, 0, 0) 1574} 1575 1576// ---- directory listing (Linux RV64 getdents64) --------------- 1577// 1578// Foundation for ls / glob / dir-walk helpers. Linux returns 1579// linux_dirent64 records: 1580// u64 d_ino (inode, ignored here) 1581// s64 d_off (next-record offset) 1582// u16 d_reclen (this record's byte length) 1583// u8 d_type (file type; DT_DIR=4, DT_REG=8, DT_LNK=10) 1584// char d_name[] (null-terminated name, padded so d_reclen 1585// carries us to the next record boundary) 1586// Total struct header: 19 bytes, then name up to d_reclen - 19. 1587 1588@ifdef TARGET_X86_64 1589const SYS_GETDENTS64: i64 = 217 1590@endif 1591@ifndef TARGET_X86_64 1592const SYS_GETDENTS64: i64 = 61 1593@endif 1594 1595const DT_UNKNOWN: i64 = 0 1596const DT_FIFO: i64 = 1 1597const DT_CHR: i64 = 2 1598const DT_DIR: i64 = 4 1599const DT_BLK: i64 = 6 1600const DT_REG: i64 = 8 1601const DT_LNK: i64 = 10 1602const DT_SOCK: i64 = 12 1603 1604// Raw syscall. Returns bytes written on success (0 = end-of-dir), 1605// or -errno on failure. 1606func sys_getdents64(fd: i64, buf: *u8, buf_len: i64) -> i64 { 1607 return __syscall(SYS_GETDENTS64, fd, buf, buf_len, 0, 0, 0) 1608} 1609 1610// Extract fields from a linux_dirent64 record. `rec` points at 1611// the start of the record; fields are at fixed offsets. 1612func dirent_reclen(rec: *u8) -> i64 { 1613 // d_reclen is u16 at offset 16. Read as two bytes little-endian. 1614 let lo: i64 = rec[16] 1615 let hi: i64 = rec[17] 1616 return lo | (hi << 8) 1617} 1618 1619func dirent_type(rec: *u8) -> i64 { 1620 return rec[18] 1621} 1622 1623// Pointer to the null-terminated name inside the record. 1624func dirent_name(rec: *u8) -> *u8 { 1625 let base: i64 = rec as i64 1626 return (base + 19) as *u8 1627} 1628 1629// ---- content-addressed file reader --------------------------- 1630 1631func sys_read_file(path: *u8, out_len: *i64) -> *u8 { 1632 let fd: i64 = sys_openat_rd(path) 1633 if fd < 0 { 1634 *out_len = 0 1635 return 0 as *u8 1636 } 1637 // DEBT-EATEN 2026-07-15: the old fixed 4 GiB cap SILENTLY TRUNCATED bigger files (a 9 GB gguf would 1638 // short-read into plausible-garbage tensors -- the worst failure class). Now the buffer is sized from 1639 // the file itself (lseek END), so ANY size reads fully. Physical pages still allocate on-demand. For 1640 // zero-copy any-size READ-ONLY access prefer sys_map_file (below). 1641 // DEBT-EATEN 2026-08-19 (1787076780): when the size is UNKNOWABLE (lseek END <= 0: /proc files, pipes 1642 // -- AND every empty regular file, which reports 0 just the same) this used to reserve 1643 // SYS_MAGIC_4294967296 of address space per call. Untouched pages were never resident, but the 1644 // mapping WAS: a daemon that read an empty registry every sweep ballooned its VmSize by 4 GiB per 1645 // read (measured: smoke instances at a 4.2 GB base), the leak screens flagged it, and sys_free_file 1646 // could only release what was read. The size-unknowable path now GROWS: start at SYS_READ_GROW_INIT, 1647 // double while the window fills, and hand back an EXACT mapping (total + 16) so sys_free_file 1648 // releases all of it. An empty file costs one small read and a 16-byte arena cell; /proc/stat fits 1649 // the first window; a pipe of any length still reads whole. The known-size path is unchanged. 1650 let fsz: i64 = sys_lseek(fd, 0, 2) 1651 sys_lseek(fd, 0, 0) 1652 var cap: i64 = SYS_READ_GROW_INIT 1653 var grow: i64 = 1 1654 if fsz > 0 { cap = fsz; grow = 0 } 1655 var buf: *u8 = sys_mmap(cap + 16) 1656 var total: i64 = 0 1657 var go: i64 = 1 1658 while go == 1 { 1659 let base: i64 = buf as i64 1660 let tail: *u8 = (base + total) as *u8 1661 let n: i64 = sys_read(fd, tail, cap - total) 1662 if n <= 0 { go = 0 } 1663 if n > 0 { total = total + n } 1664 if total >= cap { 1665 if grow == 0 { go = 0 } else { 1666 // the window filled and the size is unknown: double it, copy, release the old mapping 1667 let ncap: i64 = cap * 2 1668 let nb: *u8 = sys_mmap(ncap + 16) 1669 var ci: i64 = 0 1670 let obase: i64 = buf as i64 1671 let nbase: i64 = nb as i64 1672 while ci < total { let src: *u8 = (obase + ci) as *u8; let dst: *u8 = (nbase + ci) as *u8; dst[0] = src[0]; ci = ci + 1 } 1673 sys_munmap(buf, cap + 16) 1674 buf = nb 1675 cap = ncap 1676 } 1677 } 1678 } 1679 sys_close(fd) 1680 if grow == 1 { 1681 // hand back an EXACT mapping so the paired free releases everything (the doubled window would 1682 // otherwise leave its slack mapped forever -- the address-space leak this change exists to end) 1683 let xb: *u8 = sys_mmap(total + 16) 1684 var xi: i64 = 0 1685 let gbase: i64 = buf as i64 1686 let xbase: i64 = xb as i64 1687 while xi < total { let gsrc: *u8 = (gbase + xi) as *u8; let xdst: *u8 = (xbase + xi) as *u8; xdst[0] = gsrc[0]; xi = xi + 1 } 1688 sys_munmap(buf, cap + 16) 1689 buf = xb 1690 } 1691 // Null-terminate for the lexer. 1692 let bbase: i64 = buf as i64 1693 let term: *u8 = (bbase + total) as *u8 1694 term[0] = 0 1695 *out_len = total 1696 return buf 1697} 1698 1699// PAIRED FREE FOR sys_read_file (2026-08-17). sys_read_file mmaps `cap + 16` where cap is the FILE SIZE 1700// and returns only the pointer -- so any caller that frees it must know the padding, and a caller that 1701// unmaps `len` alone leaks the tail page whenever the file size sits just under a page boundary. 1702// ★A CALLER FORCED TO KNOW ITS ALLOCATOR'S PADDING IS A COUPLING THAT WILL DRIFT -- so the +16 lives 1703// HERE, beside the +16 it mirrors, instead of being retyped at every call site. 1704// Pass the length sys_read_file reported through out_len; this re-derives the mapping from it. 1705// Null-safe by construction: sys_read_file returns 0 on failure, so callers need no extra guard -- 1706// ★A FREE THAT REFUSES NULL IS A FREE NOBODY HAS TO WRAP IN AN IF. 1707// EXACT for every path since 2026-08-19: the size-unknowable fallback (lseek <= 0: /proc, pipes, empty 1708// regular files) now returns a mapping of exactly total + 16, so this releases ALL of it. (It used to 1709// map SYS_MAGIC_4294967296 of address space and release only what was read -- stated then, ended now.) 1710// WHY IT EXISTS: nx_sites_daemon serves /wiki/roadmap by calling sys_read_file PER REQUEST inside a loop 1711// that runs up to NX_SD_MAX_REQ_PER_CONN (64) times per connection and never released it -- an 8,408 B 1712// file became 3 fresh pages and a fresh kernel VMA on every hit, held until the child exited. 1713func sys_free_file(buf: *u8, len: i64) -> i64 { 1714 if (buf as i64) == 0 { return 0 } 1715 if len < 0 { return 0 } 1716 return sys_munmap(buf, len + 16) 1717} 1718 1719// Read-only FILE-BACKED map of the whole file (PROT_READ=1, MAP_PRIVATE=2): any size, zero-copy -- only 1720// touched pages become resident (the lazy-MoE shape: a 9 GB model serves in ~active-set RSS, and load 1721// time is ~0 because nothing is copied). NO NUL pad (a file mapping cannot be extended) -- BINARY 1722// consumers only; text/lexer callers keep sys_read_file. Returns 0 on failure; *out_len = file size. 1723// Read-only by construction (PROT_READ; writes fault -- Rule 26-friendly). 1724func sys_map_file(path: *u8, out_len: *i64) -> *u8 { 1725 *out_len = 0 1726 let fd: i64 = sys_openat_rd(path) 1727 if fd < 0 { return 0 as *u8 } 1728 let fsz: i64 = sys_lseek(fd, 0, 2) 1729 if fsz <= 0 { sys_close(fd); return 0 as *u8 } 1730 let r: i64 = __syscall(SYS_MMAP, 0, fsz, 1, 2, fd, 0) 1731 sys_close(fd) 1732 if r <= 0 { return 0 as *u8 } 1733 *out_len = fsz 1734 return r as *u8 1735} 1736 1737// Sleep for `ms` milliseconds against CLOCK_MONOTONIC (relative). 1738// Returns 0 on success, negative errno on failure. Caller-supplied 1739// budget: ms <= 0 is a no-op; very large values are accepted as-is 1740// (the kernel will saturate to its own clamp). Defined at the bottom 1741// of this file so sys_mmap is in scope (single-pass parser). 1742func sys_sleep_ms(ms: i64) -> i64 { 1743 if ms <= 0 { return 0 } 1744 // struct timespec { sec: i64, nsec: i64 } -- 16 bytes RV64. 1745 let req: *u8 = sys_mmap(16) 1746 let rem: *u8 = sys_mmap(16) 1747 let secs: i64 = ms / 1000 1748 let nsec: i64 = (ms - secs * 1000) * SYS_MAGIC_1000000 // remainder ms -> ns 1749 let req_sec: *i64 = req as *i64 1750 let req_nsec: *i64 = ((req as i64) + 8) as *i64 1751 req_sec[0] = secs 1752 req_nsec[0] = nsec 1753 // clock_nanosleep(CLOCK_MONOTONIC=1, flags=0, req, rem). On EINTR (-4) a signal (e.g. SIGCHLD from a 1754 // reaped child) cut the sleep short and wrote the leftover into rem -- RESUME it, otherwise a caller 1755 // that uses the sleep as a timer (the torrent pool's 2s tick) gets spun into a busy loop by child 1756 // deaths and any tick-based budget collapses to milliseconds. A sleep must sleep its full duration. 1757 var r: i64 = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1758 var guard: i64 = 0 1759 while r == (0 - 4) { 1760 if guard > SYS_MAGIC_100000 { r = 0 } else { 1761 let rs: *i64 = rem as *i64 1762 let rn: *i64 = ((rem as i64) + 8) as *i64 1763 req_sec[0] = rs[0] 1764 req_nsec[0] = rn[0] 1765 r = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1766 guard = guard + 1 1767 } 1768 } 1769 sys_munmap(req, 16); sys_munmap(rem, 16) // FREE the timespec pages -- every call mmap'd 2 pages; in a 1770 // long-running poll loop (the supervisor's 15s tick) that leaked ~8KB/iter until mmap -> -12 -> SEGFAULT. 1771 return r 1772} 1773 1774// ---- sockets (RV64 generic syscall numbers) ---------------------- 1775// 1776// Source uses RV64 numbers; the x86_64 backend's 1777// x86ctx_rv64_to_x86_64_syscall table translates at codegen time. 1778// Numbers from arch/arm64/include/asm/unistd.h (RV64 inherits the 1779// generic ABI). 1780 1781// Socket-family syscall numbers via @ifdef macro -- mirrors the 1782// pattern already used for SYS_READ/WRITE/MMAP/etc. above. Without 1783// this gate, --target x86_64 compiled the RV64 numbers as literals 1784// into the `syscall` instruction (e.g. 198 = sched_setaffinity on 1785// x86_64, not socket) and any daemon using sys_socket() died with 1786// ENOSYS before printing its banner -- caught by the nx_signaling 1787// stone S2 deploy on 2026-05-20 (see [[project-cross-isa-syscall- 1788// unification-gap-2026-05-20]]). 1789@ifdef TARGET_X86_64 1790const SYS_SOCKET: i64 = 41 1791const SYS_BIND: i64 = 49 1792const SYS_LISTEN: i64 = 50 1793const SYS_ACCEPT: i64 = 43 1794const SYS_CONNECT: i64 = 42 1795const SYS_SETSOCKOPT: i64 = 54 1796const SYS_SENDTO: i64 = 44 1797const SYS_RECVFROM: i64 = 45 1798const SYS_SHUTDOWN: i64 = 48 1799@endif 1800 1801@ifndef TARGET_X86_64 1802const SYS_SOCKET: i64 = 198 1803const SYS_BIND: i64 = 200 1804const SYS_LISTEN: i64 = 201 1805const SYS_ACCEPT: i64 = 202 1806const SYS_CONNECT: i64 = 203 1807const SYS_SETSOCKOPT: i64 = 208 1808const SYS_SENDTO: i64 = 206 1809const SYS_RECVFROM: i64 = 207 1810const SYS_SHUTDOWN: i64 = 210 1811@endif 1812 1813// Socket-option constants used by nx_http_server / nx_https_server. 1814const SOL_SOCKET: i64 = 1 1815const SO_REUSEADDR: i64 = 2 1816// Receive/send timeouts (Linux x86_64). optval is a struct timeval 1817// {tv_sec: i64, tv_usec: i64} (16 bytes). Essential on PUBLIC sockets: 1818// without them, a single silent/slow client hangs a blocking read 1819// forever -> trivial DoS on a single-threaded accept loop. 1820const SO_SNDTIMEO: i64 = 21 1821const SO_RCVTIMEO: i64 = 20 1822 1823// setsockopt(2) -- set a socket option. Defined BEFORE its first caller 1824// (sys_set_socket_timeout, below): NishiLang forbids forward references, 1825// so the definition must precede every use. 1826func sys_setsockopt(fd: i64, level: i64, optname: i64, 1827 optval: *u8, optlen: i64) -> i64 { 1828 return __syscall(SYS_SETSOCKOPT, fd, level, optname, optval, optlen, 0) 1829} 1830 1831// Set a receive+send timeout (in whole seconds) on a socket fd. 1832// tv is munmap'd before return (LEAK FIXED 2026-07-16): this is called once per PROBE by the daemon 1833// supervisor (35/cycle forever -> ~800MB VSZ/day) and once per CONNECTION by fork-per-connection daemons. 1834// The unfreed page-per-call ballooned VSZ until heuristic overcommit made fork() return -ENOMEM (the 1835// proven pid=-12 failure class) -- likely the historical VSZ pressure behind the vsz_watchdog. 1836func sys_set_socket_timeout(fd: i64, secs: i64) -> i64 { 1837 let tv: *i64 = (sys_mmap(16)) as *i64 1838 tv[0] = secs // tv_sec 1839 tv[1] = 0 // tv_usec 1840 sys_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, tv as *u8, 16) 1841 sys_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, tv as *u8, 16) 1842 sys_munmap(tv as *u8, 16) 1843 return 0 1844} 1845 1846// alarm(2): deliver SIGALRM after `secs` seconds (0 cancels a pending alarm). No SIGALRM handler is installed, so 1847// the default action TERMINATES the process. Used as a per-request watchdog inside a forked request-child: a 1848// pathologically-slow page can then never hang the child forever (which would leak its buffers + pile up procs). 1849const SYS_ALARM: i64 = 37 1850func sys_alarm(secs: i64) -> i64 { return __syscall(SYS_ALARM, secs, 0, 0, 0, 0, 0) } 1851 1852const AF_INET: i64 = 2 1853const SOCK_STREAM: i64 = 1 1854const SOCK_DGRAM: i64 = 2 1855 1856func sys_socket(domain: i64, sock_type: i64, protocol: i64) -> i64 { 1857 return __syscall(SYS_SOCKET, domain, sock_type, protocol, 0, 0, 0) 1858} 1859// Pack an AF_INET any-address sockaddr_in (16 bytes) for `port` at `addr`. 1860// RESTORED INTO THE OWNER 2026-08-19: this lived in the old full nx_syscalls_x86_64.nx and was the 1861// one wrapper WITH LIVE CALLERS (nx_nishipages_serve, nx_udp) that the 2026-07-31 alias-stub 1862// consolidation dropped -- both lanes sat NAS-unbuildable ("I do not know the name") until the 1863// rebuild-drain surfaced them. Body verbatim from the old file, including its documented 1864// workaround: NO `as u8` casts on the byte stores -- the array-element-store already truncates 1865// when the lvalue is *u8, and casts on this path once tripped a codegen defect. 1866// (The old file's other two uncalled orphans, sys_pivot_root/sys_umount2, were left dead on a 1867// zero-caller full-tree grep -- restoring an uncalled wrapper is inventory, not capability.) 1868func sockaddr_in_init(addr: *u8, port: i64) -> i64 { 1869 addr[0] = 2 // AF_INET low byte 1870 addr[1] = 0 1871 // Port in network byte order (big-endian). 1872 let hi: i64 = (port >> 8) & 0xFF 1873 let lo: i64 = port & 0xFF 1874 addr[2] = hi 1875 addr[3] = lo 1876 addr[4] = 0 1877 addr[5] = 0 1878 addr[6] = 0 1879 addr[7] = 0 1880 addr[8] = 0 1881 addr[9] = 0 1882 addr[10] = 0 1883 addr[11] = 0 1884 addr[12] = 0 1885 addr[13] = 0 1886 addr[14] = 0 1887 addr[15] = 0 1888 return 0 1889} 1890 1891func sys_bind(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1892 return __syscall(SYS_BIND, fd, addr, addr_len, 0, 0, 0) 1893} 1894func sys_listen(fd: i64, backlog: i64) -> i64 { 1895 return __syscall(SYS_LISTEN, fd, backlog, 0, 0, 0, 0) 1896} 1897// accept(2) -- accept the next pending connection on a listening socket. 1898// Single-arg form (kernel ignores NULL addr/addr_len writes). Existing 1899// nx_http_server callers use this signature; the 3-arg form is provided 1900// as sys_accept_with_addr for outliers needing peer address. 1901func sys_accept(fd: i64) -> i64 { 1902 return __syscall(SYS_ACCEPT, fd, 0, 0, 0, 0, 0) 1903} 1904func sys_accept_with_addr(fd: i64, addr: *u8, addr_len: *i64) -> i64 { 1905 return __syscall(SYS_ACCEPT, fd, addr, addr_len, 0, 0, 0) 1906} 1907// shutdown(2) -- half-close a socket. how: 0=RD, 1=WR, 2=RDWR. 1908func sys_shutdown(fd: i64, how: i64) -> i64 { 1909 return __syscall(SYS_SHUTDOWN, fd, how, 0, 0, 0, 0) 1910} 1911func sys_connect(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1912 return __syscall(SYS_CONNECT, fd, addr, addr_len, 0, 0, 0) 1913} 1914func sys_sendto(fd: i64, buf: *u8, n: i64, flags: i64, 1915 dest_addr: *u8, addr_len: i64) -> i64 { 1916 return __syscall(SYS_SENDTO, fd, buf, n, flags, dest_addr, addr_len) 1917} 1918func sys_recvfrom(fd: i64, buf: *u8, n: i64, flags: i64, 1919 src_addr: *u8, addr_len: *i64) -> i64 { 1920 return __syscall(SYS_RECVFROM, fd, buf, n, flags, src_addr, addr_len) 1921} 1922 1923// ---- SCM_RIGHTS DESCRIPTOR PASSING (sendmsg/recvmsg over AF_UNIX) ----------------------------- 1924// ADDED 2026-08-21 for /compare/trafficsafety TS1. Until now sys_sendmsg was ABSENT-PROVEN from the 1925// whole tree (corpus_complete=1), so the mechanism nginx, HAProxy and Envoy all use for hitless 1926// replacement -- MOVING the listening descriptor rather than re-binding it -- could not be written 1927// at all. SO_REUSEPORT co-binding is an ACCEPT-DISTRIBUTION primitive, NOT a handoff primitive: 1928// LWN documents that changing the set of listening sockets on a port drops connections during the 1929// three-way handshake, so co-binding proves two binders and can never prove zero drops. 1930// 1931// EVERY OFFSET BELOW IS MEASURED, NOT RECALLED. They were read out of the platform's own headers 1932// with offsetof/sizeof/CMSG_LEN compiled for x86_64: 1933// msghdr 56 = name 0 | namelen 8 (u32) | iov 16 | iovlen 24 | control 32 | controllen 40 | flags 48 (u32) 1934// iovec 16 = base 0 | len 8 1935// cmsghdr 16 = len 0 (u64) | level 8 (u32) | type 12 (u32), data at 16 1936// CMSG_LEN(4)=20 CMSG_SPACE(4)=24 sendmsg=46 recvmsg=47 socketpair=53 1937// AF_UNIX=1 SOL_SOCKET=1 SCM_RIGHTS=1 MSG_CMSG_CLOEXEC=1073741824 1938// A WRONG LAYOUT HERE DOES NOT FAIL LOUD. The syscall still returns a positive byte count and 1939// simply transfers no descriptor, which is why the gate for this proves the property by passing a 1940// REAL descriptor between two REAL processes and then USING it, never by reading a return code. 1941// x86_64 Linux numbers, DELIBERATELY UNGUARDED, and the reason is a measurement rather than a 1942// preference. The first draft of this block wrapped these three in the same 1943// @ifdef TARGET_X86_64 / @ifndef pair every other syscall number in this file uses. On an x86 build 1944// that made every call ENOSYS, and the probe that caught it printed why: 1945// CONSTS SYS_SENDMSG=211 SYS_RECVMSG=212 SYS_SOCKETPAIR=199 SYS_WRITE=64 1946// N sendmsg PLAIN via the CONST rc=-38 (211 is unassigned on x86_64) 1947// N2 sendmsg PLAIN via the LITERAL rc=1 1948// SYS_WRITE reading 64 is the tell and it is NOT MINE: the file's own original guarded block 1949// resolves to its RV64 branch when the constant is referenced, on a build whose sys_write plainly 1950// works. So a constant inside these guards is not reliably the value the guard appears to select. 1951// !! A GUARD THAT SILENTLY SELECTS THE OTHER TARGET'S NUMBER IS WORSE THAN NO GUARD: the call still 1952// compiles, still returns, and dispatches a DIFFERENT SYSCALL. Syscall 199 on x86_64 is 1953// fremovexattr, which is why socketpair appeared to answer EFAULT for every input including a NULL 1954// vector and an unsupported domain -- varying the ARGUMENTS can never reveal that the NUMBER is 1955// wrong, because every variant was equally wrong. 1956// => RV64 support for these three is an OPEN, NAMED requirement, blocked on that toolchain 1957// behaviour. It is left undone and stated rather than papered over with a guard measured not to 1958// work. The estate already keeps nx_syscalls_x86_64.nx as the explicit single-target mirror for 1959// exactly this class of problem. 1960const SYS_SENDMSG: i64 = 46 1961const SYS_RECVMSG: i64 = 47 1962const SYS_SOCKETPAIR: i64 = 53 1963const SCM_AF_UNIX: i64 = 1 1964const SCM_SOL_SOCKET: i64 = 1 1965const SCM_RIGHTS_TYPE: i64 = 1 1966const SCM_MSG_CMSG_CLOEXEC: i64 = 1073741824 1967const SCM_MSGHDR_BYTES: i64 = 56 1968const SCM_MSGHDR_OFF_IOV: i64 = 16 1969const SCM_MSGHDR_OFF_IOVLEN: i64 = 24 1970const SCM_MSGHDR_OFF_CTRL: i64 = 32 1971const SCM_MSGHDR_OFF_CTRLLEN: i64 = 40 1972const SCM_IOVEC_BYTES: i64 = 16 1973const SCM_IOVEC_OFF_BASE: i64 = 0 1974const SCM_IOVEC_OFF_LEN: i64 = 8 1975const SCM_CMSG_OFF_LEN: i64 = 0 1976const SCM_CMSG_OFF_LEVEL: i64 = 8 1977const SCM_CMSG_OFF_TYPE: i64 = 12 1978const SCM_CMSG_OFF_DATA: i64 = 16 1979const SCM_CMSG_LEN_1FD: i64 = 20 1980const SCM_CMSG_SPACE_1FD: i64 = 24 1981const SCM_IOV_COUNT_ONE: i64 = 1 1982const SCM_U32_BYTES: i64 = 4 1983const SCM_BYTE_RADIX: i64 = 256 1984const SCM_FDPAIR_BYTES: i64 = 8 1985// One real data byte travels with the ancillary data ON PURPOSE: a sendmsg carrying SCM_RIGHTS and 1986// NO ordinary payload is the classic silent no-transfer, and it returns 0 rather than an error. 1987const SCM_PAYLOAD_BYTES: i64 = 1 1988const SCM_PAYLOAD_BYTE: i64 = 70 1989// Distinguishable refusals, each naming WHICH conjunct failed -- a compound assertion that will not 1990// name its failing conjunct is a false-alarm generator. All are negative and all sit far outside the 1991// errno range, so no caller can confuse one with a kernel error or with a valid descriptor. 1992const SCM_ERR_NO_CMSG: i64 = 0 - 901 1993const SCM_ERR_CMSG_LEN: i64 = 0 - 902 1994const SCM_ERR_CMSG_LEVEL: i64 = 0 - 903 1995const SCM_ERR_CMSG_TYPE: i64 = 0 - 904 1996 1997func scm_zero(base: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { base[i] = 0; i = i + 1 } return 0 } 1998func scm_put_i64(base: *u8, off: i64, v: i64) -> i64 { 1999 let p: *i64 = ((base as i64) + off) as *i64 2000 p[0] = v 2001 return 0 2002} 2003func scm_get_i64(base: *u8, off: i64) -> i64 { 2004 let p: *i64 = ((base as i64) + off) as *i64 2005 return p[0] 2006} 2007// The two cmsg header fields and the descriptor slot itself are 4-byte ints, so they are packed and 2008// unpacked byte by byte in little-endian order. Radix arithmetic rather than bit shifts, matching 2009// sockaddr_in_init's documented style on this exact path. 2010func scm_put_u32(base: *u8, off: i64, v: i64) -> i64 { 2011 var i: i64 = 0 2012 var m: i64 = v 2013 while i < SCM_U32_BYTES { 2014 base[off + i] = m % SCM_BYTE_RADIX 2015 m = m / SCM_BYTE_RADIX 2016 i = i + 1 2017 } 2018 return 0 2019} 2020func scm_get_u32(base: *u8, off: i64) -> i64 { 2021 var v: i64 = 0 2022 var mult: i64 = 1 2023 var i: i64 = 0 2024 while i < SCM_U32_BYTES { 2025 v = v + (base[off + i] as i64) * mult 2026 mult = mult * SCM_BYTE_RADIX 2027 i = i + 1 2028 } 2029 return v 2030} 2031 2032func sys_sendmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 2033 return __syscall(SYS_SENDMSG, fd, msg, flags, 0, 0, 0) 2034} 2035func sys_recvmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 2036 return __syscall(SYS_RECVMSG, fd, msg, flags, 0, 0, 0) 2037} 2038// socketpair(2). sv receives TWO 4-byte descriptors, so it is a *u8 read with scm_get_u32 -- a 2039// single *i64 read would splice both descriptors into one number and the second would vanish. 2040// !! THIS NUMBER IS NOT REACHING socketpair, AND THE FIRST DIAGNOSIS OF THAT WAS WRONG. 2041// Measured 2026-08-21: every call returns -14 (EFAULT) -- with a valid pointer, with a NULL vector, 2042// and with an UNSUPPORTED DOMAIN alike. The first reading of that evidence was "the host refuses 2043// this call for every input", and it was REFUTED by measuring the emitted constants instead of the 2044// arguments. TARGET_X86_64 is hard-pinned UNDEFINED in this toolchain (see nx_syscalls_x86_64.nx 2045// and nx_tokenizer.nx), so the @ifndef branch is what compiles and the x86 backend TRANSLATES RV64 2046// syscall numbers at emit time. Under that translation 53 is RV64 fchmodat, whose SECOND argument 2047// is a path pointer -- and SOCK_STREAM==1 as a path pointer is exactly EFAULT, every time, 2048// regardless of the other arguments. 2049// * VARYING THE ARGUMENTS CAN NEVER REVEAL THAT THE SYSCALL NUMBER IS WRONG: every variant is 2050// equally wrong, so a set of controls that all agree reads as a confident finding about the host. 2051// The control that actually discriminated was PRINTING THE CONSTANT the binary emits. 2052// => The likely correct value here is the RV64 number 199, exactly as sendmsg/recvmsg above needed 2053// their own numbers rather than the guarded pair. That is NOT asserted: it is UNTESTED, and this 2054// comment says so rather than shipping a plausible number with a confident sentence. 2055// => NOTHING DEPENDS ON IT. The descriptor-passing lane uses a NAMED AF_UNIX rendezvous 2056// (sys_unix_listen + sys_unix_connect_fd below), which is proven end to end by nx_scm_rights_gate 2057// and is also what nginx, HAProxy and systemd actually use to move a listener between processes. 2058// socketpair was only ever the convenience. 2059func sys_socketpair(domain: i64, sock_type: i64, protocol: i64, sv: *u8) -> i64 { 2060 return __syscall(SYS_SOCKETPAIR, domain, sock_type, protocol, sv, 0, 0) 2061} 2062 2063// Bind+listen a NAMED AF_UNIX stream socket -- the accepting half of the rendezvous whose 2064// connecting half is nx_unix_connect. Returns the listening fd, or a negative errno. 2065// The caller owns the path: unlink it first (a stale node makes bind return EADDRINUSE) and unlink 2066// it after, because an AF_UNIX bind leaves a filesystem entry that outlives the process. 2067const SCM_SUN_PATH_OFF: i64 = 2 // sockaddr_un = [sa_family: u16][sun_path: 108] 2068const SCM_SUN_BYTES: i64 = 110 2069const SCM_SUN_PATH_MAX: i64 = 107 2070func sys_unix_listen(path: *u8, backlog: i64) -> i64 { 2071 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 2072 if fd < 0 { return fd } 2073 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 2074 var i: i64 = 0 2075 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 2076 sa[0] = SCM_AF_UNIX 2077 sa[1] = 0 2078 var p: i64 = 0 2079 while path[p] != (0 as u8) { 2080 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 2081 sa[SCM_SUN_PATH_OFF + p] = path[p] 2082 p = p + 1 2083 } 2084 let br: i64 = sys_bind(fd, sa, SCM_SUN_PATH_OFF + p + 1) 2085 if br < 0 { sys_close(fd); return br } 2086 let lr: i64 = sys_listen(fd, backlog) 2087 if lr < 0 { sys_close(fd); return lr } 2088 return fd 2089} 2090 2091// The CONNECTING half of the same rendezvous. Returns the connected fd or a negative errno. 2092// RESIDUAL NAMED RATHER THAN LEFT SILENT: nx_unix_socket.nx already carries an nx_unix_connect with 2093// this exact body. It is not composed here because that file also defines a main(), so importing it 2094// would inject a second main into every one of the 52 daemons that reach nx_http_server -- a 2095// resolution-by-definition-order hazard this tree has already been bitten by. The primitive belongs 2096// in the shim; the older standalone file should be reduced to a caller of this one, and that is a 2097// separate change to a file with its own consumers rather than something to fold in silently here. 2098func sys_unix_connect_fd(path: *u8) -> i64 { 2099 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 2100 if fd < 0 { return fd } 2101 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 2102 var i: i64 = 0 2103 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 2104 sa[0] = SCM_AF_UNIX 2105 sa[1] = 0 2106 var p: i64 = 0 2107 while path[p] != (0 as u8) { 2108 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 2109 sa[SCM_SUN_PATH_OFF + p] = path[p] 2110 p = p + 1 2111 } 2112 let cr: i64 = sys_connect(fd, sa, SCM_SUN_PATH_OFF + p + 1) 2113 if cr < 0 { sys_close(fd); return cr } 2114 return fd 2115} 2116 2117// Send ONE open descriptor over a connected AF_UNIX socket. Returns the sendmsg result: the number 2118// of ordinary data bytes sent (SCM_PAYLOAD_BYTES on success) or a negative errno. The descriptor 2119// itself is NOT closed here -- both ends legitimately hold it until the sender chooses to let go, 2120// and that overlap is the entire point: there must be no instant at which zero processes hold the 2121// listening socket. 2122func sys_send_fd(sock: i64, fd: i64) -> i64 { 2123 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 2124 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 2125 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 2126 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 2127 scm_zero(msg, SCM_MSGHDR_BYTES) 2128 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 2129 data[0] = SCM_PAYLOAD_BYTE 2130 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 2131 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 2132 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 2133 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 2134 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 2135 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 2136 scm_put_i64(cbuf, SCM_CMSG_OFF_LEN, SCM_CMSG_LEN_1FD) 2137 scm_put_u32(cbuf, SCM_CMSG_OFF_LEVEL, SCM_SOL_SOCKET) 2138 scm_put_u32(cbuf, SCM_CMSG_OFF_TYPE, SCM_RIGHTS_TYPE) 2139 scm_put_u32(cbuf, SCM_CMSG_OFF_DATA, fd) 2140 let r: i64 = sys_sendmsg(sock, msg, 0) 2141 sys_munmap(msg, SCM_MSGHDR_BYTES) 2142 sys_munmap(iov, SCM_IOVEC_BYTES) 2143 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 2144 sys_munmap(data, SCM_PAYLOAD_BYTES) 2145 return r 2146} 2147 2148// Receive ONE descriptor from a connected AF_UNIX socket. Returns the NEW descriptor number in this 2149// process (>= 0), a negative errno from recvmsg, or one of the SCM_ERR_* codes above. 2150// flags: 0, or SCM_MSG_CMSG_CLOEXEC so the arriving descriptor is not leaked into grandchildren -- 2151// the estate has already lost a port for six days to exactly that inheritance (nx_cloexec_gate). 2152// THE VALIDATION IS THE WHOLE POINT. recvmsg happily returns a positive byte count having delivered 2153// no ancillary data at all, so the kernel's REWRITTEN msg_controllen is read back rather than the 2154// value we asked for, and each of the three cmsg header fields is checked separately so a failure 2155// says which one. 2156func sys_recv_fd(sock: i64, flags: i64) -> i64 { 2157 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 2158 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 2159 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 2160 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 2161 scm_zero(msg, SCM_MSGHDR_BYTES) 2162 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 2163 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 2164 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 2165 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 2166 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 2167 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 2168 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 2169 let r: i64 = sys_recvmsg(sock, msg, flags) 2170 var out: i64 = r 2171 if r >= 0 { 2172 out = SCM_ERR_NO_CMSG 2173 if scm_get_i64(msg, SCM_MSGHDR_OFF_CTRLLEN) >= SCM_CMSG_LEN_1FD { 2174 out = SCM_ERR_CMSG_LEN 2175 if scm_get_i64(cbuf, SCM_CMSG_OFF_LEN) == SCM_CMSG_LEN_1FD { 2176 out = SCM_ERR_CMSG_LEVEL 2177 if scm_get_u32(cbuf, SCM_CMSG_OFF_LEVEL) == SCM_SOL_SOCKET { 2178 out = SCM_ERR_CMSG_TYPE 2179 if scm_get_u32(cbuf, SCM_CMSG_OFF_TYPE) == SCM_RIGHTS_TYPE { 2180 out = scm_get_u32(cbuf, SCM_CMSG_OFF_DATA) 2181 } 2182 } 2183 } 2184 } 2185 } 2186 sys_munmap(msg, SCM_MSGHDR_BYTES) 2187 sys_munmap(iov, SCM_IOVEC_BYTES) 2188 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 2189 sys_munmap(data, SCM_PAYLOAD_BYTES) 2190 return out 2191} 2192 2193// Ordinary permission bits only. Special privilege bits are never copied by staging. 2194const NX_FILE_PERMISSION_MASK:i64=511 2195const NX_FILE_DESCRIPTOR_INVALID:i64=0-22 2196func sys_fchmod_fd(fd:i64,mode:i64)->i64{ 2197 if fd<0 || mode<0 || mode>NX_FILE_PERMISSION_MASK {return NX_FILE_DESCRIPTOR_INVALID} 2198 return __syscall(52,fd,mode,0,0,0,0) 2199} 2200// Portable descriptor syscall; consumers below use the x86-64 stat ABI layout. 2201const NX_STAT_X64_BYTES:i64=144 2202const NX_STAT_X64_MODE_OFFSET:i64=24 2203const NX_STAT_X64_BLOCK_OFFSET:i64=56 2204const NX_STAT_X64_SIZE_OFFSET:i64=48 2205const NX_STAT_X64_DEVICE_OFFSET:i64=0 2206const NX_STAT_X64_INODE_OFFSET:i64=8 2207const NX_STAT_TYPE_MASK:i64=61440 2208const NX_STAT_REGULAR_FILE:i64=32768 2209func sys_fstat_fd(fd:i64,stat:*u8)->i64{ 2210 if fd<0 || (stat as i64)==0{return NX_FILE_DESCRIPTOR_INVALID} 2211 return __syscall(80,fd,stat,0,0,0,0) 2212} 2213func sys_stat_permissions(stat:*u8)->i64{ 2214 let mode:i64=(stat[NX_STAT_X64_MODE_OFFSET] as i64)+((stat[NX_STAT_X64_MODE_OFFSET+1] as i64)<<8) 2215 return mode & NX_FILE_PERMISSION_MASK 2216} 2217 2218// nx_buf_dyn.nx -- dynamic growable byte buffer. 2219// 2220// Every formatter / builder / ELF emitter today picks a fixed 2221// upfront cap and bails on overflow. When the cap is wrong the 2222// caller has to recompile or re-mmap. This module gives them 2223// std::vector-style amortized growth: 2x doubling when full. 2224// 2225// API: 2226// buf = nx_bd_new(initial_cap) 2227// nx_bd_byte(buf, b) 2228// nx_bd_bytes(buf, src, n) 2229// nx_bd_u32(buf, v) -- LE 2230// nx_bd_u64(buf, v) -- LE 2231// nx_bd_str(buf, s) -- NUL-terminated, NUL not written 2232// nx_bd_strz(buf, s) -- NUL-terminated, NUL written 2233// nx_bd_clear(buf) -- len=0, keeps capacity 2234// buf.buf, buf.len -- direct access for emit 2235// 2236// Growth: 2x when full, never less than +64. Old bytes are 2237// memcpy'd to the new region; the old region is leaked (we don't 2238// have munmap discipline yet, and arenas reset wholesale anyway). 2239 2240// nx_safety_envelope: 2241// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 2242// sil_target: SIL1 2243// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 2244// verdict: NOT_YET_EVALUATED 2245 2246// syscalls.nx -- alias stub. 2247// 2248// nx_syscalls.nx is the canonical syscall surface: same 15 funcs 2249// this file used to define (sys_write/read/mmap/openat/close/exit/ 2250// brk/lseek/getpid/kill/fork/execve/wait4/clone/ioctl) plus 9 2251// additions for sockets and time (sys_socket/bind/listen/accept/ 2252// connect/clock_gettime_mono/now_ms/sleep_ms/openat_append). 2253// 2254// Consumers using `import "syscalls.nx"` resolve via NishiLang's 2255// textual import splicing with path-dedup (runtime/import.nx I1): 2256// this stub splices nx_syscalls.nx once, and any other file that 2257// also imports "nx_syscalls.nx" directly gets deduped to the same 2258// canonical splice. No duplicate symbols. This fixed the 2259// nxasm_main.nx duplicate-symbol error on first link 2026-05-19. 2260// 2261// Deprecation: when every "syscalls.nx" consumer migrates to the 2262// nx_-prefixed name, delete this stub. bench/nx_import_closure.sh 2263// will catch any straggler before the build ships. 2264 2265 2266 2267 2268struct NxBufDyn { 2269 buf: *u8, 2270 len: i64, 2271 cap: i64, 2272} 2273 2274const NX_BD_BYTES: i64 = 24 2275const NX_BD_MIN_CAP: i64 = 64 2276const NX_BD_MIN_GROW: i64 = 64 2277 2278func nx_bd_new(initial_cap: i64) -> *NxBufDyn { 2279 let raw: *u8 = sys_mmap(NX_BD_BYTES) 2280 let b: *NxBufDyn = raw as *NxBufDyn 2281 var c: i64 = initial_cap 2282 if c < NX_BD_MIN_CAP { c = NX_BD_MIN_CAP } 2283 b.buf = sys_mmap(c) 2284 b.len = 0 2285 b.cap = c 2286 return b 2287} 2288 2289// Ensure room for `extra` more bytes. Doubles cap until it fits. 2290func nx_bd_grow(b: *NxBufDyn, extra: i64) -> i64 { 2291 if b.len + extra <= b.cap { return 0 } 2292 var new_cap: i64 = b.cap * 2 2293 if new_cap < b.len + extra { 2294 new_cap = b.len + extra + NX_BD_MIN_GROW 2295 } 2296 let new_buf: *u8 = sys_mmap(new_cap) 2297 var i: i64 = 0 2298 while i < b.len { 2299 new_buf[i] = b.buf[i] 2300 i = i + 1 2301 } 2302 b.buf = new_buf 2303 b.cap = new_cap 2304 return 0 2305} 2306 2307func nx_bd_byte(b: *NxBufDyn, v: i64) -> i64 { 2308 nx_bd_grow(b, 1) 2309 b.buf[b.len] = v & 0xFF 2310 b.len = b.len + 1 2311 return 0 2312} 2313 2314func nx_bd_bytes(b: *NxBufDyn, src: *u8, n: i64) -> i64 { 2315 nx_bd_grow(b, n) 2316 var i: i64 = 0 2317 while i < n { 2318 b.buf[b.len + i] = src[i] 2319 i = i + 1 2320 } 2321 b.len = b.len + n 2322 return 0 2323} 2324 2325func nx_bd_u16(b: *NxBufDyn, v: i64) -> i64 { 2326 nx_bd_byte(b, v & 0xFF) 2327 return nx_bd_byte(b, (v >> 8) & 0xFF) 2328} 2329 2330func nx_bd_u32(b: *NxBufDyn, v: i64) -> i64 { 2331 nx_bd_byte(b, v & 0xFF) 2332 nx_bd_byte(b, (v >> 8) & 0xFF) 2333 nx_bd_byte(b, (v >> 16) & 0xFF) 2334 return nx_bd_byte(b, (v >> 24) & 0xFF) 2335} 2336 2337func nx_bd_u64(b: *NxBufDyn, v: i64) -> i64 { 2338 var i: i64 = 0 2339 while i < 8 { 2340 nx_bd_byte(b, (v >> (i * 8)) & 0xFF) 2341 i = i + 1 2342 } 2343 return 0 2344} 2345 2346// Append a NUL-terminated string WITHOUT the NUL. 2347func nx_bd_str(b: *NxBufDyn, s: *u8) -> i64 { 2348 var i: i64 = 0 2349 while s[i] != 0 { 2350 nx_bd_byte(b, s[i]) 2351 i = i + 1 2352 } 2353 return 0 2354} 2355 2356// Append a NUL-terminated string WITH the NUL. 2357func nx_bd_strz(b: *NxBufDyn, s: *u8) -> i64 { 2358 var i: i64 = 0 2359 while s[i] != 0 { 2360 nx_bd_byte(b, s[i]) 2361 i = i + 1 2362 } 2363 return nx_bd_byte(b, 0) 2364} 2365 2366func nx_bd_clear(b: *NxBufDyn) -> i64 { 2367 b.len = 0 2368 return 0 2369} 2370 2371// Patch a u32 at a previously-recorded offset (e.g. backpatching 2372// a length field whose value wasn't known at the time of writing). 2373func nx_bd_patch_u32(b: *NxBufDyn, off: i64, v: i64) -> i64 { 2374 if off + 4 > b.len { return -1 } 2375 b.buf[off] = v & 0xFF 2376 b.buf[off + 1] = (v >> 8) & 0xFF 2377 b.buf[off + 2] = (v >> 16) & 0xFF 2378 b.buf[off + 3] = (v >> 24) & 0xFF 2379 return 0 2380} 2381 2382// ---- self-test --------------------------------------------------- 2383 2384 2385// Owned capture storage is separate from legacy arena-backed NxBufDyn. 2386// Caller initializes all fields to zero and retains exclusive ownership. Only 2387// this API may replace buf; descriptors and external source spans must stay live. 2388// max_bytes=0 applies no caller policy ceiling; i64 extent and kernel admission still apply. 2389struct NxBufOwned { buf:*u8, len:i64, cap:i64, } 2390const NX_BO_BYTES:i64=24 2391const NX_BO_I64_MAX:i64=9223372036854775807 2392const NX_BO_INVALID:i64=-100 2393const NX_BO_RANGE:i64=-101 2394const NX_BO_CAPACITY:i64=-102 2395const NX_BO_ALLOCATION:i64=-103 2396const NX_BO_ALIAS:i64=-104 2397const NX_BO_RELEASE:i64=-105 2398func nx_bo_state(b:*NxBufOwned)->i64{ 2399 let bp:i64=b as i64;if bp<=0||bp>NX_BO_I64_MAX-NX_BO_BYTES{return NX_BO_INVALID} 2400 if b.len<0||b.cap<0||b.len>b.cap{return NX_BO_INVALID} 2401 let p:i64=b.buf as i64;if b.cap==0{if p!=0||b.len!=0{return NX_BO_INVALID};return 0} 2402 if p<=0||b.cap>NX_BO_I64_MAX-p{return NX_BO_RANGE} 2403 if bp<p+b.cap&&p<bp+NX_BO_BYTES{return NX_BO_ALIAS};return 0 2404} 2405func nx_bo_append(b:*NxBufOwned,src:*u8,n:i64,max_bytes:i64)->i64{ 2406 let valid:i64=nx_bo_state(b);if valid<0{return valid} 2407 if n<0||max_bytes<0{return NX_BO_INVALID} 2408 if n>NX_BO_I64_MAX-b.len{return NX_BO_RANGE} 2409 let need:i64=b.len+n;if max_bytes>0&&need>max_bytes{return NX_BO_CAPACITY} 2410 if n==0{return 0} 2411 let sp:i64=src as i64;if sp<=0{return NX_BO_INVALID};if n>NX_BO_I64_MAX-sp{return NX_BO_RANGE} 2412 let bp:i64=b as i64;let oldp:i64=b.buf as i64 2413 if sp<bp+NX_BO_BYTES&&bp<sp+n{return NX_BO_ALIAS} 2414 if b.cap>0&&sp<oldp+b.cap&&oldp<sp+n{return NX_BO_ALIAS} 2415 if need<=b.cap{var i:i64=0;while i<n{b.buf[b.len+i]=src[i];i=i+1};b.len=need;return 0} 2416 var next:i64=need 2417 if b.cap>0&&b.cap<=NX_BO_I64_MAX/2{let doubled:i64=b.cap*2;if doubled>next{next=doubled}} 2418 if max_bytes>0&&next>max_bytes{next=max_bytes} 2419 let fresh:*u8=sys_mmap_try(next);if (fresh as i64)<=0{return NX_BO_ALLOCATION} 2420 var i:i64=0;while i<b.len{fresh[i]=b.buf[i];i=i+1} 2421 var j:i64=0;while j<n{fresh[b.len+j]=src[j];j=j+1} 2422 if b.cap>0{let freed:i64=sys_munmap_direct(b.buf,b.cap);if freed<0{sys_munmap_direct(fresh,next);return NX_BO_RELEASE}} 2423 b.buf=fresh;b.len=need;b.cap=next;return 0 2424} 2425// Successful release zeroes descriptor; repeated release is a no-op. 2426// A release refusal retains the descriptor so the caller can report/reconcile it. 2427func nx_bo_release(b:*NxBufOwned)->i64{ 2428 let valid:i64=nx_bo_state(b);if valid<0{return valid} 2429 if b.cap==0{return 0} 2430 let rc:i64=sys_munmap_direct(b.buf,b.cap);if rc<0{return NX_BO_RELEASE} 2431 b.buf=0 as *u8;b.len=0;b.cap=0;return 0 2432} 2433 2434 2435// EINTR is -4. sys_read returns -errno, so a NEGATIVE result is an ERROR and r==0 alone is EOF. 2436 2437const TR_ERR_DRAIN: i64 = 0 - 8 2438const TR_ERR_FD_SETUP: i64 = 0 - 12 2439const TR_ERR_PARENT_LIFETIME: i64 = 0 - 11 2440const TR_DRAIN_BYTES: i64 = 4096 2441 2442const TR_EINTR: i64 = 0 - 4 2443// bounded so a genuinely unreadable fd cannot spin forever (same shape as sys_sleep_ms's guard) 2444const TR_EINTR_MAX: i64 = 4096 2445 2446// ---- EXEC FAILURE MUST SPEAK (2026-08-28, 503 lane) ---------------------------------------------- 2447// WHAT THIS DELETES. Both capture primitives below used to end the child with a bare sys_exit(127) 2448// when execve returned, writing NOTHING to the pipe -- and the header above records that as acceptable 2449// (a bad path can only produce a 127 exit + empty capture). But the tools daemon's async lane writes 2450// that empty capture to _jobs/job_<id>.out, and its own receipt tells every caller that an EMPTY 2451// ARTIFACT MEANS THE JOB IS STILL RUNNING. So a tool that never started and a tool still working are 2452// THE SAME OBSERVATION, forever: the caller either waits without bound or concludes the work landed. 2453// A PROCESS THAT DIES BEFORE IT CAN SPEAK IS INDISTINGUISHABLE FROM ONE THAT IS STILL THINKING, AND 2454// THE SILENCE IS READ AS THE MORE FLATTERING OF THE TWO. 2455// MEASURED 2026-08-28 with a control pair: a 130000-byte single argument writes its file and returns 2456// OK, while a 132000-byte one produced an empty artifact and no diagnostic anywhere. The CAUSE is not 2457// ours -- Linux caps ONE argv element at MAX_ARG_STRLEN = 32 pages = 131072 bytes, a PER-ARGUMENT 2458// limit no larger total-argv budget relaxes -- but the SILENCE was ours, and that is what this fixes. 2459// The child is already past dup3 here, so fd 1 IS the capture pipe: the parent drains this text and the 2460// caller reads a named cause instead of nothing. ONE message, TWO call sites, so they cannot drift. 2461const TR_EXIT_EXECFAIL: i64 = 127 2462const TR_MODE_0644: i64 = 420 2463 2464func tr_exec_failed(path: *u8) -> i64 { 2465 let m1: *u8 = "NX-EXEC-FAILED rc=127 path=" as *u8 2466 var n1: i64 = 0 2467 while m1[n1] != (0 as u8) { n1 = n1 + 1 } 2468 sys_write(1, m1, n1) 2469 var pn: i64 = 0 2470 while path[pn] != (0 as u8) { pn = pn + 1 } 2471 sys_write(1, path, pn) 2472 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 2473 var n2: i64 = 0 2474 while m2[n2] != (0 as u8) { n2 = n2 + 1 } 2475 sys_write(1, m2, n2) 2476 sys_exit(TR_EXIT_EXECFAIL) 2477 return TR_EXIT_EXECFAIL 2478} 2479 2480// tr_run_capture: fork -> child wires its stdout(+stderr) to a pipe and execve's `path` with `argv` 2481// (a NUL-terminated *i64 array of *u8-as-i64, argv[0] conventionally = path) -> parent closes the write 2482// end, drains the pipe into out[0..cap), wait4's the child, and returns wait_exit_code (0..255), or a 2483// negative sentinel on a harness failure. On child execve failure the child exits 127 (captured as such). 2484// *outlen (if non-null) receives the number of bytes captured. 2485// 2486// never-brick: path is an absolute ELF chosen by the caller's allowlist; a bad path can only produce a 2487// 127 exit + empty capture, never a shell injection and never a write to persistent hardware state. 2488// RUN A TOOL WHOSE STDOUT IS ITS PRODUCT, NOT ITS COMMENTARY. 2489// 2490// WHY THIS EXISTS BESIDE tr_run_capture RATHER THAN INSTEAD OF IT. Every capture helper in this lib 2491// merges the child's stdout and stderr onto ONE pipe, which is exactly right when the output is a 2492// report to be scanned: a diagnostic and a result belong in the same buffer and the caller reads 2493// both. It is exactly WRONG when stdout carries an ARTIFACT. The sovereign compiler writes assembly 2494// to stdout and progress to stderr, so a caller that captures it merged and writes the buffer to a 2495// .s file produces a file with diagnostics glued to the front -- which the assembler then rejects 2496// with an error about the SOURCE, sending the reader at the compiler instead of at the plumbing. 2497// Measured 2026-09-04: that mistake cost a gate five failing teeth and read exactly like a broken 2498// compiler while the compiler was correct. 2499// 2500// THE ESTATE ALREADY HAD THIS AND COULD NOT REACH IT. `sbr_run` inside nx_sov_build_run does fork + 2501// redirect + execve correctly, but it lives inside a PROGRAM, so every other consumer must either 2502// duplicate it or work around it -- the duplicate-ruler defect in its most common form. This is the 2503// same mechanism lifted into the lib both consumers already import, so there is one of it. 2504// 2505// Contract: stdout -> out_path (created/truncated), stderr -> err_path when non-zero, else the 2506// child's stderr is left on the parent's. Returns the child's exit status, or 128+signal if it died 2507// to one -- because a SEGFAULTED tool whose status decodes as 0 is a silent fake success, which is 2508// the defect the shell convention exists to prevent. Negative returns are the same named plumbing 2509// failures tr_run_capture uses, so a caller can tell "the tool failed" from "I could not run it". 2510func tr_run_redirect(path: *u8, argv: *i64, out_path: *u8, err_path: *u8) -> i64 { 2511 let ofd: i64 = sys_openat_wr(out_path, TR_MODE_0644) 2512 if ofd < 0 { return 0 - 5 } // TR_ERR_OUTOPEN 2513 var efd: i64 = 0 - 1 2514 if (err_path as i64) != 0 { 2515 efd = sys_openat_wr(err_path, TR_MODE_0644) 2516 if efd < 0 { sys_close(ofd); return 0 - 6 } // TR_ERR_ERROPEN 2517 } 2518 let pid: i64 = sys_fork() 2519 if pid < 0 { 2520 sys_close(ofd) 2521 if efd >= 0 { sys_close(efd) } 2522 return 0 - 3 2523 } 2524 if pid == 0 { 2525 sys_dup3(ofd, 1, 0) 2526 if efd >= 0 { sys_dup3(efd, 2, 0) } 2527 sys_close(ofd) 2528 if efd >= 0 { sys_close(efd) } 2529 var fdc: i64 = 3 2530 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 } 2531 let envp: *i64 = sys_mmap(16) as *i64 2532 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64 2533 envp[1] = 0 2534 sys_execve_clean(path, argv, envp) 2535 // execve failed. There is no pipe to name the cause on here, so exit with the shell's 2536 // not-executable status and let the caller's own existence check say which file it was. 2537 sys_exit(127) 2538 return 0 2539 } 2540 sys_close(ofd) 2541 if efd >= 0 { sys_close(efd) } 2542 let stp: *i64 = sys_mmap(16) as *i64 2543 let w: i64 = sys_wait4(pid, stp, 0) 2544 if w < 0 { return 0 - 4 } 2545 return wait_status_rc(stp[0]) 2546} 2547 2548func tr_run_capture(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64) -> i64 { 2549 if (outlen as i64) != 0 { outlen[0] = 0 } 2550 let fds: *i64 = sys_mmap(16) as *i64 2551 if sys_pipe2(fds, 0) != 0 { return 0 - 2 } // TR_ERR_PIPE 2552 // pipe2 writes int[2] (TWO 32-bit fds) into the first 8 bytes -> read end = low 32 bits of fds[0], 2553 // write end = high 32 bits. Reading them as two i64 slots leaves the write fd un-tracked (never closed), 2554 // so the reader never sees EOF and blocks forever. Unpack the 32-bit fds explicitly. 2555 let packed: i64 = fds[0] 2556 let rfd: i64 = packed & 0xFFFFFFFF 2557 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF 2558 2559 let pid: i64 = sys_fork() 2560 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 3 } // TR_ERR_FORK 2561 if pid == 0 { 2562 // ---- CHILD ---- wire stdout(1) + stderr(2) to the pipe write end, close both raw ends, exec. 2563 sys_dup3(wfd, 1, 0) 2564 sys_dup3(wfd, 2, 0) 2565 sys_close(rfd) 2566 sys_close(wfd) 2567 var fdc: i64 = 3 2568 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 } 2569 let envp: *i64 = sys_mmap(16) as *i64 2570 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64 2571 envp[1] = 0 2572 sys_execve_clean(path, argv, envp) 2573 tr_exec_failed(path) // execve failed -> NAMED cause on the pipe, then 127 2574 return 0 2575 } 2576 2577 // ---- PARENT ---- close the write end (so read() sees EOF when the child exits), drain the pipe. 2578 sys_close(wfd) 2579 var total: i64 = 0 2580 var run: i64 = 1 2581 var eintr: i64 = 0 2582 while run == 1 { 2583 if total >= cap { run = 0 } else { 2584 let r: i64 = sys_read(rfd, ((out as i64) + total) as *u8, cap - total) 2585 // r < 0 is an ERROR, NOT EOF. `r <= 0` conflated them: EINTR -- overwhelmingly SIGCHLD from a 2586 // reaped child, which THIS function creates -- ended the drain, returning a SHORT capture that 2587 // outlen then reported as COMPLETE. That is how one organ's output arrives truncated at a 2588 // DIFFERENT point every run and reads as a flaky gate (MEASURED 2026-08-08: 11 runs of one 2589 // nx_coa_gate binary -> 4 different prefixes, ZERO FAIL lines). RESUME on EINTR exactly as 2590 // sys_sleep_ms does for clock_nanosleep, whose comment already names this cause. 2591 if r > 0 { total = total + r } else { 2592 if r == 0 { run = 0 } else { 2593 if r == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 } 2594 } 2595 } 2596 } 2597 } 2598 sys_close(rfd) 2599 if (outlen as i64) != 0 { outlen[0] = total } 2600 2601 let stp: *i64 = sys_mmap(16) as *i64 2602 let w: i64 = sys_wait4(pid, stp, 0) 2603 if w < 0 { return 0 - 4 } // TR_ERR_WAIT 2604 return wait_status_rc(stp[0]) 2605} 2606 2607// ---- BOUNDED EXEC (seq1412) ---------------------------------------------------------------- 2608// tr_run_capture has NO timeout: the parent blocks in the read() drain until EOF. A child that never 2609// exits -- or that forks something holding stdout open -- hangs its caller forever. That primitive has 2610// 51+ call sites including nx_seat (every session boot), nx_gate_rollup, and the tools-daemon exec path 2611// (tea_run), so ONE hanging organ can wedge tools/call for every MCP client. 2612// 2613// ADDITIVE ON PURPOSE: tr_run_capture's signature and behaviour are untouched, so none of those 51 2614// callers change. New/critical callers opt in here. 2615// 2616// Bounded capture shares a deadline-polled drain and an owned process-group 2617// watchdog. Unbounded and promotable capture retain their existing contracts. 2618const TR_ERR_TIMEOUT: i64 = 0 - 5 2619const TR_SIGKILL: i64 = 9 2620 2621// timeout_ms <= 0 -> delegates to the unbounded tr_run_capture (explicit opt-out, never a silent one). 2622// Returns the child's exit code, TR_ERR_TIMEOUT if the deadline fired, or the -2/-3/-4 harness sentinels. 2623func tr_run_capture_to(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64) -> i64 { 2624 return tr_run_capture_tr(path, argv, out, cap, outlen, timeout_ms, 0 as *i64) 2625} 2626 2627// ---- BOUNDED, PROMOTABLE DRAIN (2026-08-22) ----------------------------------------------------- 2628// tr_run_capture_to's header above records that bounding the drain "would need O_NONBLOCK on the read 2629// end, and there is no sys_fcntl in nx_syscalls -- so the non-blocking design is not buildable." That 2630// premise is FALSE, and it cost the estate every synchronous-lane 503: poll(2) needs no O_NONBLOCK, and 2631// sys_poll ships in nx_syscalls with 50 call sites (corpus_complete=1). A retrieval failure was written 2632// into the source as an impossibility, and the watchdog+SIGKILL below it DESTROYED THE ANSWER of every 2633// call that outran the window while the work itself ran on and landed. 2634// ★A LAW RECORDED IN A HEADER IS STILL A HYPOTHESIS -- THIS ONE WAS REFUTED BY ONE GREP. 2635// The promotable implementation remains separate. Bounded and cwd capture now 2636// share tr_run_capture_core; callers keep their existing signatures. 2637// * deadline_ms is a WHOLE-CALL budget, not a per-read idle timer -- the distinction the edge's 2638// SO_RCVTIMEO gets wrong. remaining = deadline_ms - (now - t0), recomputed every pass. 2639// * poll > 0 -> read (POLLIN is set, so it cannot block); the r>0 / r==0 / EINTR discipline is 2640// copied from tr_run_capture_to verbatim, truncation-at-a-different-point bug included. 2641// * poll == 0 -> THE DEADLINE. DO NOT KILL. The caller receives TR_PROMOTE plus the live worker pid and 2642// the live read end, and now owns a worker that will finish and a pipe that will carry its 2643// tail. No watchdog fork exists on this path: one FEWER process per sync call. 2644// * deadline_ms <= 0 -> delegates to the unbounded tr_run_capture -- the explicit opt-out contract 2645// tr_run_capture_to already has, never a silent one. 2646// pollfd is the kernel ABI struct { i32 fd; i16 events; i16 revents } = 8 bytes. 2647// ⚠INCUMBENT NAMED, NOT HIDDEN: nx_ts_drain_lib.nx carries tsd_pollfd_set / tsd_pollfd_ready for this 2648// same struct. It is deliberately NOT imported here: nx_tool_run sits in the closure of 51 consumers 2649// and that lib transitively imports nx_resmon_lib + nx_itoa_lib, so a symbol collision in any ONE of 2650// those closures would break a build nobody in this lane can see. The consolidation is nx_oo_extract 2651// lifting BOTH copies into a tiny nx_pollfd_lib -- that is the named remedy; this is the named debt. 2652const TR_PROMOTE: i64 = 0 - 7 // distinct from TR_ERR_TIMEOUT (-5) and TR_ERR_CHDIR (-6) 2653const TR_POLLFD_BYTES: i64 = 8 // sizeof(struct pollfd) 2654const TR_POLLIN: i64 = 1 // POLLIN 2655const TR_PF_EV_OFF: i64 = 4 // offsetof(struct pollfd, events) 2656const TR_PF_RE_OFF: i64 = 6 // offsetof(struct pollfd, revents) 2657const TR_PF_FD_BYTES: i64 = 4 // sizeof(i32 fd) 2658const TR_BITS_PER_BYTE: i64 = 8 2659const TR_BYTE_MASK: i64 = 0xff 2660func tr_pollfd_set(p: *u8, fd: i64) -> i64 { 2661 var k: i64 = 0 2662 while k < TR_PF_FD_BYTES { p[k] = ((fd >> (k * TR_BITS_PER_BYTE)) & TR_BYTE_MASK) as u8; k = k + 1 } 2663 p[TR_PF_EV_OFF] = TR_POLLIN as u8 2664 p[TR_PF_EV_OFF + 1] = 0 as u8 2665 p[TR_PF_RE_OFF] = 0 as u8 2666 p[TR_PF_RE_OFF + 1] = 0 as u8 2667 return 0 2668} 2669// Returns the child's exit code if it finished inside the deadline, TR_PROMOTE (with *out_pid / *out_rfd 2670// filled) if the deadline fired first, or the -2/-3/-4 harness sentinels. On TR_PROMOTE the caller OWNS 2671// the worker and the read end: it must drain rfd to EOF (the tail of the answer) or close it. 2672func tr_run_capture_deadline(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, deadline_ms: i64, out_pid: *i64, out_rfd: *i64) -> i64 { 2673 if (out_pid as i64) != 0 { out_pid[0] = 0 } 2674 if (out_rfd as i64) != 0 { out_rfd[0] = 0 - 1 } 2675 if deadline_ms <= 0 { return tr_run_capture(path, argv, out, cap, outlen) } 2676 if (outlen as i64) != 0 { outlen[0] = 0 } 2677 let fds: *i64 = sys_mmap(16) as *i64 2678 if sys_pipe2(fds, 0) != 0 { return 0 - 2 } 2679 let packed: i64 = fds[0] 2680 let rfd: i64 = packed & 0xFFFFFFFF 2681 let wfd: i64 = (packed >> 32) & 0xFFFFFFFF 2682 let t0: i64 = sys_now_ms() 2683 let pid: i64 = sys_fork() 2684 if pid < 0 { sys_close(rfd); sys_close(wfd); return 0 - 3 } 2685 if pid == 0 { 2686 sys_default_signal(13) 2687 sys_dup3(wfd, 1, 0) 2688 sys_dup3(wfd, 2, 0) 2689 sys_close(rfd) 2690 sys_close(wfd) 2691 var fdc: i64 = 3 2692 while fdc < 256 { sys_close(fdc); fdc = fdc + 1 } 2693 let envp: *i64 = sys_mmap(16) as *i64 2694 envp[0] = "PATH=/usr/bin:/bin" as *u8 as i64 2695 envp[1] = 0 2696 sys_execve_clean(path, argv, envp) 2697 tr_exec_failed(path) 2698 return 0 2699 } 2700 sys_close(wfd) 2701 let pfd: *u8 = sys_mmap(TR_POLLFD_BYTES) 2702 var total: i64 = 0 2703 var run: i64 = 1 2704 var eintr: i64 = 0 2705 var promoted: i64 = 0 2706 while run == 1 { 2707 if total >= cap { run = 0 } else { 2708 var remaining: i64 = deadline_ms - (sys_now_ms() - t0) 2709 if remaining < 0 { remaining = 0 } 2710 tr_pollfd_set(pfd, rfd) 2711 let pr: i64 = sys_poll(pfd, 1, remaining) 2712 if pr > 0 { 2713 let r: i64 = sys_read(rfd, ((out as i64) + total) as *u8, cap - total) 2714 if r > 0 { total = total + r } else { 2715 if r == 0 { run = 0 } else { 2716 if r == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 } 2717 } 2718 } 2719 } else { 2720 if pr == 0 { promoted = 1; run = 0 } else { 2721 if pr == TR_EINTR { if eintr > TR_EINTR_MAX { run = 0 } else { eintr = eintr + 1 } } else { run = 0 } 2722 } 2723 } 2724 } 2725 } 2726 if (outlen as i64) != 0 { outlen[0] = total } 2727 if promoted == 1 { 2728 if (out_pid as i64) != 0 { out_pid[0] = pid } 2729 if (out_rfd as i64) != 0 { out_rfd[0] = rfd } 2730 return TR_PROMOTE 2731 } 2732 sys_close(rfd) 2733 let stp: *i64 = sys_mmap(16) as *i64 2734 let w: i64 = sys_wait4(pid, stp, 0) 2735 if w < 0 { return 0 - 4 } 2736 return wait_status_rc(stp[0]) 2737} 2738 2739// ---- SANDBOXED RUN (2026-08-06) --------------------------------------------------------------- 2740// ***CONTAINMENT BEATS ENUMERATION.*** On 2026-08-06 a coverage sweep ran nx_cap_grant_e2e_gate -- a 2741// destructive end-to-end test that exercises the REAL capability system in place. It rewrote 2742// nishihost/tool_allowlist.conf with a 52-byte fixture (750 GREEN rows lost) and left 2743// tools_cap_secret.key ABSENT, so nx_tools_api fell back to its forgeable placeholder and EVERY 2744// capability in the estate was denied. A denylist of such gates was written afterwards, and a denylist 2745// is an ENUMERATION -- it only ever protects against the instances somebody already thought of, and its 2746// detector is a source-literal proxy, so it is a FLOOR not a total. 2747// This is the containment: run the child with its OWN cwd, so a gate reaching for ../<production-file> 2748// lands inside a scratch tree instead of the live one. It cannot stop an ABSOLUTE path -- nothing short 2749// of a namespace can -- so it composes with the denylist rather than replacing it. Defence in depth, 2750// stated honestly, because a containment that oversells itself is how the next one gets skipped. 2751// ***FAIL-CLOSED: if the chdir does not take, the child EXITS rather than running in the wrong tree.*** 2752// Running the subject in the directory you were trying to protect is the exact failure this prevents, 2753// so "could not chdir, so proceeded" must never be reachable. 2754// ⚠<path> is resolved AFTER the chdir -- pass it absolute, or relative to <cwd> (e.g. ../_build/x.elf). 2755const TR_ERR_CHDIR: i64 = 0 - 6 2756func tr_run_capture_cwd(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64, cwd: *u8) -> i64 { 2757 if (cwd as i64) == 0 { if (outlen as i64) != 0 { outlen[0]=0 }; return TR_ERR_CHDIR } 2758 return tr_run_capture_core(path,argv,out,cap,outlen,timeout_ms,0 as *i64,cwd) 2759} 2760 2761// Bounded capture retains the caller-sized prefix while draining excess bytes. 2762// FIT/CUT is based on observed output, not whether the buffer filled exactly. 2763const TR_FIT: i64 = 0 2764const TR_CUT: i64 = 1 2765func tr_drain_tr(rfd: i64, out: *u8, cap: i64, trunc: *i64) -> i64 { 2766 if (trunc as i64) != 0 { trunc[0] = TR_FIT } 2767 if cap < 0 { return TR_ERR_DRAIN } 2768 let scratch: *u8 = sys_mmap(TR_DRAIN_BYTES) 2769 var total: i64 = 0 2770 var result: i64 = 0 2771 var run: i64 = 1 2772 var eintr: i64 = 0 2773 while run == 1 { 2774 var dest: *u8 = scratch 2775 var room: i64 = TR_DRAIN_BYTES 2776 if total < cap { dest = ((out as i64)+total) as *u8; room = cap-total } 2777 let r: i64 = sys_read(rfd,dest,room) 2778 if r > 0 { 2779 eintr = 0 2780 if total < cap { total = total+r } else { 2781 if (trunc as i64) != 0 { trunc[0] = TR_CUT } 2782 } 2783 } else { 2784 if r == 0 { run = 0 } else { 2785 if r == TR_EINTR { 2786 eintr = eintr+1 2787 if eintr > TR_EINTR_MAX { result = TR_ERR_DRAIN; run = 0 } 2788 } else { result = TR_ERR_DRAIN; run = 0 } 2789 } 2790 } 2791 } 2792 sys_munmap(scratch,TR_DRAIN_BYTES) 2793 if result < 0 { return result } 2794 return total 2795} 2796 2797// tr_run_capture_tr: bounded capture that REPORTS whether it was cut off. trunc may be null. 2798func tr_run_capture_tr(path: *u8, argv: *i64, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64, trunc: *i64) -> i64 { 2799 return tr_run_capture_core(path,argv,out,cap,outlen,timeout_ms,trunc,0 as *u8) 2800} 2801 2802// Capture owns one private workspace and one shared control page per invocation. 2803// The control page distinguishes a fired deadline from an unrelated SIGKILL. 2804const TR_CONTROL_BYTES: i64 = 4096 2805const TR_ERR_GROUP: i64 = 0 - 9 2806const TR_ERR_ALLOC: i64 = 0 - 10 2807const TR_ECHILD: i64 = 0 - 10 // Linux errno, distinct from the public result namespace 2808func tr_clock_ms(ts: *i64) -> i64 { 2809 if sys_clock_gettime_mono(ts) < 0 { return TR_ERR_DRAIN } 2810 return ts[0]*1000 + ts[1]/SYS_MAGIC_1000000 2811} 2812// Observation keeps the PID reserved until every watchdog capable of signalling it is gone. 2813func tr_observe_exit(pid: i64, info: *u8) -> i64 { 2814 var rc: i64=sys_waitid(NX_WAIT_P_PID,pid,info,NX_WAIT_EXITED | NX_WAIT_NOWAIT) 2815 while rc == TR_EINTR { rc=sys_waitid(NX_WAIT_P_PID,pid,info,NX_WAIT_EXITED | NX_WAIT_NOWAIT) } 2816 return rc 2817} 2818func tr_reap(pid: i64, status: *i64) -> i64 { 2819 var rc: i64=sys_wait4(pid,status,0) 2820 while rc == TR_EINTR { rc=sys_wait4(pid,status,0) } 2821 return rc 2822} 2823// A pipe can stay open after its direct producer exits. Poll against the whole-call 2824// deadline; neither a full capture nor continuous output resets that deadline. 2825// Fixed-buffer callers retain the original contract; owned capture shares the same drain and deadline. 2826func tr_drain_until(rfd:i64,out:*u8,cap:i64,trunc:*i64,outlen:*i64,deadline:i64)->i64{ 2827 return tr_drain_until_owned(rfd,out,cap,trunc,outlen,deadline,0 as *NxBufOwned,0) 2828} 2829func tr_drain_until_owned(rfd:i64,out:*u8,cap:i64,trunc:*i64,outlen:*i64,deadline:i64,owned:*NxBufOwned,max_bytes:i64)->i64 { 2830 let scratch: *u8=sys_mmap(TR_DRAIN_BYTES) 2831 let state: *u8=sys_mmap(TR_CONTROL_BYTES) 2832 let ts: *i64=((state as i64)+TR_POLLFD_BYTES) as *i64 2833 var total: i64=0 2834 var result: i64=0 2835 var run: i64=1 2836 var eintr: i64=0 2837 while run == 1 { 2838 var remaining: i64=0-1 2839 if deadline > 0 { 2840 let now: i64=tr_clock_ms(ts) 2841 if now < 0 { result=TR_ERR_DRAIN; run=0 } else { 2842 remaining=deadline-now 2843 if remaining <= 0 { result=TR_ERR_TIMEOUT; run=0 } 2844 } 2845 } 2846 if run == 1 { 2847 tr_pollfd_set(state,rfd) 2848 let ready: i64=sys_poll(state,1,remaining) 2849 if ready > 0 { 2850 var dest: *u8=scratch 2851 var room: i64=TR_DRAIN_BYTES 2852 if (owned as i64)==0 { if total < cap { dest=((out as i64)+total) as *u8; room=cap-total } } 2853 let count: i64=sys_read(rfd,dest,room) 2854 if count > 0 { 2855 eintr=0 2856 if (owned as i64)!=0 { 2857 let appended:i64=nx_bo_append(owned,scratch,count,max_bytes) 2858 if appended!=0 {result=appended;run=0}else{total=owned.len} 2859 }else{ 2860 if total < cap { total=total+count } else { 2861 if (trunc as i64) != 0 { trunc[0]=TR_CUT } 2862 } 2863 } 2864 } else { 2865 if count == 0 { run=0 } else { 2866 if count == TR_EINTR { eintr=eintr+1 } else { result=TR_ERR_DRAIN; run=0 } 2867 } 2868 } 2869 } else { 2870 if ready == 0 { result=TR_ERR_TIMEOUT; run=0 } else { 2871 if ready == TR_EINTR { eintr=eintr+1 } else { result=TR_ERR_DRAIN; run=0 } 2872 } 2873 } 2874 if eintr > TR_EINTR_MAX { result=TR_ERR_DRAIN; run=0 } 2875 } 2876 } 2877 if (outlen as i64) != 0 { outlen[0]=total } 2878 sys_munmap(state,TR_CONTROL_BYTES) 2879 sys_munmap(scratch,TR_DRAIN_BYTES) 2880 return result 2881} 2882func tr_capture_stop(pid: i64, control: *i64) -> i64 { 2883 // Only signal a group established by this invocation, never the caller's group. 2884 if control[1] == 1 { nx_kill(0-pid,TR_SIGKILL) } 2885 nx_kill(pid,TR_SIGKILL) 2886 return 0 2887} 2888// The watchdog acknowledges lifetime binding before the parent relies on it. 2889// Its startup pipe is created after the worker fork, so the worker cannot retain it. 2890func tr_watchdog_child(owner: i64, pid: i64, control: *i64, deadline: i64, wfd: i64, scratch: *u8) -> i64 { 2891 if sys_bind_parent_lifetime(owner,TR_SIGKILL) < 0 { control[2]=TR_ERR_PARENT_LIFETIME; return 126 } 2892 if wfd != 1 { if sys_dup3(wfd,1,0) < 0 { control[2]=TR_ERR_FD_SETUP; return 126 } } 2893 sys_close(0); sys_close(2) 2894 if sys_close_inherited(3) < 0 { control[2]=TR_ERR_FD_SETUP; return 126 } 2895 scratch[128]=82 as u8 2896 if sys_write(1,scratch+128,1) != 1 { control[2]=TR_ERR_FD_SETUP; return 126 } 2897 sys_close(1) 2898 let ts: *i64=(scratch+160) as *i64 2899 var running: i64=1 2900 while running == 1 { 2901 let now: i64=tr_clock_ms(ts) 2902 if now < 0 { control[2]=TR_ERR_DRAIN; running=0 } else { 2903 if now >= deadline { control[0]=1; running=0 } else { 2904 if sys_sleep_ms(deadline-now) < 0 { control[2]=TR_ERR_DRAIN; running=0 } 2905 } 2906 } 2907 } 2908 tr_capture_stop(pid,control) 2909 return 0 2910} 2911func tr_watchdog_start(owner: i64, pid: i64, control: *i64, deadline: i64) -> i64 { 2912 let scratch: *u8=sys_mmap(TR_CONTROL_BYTES) 2913 let fds: *i64=scratch as *i64 2914 if sys_pipe2(fds,0) < 0 { sys_munmap(scratch,TR_CONTROL_BYTES); return 0-2 } 2915 let rfd: i64=fds[0] & 0xFFFFFFFF 2916 let wfd: i64=(fds[0] >> 32) & 0xFFFFFFFF 2917 let wd: i64=sys_fork() 2918 if wd == 0 { 2919 let rc: i64=tr_watchdog_child(owner,pid,control,deadline,wfd,scratch) 2920 sys_exit(rc); return rc 2921 } 2922 sys_close(wfd) 2923 if wd < 0 { sys_close(rfd); sys_munmap(scratch,TR_CONTROL_BYTES); return 0-3 } 2924 let length: *i64=(scratch+16) as *i64 2925 let cut: *i64=(scratch+32) as *i64 2926 let status: *i64=(scratch+64) as *i64 2927 var result: i64=tr_drain_until(rfd,scratch+128,1,cut,length,deadline) 2928 sys_close(rfd) 2929 if result == 0 { 2930 if length[0] != 1 || cut[0] != 0 || scratch[128] != 82 as u8 { result=TR_ERR_PARENT_LIFETIME } 2931 } 2932 if control[2] < 0 { result=control[2] } 2933 if result < 0 { nx_kill(wd,TR_SIGKILL); tr_reap(wd,status) } 2934 sys_munmap(scratch,TR_CONTROL_BYTES) 2935 if result < 0 { return result } 2936 return wd 2937} 2938func tr_run_capture_core(path:*u8,argv:*i64,out:*u8,cap:i64,outlen:*i64,timeout_ms:i64,trunc:*i64,cwd:*u8)->i64{ 2939 return tr_run_capture_core_owned(path,argv,out,cap,outlen,timeout_ms,trunc,cwd,0 as *NxBufOwned,0) 2940} 2941// The caller owns partial evidence even on error and releases it with nx_bo_release. 2942// A new capture requires an empty owner; never re-execute a child to discover its output length. 2943func tr_run_capture_owned(path:*u8,argv:*i64,owned:*NxBufOwned,timeout_ms:i64,max_bytes:i64,cwd:*u8)->i64{ 2944 if (owned as i64)<=0||max_bytes<0{return TR_ERR_DRAIN} 2945 if (owned.buf as i64)!=0||owned.len!=0||owned.cap!=0{return TR_ERR_DRAIN} 2946 var length:i64=0;var cut:i64=TR_FIT 2947 return tr_run_capture_core_owned(path,argv,0 as *u8,0,&length,timeout_ms,&cut,cwd,owned,max_bytes) 2948} 2949func tr_run_capture_core_owned(path:*u8,argv:*i64,out:*u8,cap:i64,outlen:*i64,timeout_ms:i64,trunc:*i64,cwd:*u8,owned:*NxBufOwned,max_bytes:i64)->i64 { 2950 if (outlen as i64) != 0 { outlen[0]=0 } 2951 if (trunc as i64) != 0 { trunc[0]=TR_FIT } 2952 if cap < 0 { return TR_ERR_DRAIN } 2953 let control: *i64=sys_mmap_shared(TR_CONTROL_BYTES) as *i64 2954 if (control as i64) <= 0 { return TR_ERR_ALLOC } 2955 let fds: *i64=((control as i64)+32) as *i64 2956 let stp: *i64=((control as i64)+48) as *i64 2957 let wstp: *i64=((control as i64)+64) as *i64 2958 let ts: *i64=((control as i64)+80) as *i64 2959 var deadline: i64=0 2960 if timeout_ms > 0 { 2961 let now: i64=tr_clock_ms(ts) 2962 if now < 0 { sys_munmap(control as *u8,TR_CONTROL_BYTES); return TR_ERR_DRAIN } 2963 deadline=now+timeout_ms 2964 if deadline < now { sys_munmap(control as *u8,TR_CONTROL_BYTES); return TR_ERR_DRAIN } 2965 } 2966 let owner: i64=__syscall(172,0,0,0,0,0,0) 2967 if owner <= 0 { sys_munmap(control as *u8,TR_CONTROL_BYTES); return TR_ERR_PARENT_LIFETIME } 2968 if sys_pipe2(fds,0) != 0 { sys_munmap(control as *u8,TR_CONTROL_BYTES); return 0-2 } 2969 let packed: i64=fds[0] 2970 let rfd: i64=packed & 0xFFFFFFFF 2971 let wfd: i64=(packed >> 32) & 0xFFFFFFFF 2972 let pid: i64=sys_fork() 2973 if pid < 0 { 2974 sys_close(rfd); sys_close(wfd) 2975 sys_munmap(control as *u8,TR_CONTROL_BYTES) 2976 return 0-3 2977 } 2978 if pid == 0 { 2979 sys_default_signal(13) 2980 if sys_bind_parent_lifetime(owner,TR_SIGKILL) < 0 { control[2]=TR_ERR_PARENT_LIFETIME; sys_exit(126); return 0 } 2981 if sys_setpgid(0,0) != 0 { control[2]=TR_ERR_GROUP; sys_exit(126); return 0 } 2982 control[1]=1 2983 // Close the read end first: it may occupy stdout/stderr when inherited fds are closed. 2984 sys_close(rfd) 2985 if wfd != 1 { sys_dup3(wfd,1,0) } 2986 if wfd != 2 { sys_dup3(wfd,2,0) } 2987 if wfd > 2 { sys_close(wfd) } 2988 let envp: *i64=((control as i64)+96) as *i64 2989 envp[0]="PATH=/usr/bin:/bin" as *u8 as i64; envp[1]=0 2990 // Setup failure travels independently of the program's legitimate exit status. 2991 if (cwd as i64) != 0 { 2992 if sys_chdir(cwd) != 0 { control[2]=TR_ERR_CHDIR; sys_exit(126); return 0 } 2993 } 2994 sys_execve_clean(path,argv,envp) 2995 tr_exec_failed(path) 2996 return 0 2997 } 2998 sys_close(wfd) 2999 var wd: i64=0 3000 if timeout_ms > 0 { 3001 wd=tr_watchdog_start(owner,pid,control,deadline) 3002 if wd < 0 { 3003 sys_close(rfd); tr_capture_stop(pid,control); tr_reap(pid,stp) 3004 sys_munmap(control as *u8,TR_CONTROL_BYTES) 3005 return wd 3006 } 3007 } 3008 let drained: i64=tr_drain_until_owned(rfd,out,cap,trunc,outlen,deadline,owned,max_bytes) 3009 sys_close(rfd) 3010 if drained < 0 { tr_capture_stop(pid,control) } 3011 let info: *u8=((control as i64)+128) as *u8 3012 let observed: i64=tr_observe_exit(pid,info) 3013 var watchwait: i64=0 3014 if wd > 0 { nx_kill(wd,TR_SIGKILL); watchwait=tr_reap(wd,wstp) } 3015 // ECHILD means ownership is already absent: never signal a potentially reused PID. 3016 if observed < 0 { if observed != TR_ECHILD { tr_capture_stop(pid,control) } } 3017 let waited: i64=tr_reap(pid,stp) 3018 var result: i64=wait_status_rc(stp[0]) 3019 if observed < 0 || waited < 0 || watchwait < 0 { result=0-4 } 3020 if control[2] < 0 { result=control[2] } 3021 if control[0] == 1 { result=TR_ERR_TIMEOUT } 3022 if drained < 0 { result=drained } 3023 sys_munmap(control as *u8,TR_CONTROL_BYTES) 3024 return result 3025} 3026 3027// tr_run1: convenience for the common "run ELF with a single string arg" case. Builds argv = [path, arg, 0]. 3028// arg may be null -> argv = [path, 0]. 3029func tr_run1(path: *u8, arg: *u8, out: *u8, cap: i64, outlen: *i64) -> i64 { 3030 let argv: *i64 = sys_mmap(32) as *i64 3031 argv[0] = path as i64 3032 if (arg as i64) == 0 { argv[1] = 0 } else { argv[1] = arg as i64; argv[2] = 0 } 3033 return tr_run_capture(path, argv, out, cap, outlen) 3034} 3035 3036// tr_run1_to: bounded twin of tr_run1. Same argv shaping, with a deadline. 3037func tr_run1_to(path: *u8, arg: *u8, out: *u8, cap: i64, outlen: *i64, timeout_ms: i64) -> i64 { 3038 let argv: *i64 = sys_mmap(32) as *i64 3039 argv[0] = path as i64 3040 if (arg as i64) == 0 { argv[1] = 0 } else { argv[1] = arg as i64; argv[2] = 0 } 3041 return tr_run_capture_to(path, argv, out, cap, outlen, timeout_ms) 3042} 3043 3044// tr_contains: 1 if the NUL-terminated needle occurs in buf[0..n), else 0. For gates asserting on captured stdout. 3045func tr_contains(buf: *u8, n: i64, needle: *u8) -> i64 { 3046 var nl: i64 = 0 3047 while needle[nl] != (0 as u8) { nl = nl + 1 } 3048 if nl == 0 { return 1 } 3049 if n < nl { return 0 } 3050 var i: i64 = 0 3051 while i <= n - nl { 3052 var m: i64 = 1 3053 var c: i64 = 0 3054 while c < nl { if buf[i + c] != needle[c] { m = 0; c = nl } else { c = c + 1 } } 3055 if m == 1 { return 1 } 3056 i = i + 1 3057 } 3058 return 0 3059} 3060 3061// nx_gate_verdict.nx -- THE canonical gate-AUTHORING verdict lib (D001 first rung, 2026-07-18). 3062// The debt: 2555 gate organs each hand-roll puts/num/pass/ttl/PASS-FAIL/verdict -- zero DRY. 3063// This is the ONE copy gates import instead. Sibling of nx_gate_green.nx (which JUDGES a gate's 3064// output from outside; this lib EMITS it from inside). Contract emitted: 3065// " <check-name>: PASS\n" | " <check-name>: FAIL\n" per check 3066// "\nNX-<GATE-NAME> passed <p>/<t> verdict=GREEN (<note>)\n" | " verdict=RED\n" 3067// -- the exact shape nx_gate_green / nx_autograde already judge (anchor "verdict=", pat "GREEN"). 3068// Usage: 3069// let ctr: *i64 = gv_ctr() // [0]=pass [1]=ttl 3070// gv_head("my gate -- what it proves") 3071// gv_check("T1 the thing holds", t1_ok, ctr) // t1_ok: 1 pass, else fail 3072// ... 3073// let rc: i64 = gv_verdict("MY-GATE", ctr, "green note") // prints summary; 0 GREEN / 1 RED 3074// sys_exit(rc) 3075// license_tier: ORIGINAL No hw writes (Rule 26). 3076 3077 3078const GV_NL: i64 = 10 3079 3080// Named plans make completeness independent of the number of checks that happened to execute. 3081// Layout: header(count, valid, undeclared); entries(name pointer, byte length, executions). 3082const GV_PLAN_HEADER: i64 = 3 3083const GV_PLAN_ENTRY: i64 = 3 3084const GV_I64_BYTES: i64 = 8 3085 3086func gv_plan_name_eq(a: *u8, an: i64, b: *u8, bn: i64) -> i64 { 3087 if an != bn { return 0 } 3088 var i: i64 = 0 3089 while i < an { if a[i] != b[i] { return 0 }; i = i+1 } 3090 return 1 3091} 3092func gv_plan_new(names: *u8) -> *i64 { 3093 var size: i64 = 0 3094 var count: i64 = 0 3095 while names[size] != (0 as u8) { 3096 if names[size] == (GV_NL as u8) { count = count+1 } 3097 size = size+1 3098 } 3099 let plan: *i64 = sys_mmap(GV_I64_BYTES*(GV_PLAN_HEADER+GV_PLAN_ENTRY*count)) as *i64 3100 plan[0]=count; plan[1]=1; plan[2]=0 3101 if count == 0 { plan[1]=0 } 3102 if size > 0 { if names[size-1] != (GV_NL as u8) { plan[1]=0 } } 3103 var start: i64 = 0 3104 var pos: i64 = 0 3105 var row: i64 = 0 3106 while pos < size { 3107 if names[pos] == (GV_NL as u8) { 3108 let cell: i64 = GV_PLAN_HEADER+GV_PLAN_ENTRY*row 3109 plan[cell]=(names as i64)+start 3110 plan[cell+1]=pos-start 3111 plan[cell+2]=0 3112 if pos == start { plan[1]=0 } 3113 var previous: i64 = 0 3114 while previous < row { 3115 let old: i64 = GV_PLAN_HEADER+GV_PLAN_ENTRY*previous 3116 if gv_plan_name_eq(plan[cell] as *u8,plan[cell+1],plan[old] as *u8,plan[old+1]) == 1 { plan[1]=0 } 3117 previous=previous+1 3118 } 3119 row=row+1; start=pos+1 3120 } 3121 pos=pos+1 3122 } 3123 return plan 3124} 3125func gv_plan_take(plan: *i64, name: *u8) -> i64 { 3126 var size: i64 = 0 3127 while name[size] != (0 as u8) { size=size+1 } 3128 var row: i64 = 0 3129 while row < plan[0] { 3130 let cell: i64 = GV_PLAN_HEADER+GV_PLAN_ENTRY*row 3131 if gv_plan_name_eq(plan[cell] as *u8,plan[cell+1],name,size) == 1 { 3132 plan[cell+2]=plan[cell+2]+1 3133 return (plan[cell+2] == 1) as i64 3134 } 3135 row=row+1 3136 } 3137 plan[2]=plan[2]+1 3138 return 0 3139} 3140func gv_plan_complete(plan: *i64) -> i64 { 3141 if plan[1] != 1 { return 0 } 3142 if plan[2] != 0 { return 0 } 3143 var row: i64 = 0 3144 while row < plan[0] { 3145 if plan[GV_PLAN_HEADER+GV_PLAN_ENTRY*row+2] != 1 { return 0 } 3146 row=row+1 3147 } 3148 return 1 3149} 3150func gv_plan_check(plan: *i64, name: *u8, cond: i64, ctr: *i64) -> i64 { 3151 let accepted: i64 = gv_plan_take(plan,name) 3152 return gv_check(name,((accepted == 1)&&(cond == 1)) as i64,ctr) 3153} 3154func gv_plan_finish(plan: *i64, ctr: *i64) -> i64 { 3155 var row: i64 = 0 3156 while row < plan[0] { 3157 let cell: i64 = GV_PLAN_HEADER+GV_PLAN_ENTRY*row 3158 if plan[cell+2] != 1 { 3159 gv_puts(" PLAN case=" as *u8) 3160 sys_write(1,plan[cell] as *u8,plan[cell+1]) 3161 gv_puts(" executions=" as *u8); gv_num(plan[cell+2]); gv_puts("\n" as *u8) 3162 } 3163 row=row+1 3164 } 3165 let result: i64 = gv_check("declared-plan-executed-exactly-once" as *u8,gv_plan_complete(plan),ctr) 3166 gv_puts(" PLAN declared=" as *u8); gv_num(plan[0]) 3167 gv_puts(" valid=" as *u8); gv_num(plan[1]) 3168 gv_puts(" undeclared=" as *u8); gv_num(plan[2]); gv_puts("\n" as *u8) 3169 sys_munmap(plan as *u8,GV_I64_BYTES*(GV_PLAN_HEADER+GV_PLAN_ENTRY*plan[0])) 3170 return result 3171} 3172 3173const GV_CTR_BYTES: i64 = 24 3174const GV_NUM_SCRATCH: i64 = 28 3175const GV_ZERO: i64 = 48 3176const GV_B10: i64 = 10 3177 3178func gv_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 3179func gv_num(v: i64) -> i64 { 3180 let b: *u8 = sys_mmap(GV_NUM_SCRATCH) 3181 let t: *u8 = sys_mmap(GV_NUM_SCRATCH) 3182 var m: i64 = v 3183 if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) } 3184 var k: i64 = 0 3185 if m == 0 { t[0] = GV_ZERO as u8; k = 1 } 3186 while m > 0 { t[k] = (GV_ZERO + (m % GV_B10)) as u8; m = m / GV_B10; k = k + 1 } 3187 var i: i64 = 0 3188 while i < k { b[i] = t[k-1-i]; i = i + 1 } 3189 sys_write(1, b, k) 3190 sys_munmap(b, GV_NUM_SCRATCH) 3191 sys_munmap(t, GV_NUM_SCRATCH) 3192 return 0 3193} 3194// EMIT ONE MEASURED KEY AND ITS VALUE, so a gate's GREEN can be checked from OUTSIDE the estate. 3195// ★★★★★★A GATE THAT PRINTS ONLY PASS IS UNFALSIFIABLE FROM THE OUTSIDE: an arithmetic that cannot see a 3196// number can never contradict one, so a pass-only gate makes an independent SECOND METHOD CLASS 3197// structurally impossible -- and PROVEN requires two independent method classes. The estate already had 3198// the law ("PRINT THE VALUES, NOT JUST PASS/FAIL") and no primitive for it, so every gate that obeyed the 3199// law hand-rolled its own printer and most gates simply did not obey it. This is that primitive. 3200// MEASURED 2026-09-03: nx_appliedmath_gate was 26/26 GREEN while emitting NOT ONE NUMBER; once it emitted 3201// the 28 values its teeth rest on, CPython recomputed 24 of them from the declared inputs, AGREED on all 3202// 24 with 15 at zero tolerance, and REFUSED two perturbed controls. None of that was possible the hour before. 3203// Call it AFTER the teeth and BEFORE gv_verdict: the verdict line must stay LAST for gv_last_line, which 3204// anchors by POSITION rather than by text. 3205func gv_kv(k: *u8, v: i64) -> i64 { 3206 gv_puts(" " as *u8); gv_puts(k); gv_puts("=" as *u8); gv_num(v); gv_puts("\n" as *u8) 3207 return 0 3208} 3209 3210// The header a reader (or an outside adjudicator) looks for to find the emitted block. 3211// ce_number_envelope -- CE6 (codeeffectiveness): AN EFFECTIVENESS FIGURE PUBLISHES WITH ITS ENVELOPE OR NOT AT ALL. 3212// A replay script that never observed the work matched frontier agents on static benchmarks, so a bare percentage 3213// is not evidence. ONE ruler for every board: the value, n (observations behind it), the interval [lo, hi] it sits 3214// in, and the null-control value (what the same ruler reads on a subject with nothing in it). A figure with n <= 0, 3215// no interval (lo > hi), or an interval that excludes its own value is REFUSED BY NAME and never printed as a value 3216// -- the refusal line replaces it, so no reader can mistake a bare number for a measured one. Returns 3217// CE_ENV_PUBLISHED or the named refusal code so a gate can assert it. Same placement as gv_kv: after the teeth, 3218// before gv_verdict, so the verdict line stays LAST. 3219const CE_ENV_PUBLISHED: i64 = 0 3220const CE_ENV_REFUSED_BARE: i64 = 1 3221const CE_ENV_REFUSED_NO_INTERVAL: i64 = 2 3222const CE_ENV_REFUSED_EXCLUDES: i64 = 3 3223func ce_env_refuse(k: *u8, why: *u8, n: i64, lo: i64, hi: i64, code: i64) -> i64 { 3224 gv_puts(" REFUSED-" as *u8); gv_puts(why); gv_puts(" key=" as *u8); gv_puts(k) 3225 gv_puts(" rule=ce_number_envelope n=" as *u8); gv_num(n); gv_puts(" lo=" as *u8); gv_num(lo); gv_puts(" hi=" as *u8); gv_num(hi) 3226 gv_puts(" -- a figure without n and an interval containing it does not publish\n" as *u8) 3227 return code 3228} 3229func ce_number_envelope(k: *u8, v: i64, n: i64, lo: i64, hi: i64, null_v: i64) -> i64 { 3230 if n <= 0 { return ce_env_refuse(k, "BARE-NUMBER" as *u8, n, lo, hi, CE_ENV_REFUSED_BARE) } 3231 if lo > hi { return ce_env_refuse(k, "NO-INTERVAL" as *u8, n, lo, hi, CE_ENV_REFUSED_NO_INTERVAL) } 3232 if v < lo { return ce_env_refuse(k, "INTERVAL-EXCLUDES-VALUE" as *u8, n, lo, hi, CE_ENV_REFUSED_EXCLUDES) } 3233 if v > hi { return ce_env_refuse(k, "INTERVAL-EXCLUDES-VALUE" as *u8, n, lo, hi, CE_ENV_REFUSED_EXCLUDES) } 3234 gv_puts(" " as *u8); gv_puts(k); gv_puts("=" as *u8); gv_num(v) 3235 gv_puts(" n=" as *u8); gv_num(n); gv_puts(" lo=" as *u8); gv_num(lo); gv_puts(" hi=" as *u8); gv_num(hi) 3236 gv_puts(" null=" as *u8); gv_num(null_v); gv_puts("\n" as *u8) 3237 return CE_ENV_PUBLISHED 3238} 3239 3240func gv_values_head() -> i64 { 3241 gv_puts("\n VALUES emitted for independent adjudication -- recompute each from the declared inputs\n" as *u8) 3242 return 0 3243} 3244 3245// ★★★★★★ASSERT AND EMIT IN ONE CALL, SO THE NUMBER PUBLISHED AND THE NUMBER TESTED CANNOT DRIFT APART. 3246// gv_kv alone leaves a gate author two jobs -- check a value, then remember to print it -- and the estate 3247// has measured what happens to any invariant that depends on two places agreeing by discipline: 666 gates 3248// sit on this base class and emit nothing at all. These make emission a SIDE EFFECT of the assertion, so 3249// there is exactly ONE expression and disagreement is impossible by construction rather than by care. 3250// MIGRATION IS MECHANICAL AND GREPPABLE: 3251// gv_check("name", (a == b) as i64, ctr) -> gv_check_eq("name", a, b, ctr) 3252// gv_check("name", (am_abs(a-b) <= t) as i64, ctr) -> gv_check_near("name", a, b, t, ctr) 3253// The tooth line keeps its exact leading shape, so every existing reader, rollup and last-line judge is 3254// unaffected; the values are APPENDED after the verdict word. 3255func gv_check_eq(name: *u8, actual: i64, expected: i64, ctr: *i64) -> i64 { 3256 let ok: i64 = (actual == expected) as i64 3257 gv_check(name, ok, ctr) 3258 gv_puts(" actual=" as *u8); gv_num(actual) 3259 gv_puts(" expected=" as *u8); gv_num(expected) 3260 gv_puts(" delta=" as *u8); gv_num(actual - expected) 3261 gv_puts("\n" as *u8) 3262 return ok 3263} 3264 3265// The same, for a value the gate can only hold to a DECLARED tolerance. The tolerance is EMITTED beside 3266// the delta on purpose: an outside adjudicator must be told what bar it is checking against, or it will 3267// invent one -- and a tolerance that lives only in a tooth name cannot be read by a machine. 3268func gv_check_near(name: *u8, actual: i64, expected: i64, tol: i64, ctr: *i64) -> i64 { 3269 var d: i64 = actual - expected 3270 if d < 0 { d = 0 - d } 3271 let ok: i64 = (d <= tol) as i64 3272 gv_check(name, ok, ctr) 3273 gv_puts(" actual=" as *u8); gv_num(actual) 3274 gv_puts(" expected=" as *u8); gv_num(expected) 3275 gv_puts(" delta=" as *u8); gv_num(d) 3276 gv_puts(" tol=" as *u8); gv_num(tol) 3277 gv_puts("\n" as *u8) 3278 return ok 3279} 3280 3281func gv_ctr() -> *i64 { 3282 let c: *i64 = sys_mmap(GV_CTR_BYTES) as *i64 3283 c[0] = 0 3284 c[1] = 0 3285 c[2] = 0 3286 return c 3287} 3288 3289// ---- THE THIRD STATE: "I COULD NOT TEST" IS NOT "IT IS BROKEN" -------------------------------- 3290// ADDED 2026-08-07. Ten service-facing gates were built and run for the first time; SIX came back RED 3291// and NOT ONE was a regression: missing fixtures (/tmp/sni_nishi_chain.der, /tmp/mozilla_certdata.txt), 3292// missing config+creds (golive_dns.conf, porkbun), or the service under test simply not running. 3293// A gate that reports FAIL when its PRECONDITIONS are absent is not reporting on the system at all -- 3294// it is reporting on its own environment, in the same word. 3295// ★★★★★★ A DETECTOR THAT CANNOT DISTINGUISH "I COULD NOT LOOK" FROM "I LOOKED AND IT IS BROKEN" 3296// TEACHES EVERYONE TO IGNORE IT, AND THEN IT IS WORSE THAN ABSENT. 3297// This estate already learned the law twice: nx_gonogo grew a third state because a two-state verdict 3298// WILL fabricate, and nx_commons_price returns UNPRICED rather than inventing a rate. Same rule here. 3299// gv_need declares a precondition. If it is absent the gate ends SKIP (evidence=none), never RED. 3300// SKIP is NOT a pass: it blocks any claim that the thing works, exactly as missing evidence blocks a 3301// GO but never a NO-GO. 3302func gv_need(name: *u8, present: i64, ctr: *i64) -> i64 { 3303 if present == 1 { return 1 } 3304 ctr[2] = ctr[2] + 1 3305 gv_puts(" PRECONDITION MISSING: " as *u8) 3306 gv_puts(name) 3307 gv_puts(" -- cannot test, NOT a failure of the system under test\n" as *u8) 3308 return 0 3309} 3310// ---- gv_subjects: THE EMPTY-SET LAW, MADE STRUCTURAL (2026-08-22) --------------------------------- 3311// THE DEFECT: ctr[0]/ctr[1] count TEETH, never SUBJECTS. A gate with ten teeth over ZERO files reports 3312// 10/10 GREEN and every consumer reads a pass. This estate has WRITTEN the law repeatedly -- 3313// "A TOOTH THAT PASSES ON THE EMPTY SET IS NOT A TOOTH", "BIND EVERY AGGREGATE ASSERTION TO ITS 3314// DENOMINATOR" -- and never built the mechanism, so the discipline lived per-organ and hand-rolled 3315// (nx_plane_check exits 4 EMPTY-not-a-pass; nx_battery_grade returns UNMEASURED on zero answer rows). 3316// *A LAW EVERY AUTHOR MUST REMEMBER IS A LAW THAT WILL BE FORGOTTEN; ONLY A PRIMITIVE IN THE PATH HOLDS. 3317// MEASURED THE DAY THIS WAS WRITTEN: a ship harness skipped its work behind a load guard, wrote no log, 3318// and EXITED 0 -- inconclusive reading as success, inside the tooling built to enforce the opposite. 3319// 3320// A ZERO POPULATION IS NOT A FAILURE OF THE SUBJECT, IT IS THE ABSENCE OF EVIDENCE ABOUT IT. So this is 3321// DELIBERATELY a precondition (SKIP), never a RED -- and it COMPOSES gv_need rather than inventing a 3322// fourth state, so it inherits the already-proven ordering: a SKIP co-occurring with a real failure 3323// still escalates to RED and can never amnesty it. 3324// Fail-CLOSED on a negative count: an unreadable population is not permission to claim coverage. 3325// The count PRINTS ALWAYS, pass or not, so coverage is on the record instead of inferred. 3326func gv_subjects(name: *u8, n: i64, ctr: *i64) -> i64 { 3327 gv_puts(" subjects=" as *u8) 3328 gv_num(n) 3329 gv_puts(" [" as *u8) 3330 gv_puts(name) 3331 gv_puts("]\n" as *u8) 3332 var present: i64 = 0 3333 if n > 0 { present = 1 } 3334 return gv_need(name, present, ctr) 3335} 3336func gv_head(title: *u8) -> i64 { gv_puts(title); gv_puts("\n\n" as *u8); return 0 } 3337// one check: prints " <name>: PASS|FAIL", bumps counters, returns cond 3338func gv_check(name: *u8, cond: i64, ctr: *i64) -> i64 { 3339 ctr[1] = ctr[1] + 1 3340 gv_puts(" " as *u8) 3341 gv_puts(name) 3342 gv_puts(": " as *u8) 3343 if cond == 1 { ctr[0] = ctr[0] + 1; gv_puts("PASS\n" as *u8) } else { gv_puts("FAIL\n" as *u8) } 3344 return cond 3345} 3346// ---- HOISTED FROM THE NAS COPY 2026-07-31 (ws=gate-dry-d001). THE BASE CLASS HAD FORKED: the NAS tree 3347// carried gv_bite/gv_cat/gv_catn/gv_journal and this tree carried only the original six, so a gate written 3348// against one tree would not compile on the other -- and, worse, an identical migration bought DIFFERENT 3349// capability depending on where it happened. Converging the ANCESTOR is the fix; every descendant gains 3350// these without being touched, including the ones not written yet. That is the whole point of a base class. 3351// 3352// BITE-PROVEN cell: the non-vacuity law made structural. A detector counts ONLY if it FIRES on the crafted 3353// bad input AND stays SILENT on the crafted good one. A cell green before the defect exists is VACUOUS and 3354// proves nothing (the gates-green-on-garbage class). Prints the sub-verdict so vacuity is SEEN, not counted. 3355func gv_bite(name: *u8, bad: i64, good: i64, ctr: *i64) -> i64 { 3356 var ok: i64 = 0 3357 if bad == 1 { if good == 0 { ok = 1 } } 3358 ctr[1] = ctr[1] + 1 3359 gv_puts(" " as *u8) 3360 gv_puts(name) 3361 if ok == 1 { ctr[0] = ctr[0] + 1; gv_puts(": BITE-PROVEN (fires on bad, silent on good)\n" as *u8) } 3362 if ok == 0 { 3363 gv_puts(": FAIL " as *u8) 3364 if bad != 1 { gv_puts("[VACUOUS: did not fire on the bad input]" as *u8) } 3365 if good != 0 { gv_puts("[FALSE-POSITIVE: fired on the good input]" as *u8) } 3366 gv_puts("\n" as *u8) 3367 } 3368 return ok 3369} 3370 3371const GV_MODE_644: i64 = 420 3372const GV_LINE: i64 = 512 3373const GV_TAB: i64 = 9 3374const GV_SLASH: i64 = 47 3375const GV_MINUS: i64 = 45 3376 3377func gv_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; var p: i64 = o; while s[i] != (0 as u8) { d[p] = s[i]; p = p + 1; i = i + 1 } return p } 3378func gv_catn(d: *u8, o: i64, v: i64) -> i64 { 3379 let t: *u8 = sys_mmap(GV_NUM_SCRATCH) 3380 var m: i64 = v 3381 var p: i64 = o 3382 if m < 0 { d[p] = GV_MINUS as u8; p = p + 1; m = 0 - m } 3383 var k: i64 = 0 3384 if m == 0 { t[0] = GV_ZERO as u8; k = 1 } 3385 while m > 0 { t[k] = (GV_ZERO + (m % GV_B10)) as u8; m = m / GV_B10; k = k + 1 } 3386 var i: i64 = 0 3387 while i < k { d[p] = t[k-1-i]; p = p + 1; i = i + 1 } 3388 sys_munmap(t, GV_NUM_SCRATCH) 3389 return p 3390} 3391 3392// FAIL-SOFT outcome journal: every gate that emits a verdict self-records ONE actlog-grammar frame, so 3393// the HARNESS class finally has evidence at all -- flake and EROSION (a banked GREEN later going RED) 3394// become derivable, and the frames are minable for free. A write failure NEVER touches the verdict: 3395// no permission, no journal, no problem. Append-only, single line, conflict-free (O_APPEND). 3396// Deliberately self-contained (no new imports): organs define their own sj_*/cat helpers, so importing a 3397// json lib here would collide across hundreds of consumers. 3398func gv_journal(name: *u8, passed: i64, total: i64, green: i64) -> i64 { 3399 let fd: i64 = sys_openat_append("knowledge/status/harness.jrnl" as *u8, GV_MODE_644) 3400 if fd < 0 { return 0 } 3401 let ln: *u8 = sys_mmap(GV_LINE) 3402 var o: i64 = gv_catn(ln, 0, sys_now_realtime_sec()) 3403 ln[o] = GV_TAB as u8; o = o + 1 3404 o = gv_cat(ln, o, "harness" as *u8) 3405 ln[o] = GV_TAB as u8; o = o + 1 3406 o = gv_cat(ln, o, name) 3407 ln[o] = GV_TAB as u8; o = o + 1 3408 o = gv_cat(ln, o, "run" as *u8) 3409 ln[o] = GV_TAB as u8; o = o + 1 3410 if green == 1 { o = gv_cat(ln, o, "GREEN" as *u8) } else { o = gv_cat(ln, o, "RED" as *u8) } 3411 ln[o] = GV_TAB as u8; o = o + 1 3412 o = gv_catn(ln, o, passed) 3413 ln[o] = GV_SLASH as u8; o = o + 1 3414 o = gv_catn(ln, o, total) 3415 ln[o] = GV_NL as u8; o = o + 1 3416 sys_write(fd, ln, o) 3417 sys_close(fd) 3418 sys_munmap(ln, GV_LINE) 3419 return 0 3420} 3421 3422// summary + verdict; returns exit code (0 GREEN / 1 RED). Caller sys_exit(rc). 3423// ============================================================================================ 3424// AD1's REFUSING HALF, WIRED (2026-09-03, lane K). MEASURED FIRST: gv_bare_rate had sat in this file 3425// since 2026-08-27 with ZERO PRODUCTION CALLERS -- a grep for gv_bare_rate over buildroot/runtime 3426// returns matches=8 over files=23592 with coverage_complete=1 corpus_complete=1, and every one of 3427// those eight is this file's own comment, this file's own definition, or nx_rigor_envelope_gate, 3428// which is its own gate. Its sibling gv_envelope_check returns matches=1 on that same corpus: its 3429// definition, and nothing else. The rigor-envelope header above argues that a ruler in a sibling lib 3430// is adopted at advice rates while a ruler in the base class is adopted by every gate that imports 3431// it -- the ruler was duly placed in the base class and then never called from the emitter. 3432// ***THE ADOPTION ARGUMENT WAS MADE AND THE WIRING WAS NOT DONE***, so the refusal was built, 3433// gate-proven, and unreachable from every production emitter in the estate. 3434// 3435// SILENT WHEN CLEAN, BY CONSTRUCTION. This prints NOTHING unless the note actually carries a rate 3436// with no denominator, so stdout stays BYTE-IDENTICAL for every gate that does not have the defect 3437// and no existing judge-equivalence proof is invalidated by wiring it in. 3438// 3439// IT ANNOUNCES AND COUNTS, IT DOES NOT REFUSE -- CHOSEN, NOT CONCEDED. gv_verdict's return value IS 3440// the fleet verdict (/api/gate_run derives GREEN/RED/SKIP from it), so letting a bare rate flip that 3441// return would turn every gate whose note quotes a rate RED in one edit to the base class every gate 3442// imports: the permanently-red detector everyone learns to ignore, installed at the root. The 3443// refusing direction belongs to the QA admission contract, which can weigh it per candidate and 3444// abstain when it cannot look. 3445// 3446// PRINTED ABOVE THE VERDICT LINE ON PURPOSE: gv_last_line and every positional reader in the estate 3447// anchor on the FINAL line, so an announcement appended after the verdict would silently break all 3448// of them. Emitting before the first verdict byte is what keeps the verdict last. 3449func gv_note_bare_rate(note: *u8) -> i64 { 3450 if (note as i64) == 0 { return 0 } 3451 var n: i64 = 0 3452 while note[n] != (0 as u8) { n = n + 1 } 3453 let off: i64 = gv_bare_rate(note, n) 3454 if off < 0 { return 0 } 3455 gv_puts("\nNX-RIGOR bare-rate-in-note byte_offset=" as *u8) 3456 gv_num(off) 3457 gv_puts(" note_bytes=" as *u8) 3458 gv_num(n) 3459 gv_puts(" rule=AD1-gv_bare_rate\n" as *u8) 3460 gv_puts(" A rate is published here whose enclosing object carries no n. Bind it to its denominator\n" as *u8) 3461 gv_puts(" via gv_envelope / gv_envelope_json. ADVISORY: the verdict line below is UNCHANGED by this.\n" as *u8) 3462 return 1 3463} 3464 3465func gv_verdict(name: *u8, ctr: *i64, note: *u8) -> i64 { 3466 // AD1 REFUSING HALF, WIRED: scan the note we are about to publish BEFORE any verdict byte is 3467 // emitted, so this announcement can never displace the verdict line from the tail of the output. 3468 gv_note_bare_rate(note) 3469 // THIRD STATE FIRST. If any precondition was missing the run produced NO EVIDENCE about the 3470 // system under test, so it must not be reported in the same word as a real failure. 3471 // The third state IS plumbed, end to end: this returns 3, and nx_mgmt_api maps rc==3 to SKIP and 3472 // excludes it from RED. It used to return 1 under a comment admitting that was hand-waving. 3473 // A COMMENT THAT OUTLIVES THE DEFECT IT DESCRIBES BECOMES A FALSE CLAIM WITH A TRUSTED BYLINE -- 3474 // AND OTHERS BUILD WORKAROUNDS AGAINST IT. MEASURED: nx_mcu_ready_gate hand-rolled its own exit 3475 // code because this paragraph told it not to trust the return value, and that hand-rolled version 3476 // A FAILURE IS EVIDENCE, AND EVIDENCE MUST NOT BE AMNESTIED. The third state answers "I could not 3477 // look". It must not also answer "I looked, I found a defect, and something ELSE was missing too". 3478 // MEASURED 2026-08-15 on nx_adversarial_sov_gate: three checks RAN, one FAILED, one precondition was 3479 // missing -- and this function reported `verdict=SKIP ... proved NOTHING about the system under test` 3480 // while holding a real failure. The one failure a gate exists to catch was the one it could not 3481 // report. A SKIP THAT CAN SWALLOW A RED IS NOT A THIRD STATE, IT IS AN AMNESTY. 3482 // ORDERING IS THE WHOLE FIX, and it is a strict TIGHTENING: the only verdict that moves is 3483 // (preconditions missing AND at least one check failed), SKIP -> RED. A clean run cannot become RED, 3484 // a RED cannot become GREEN, and a SKIP with nothing failing is still SKIP. This can never bless 3485 // anything -- it can only stop something being blessed. 3486 // The missing preconditions are still NAMED on the verdict line, because "RED, and also partly 3487 // unobservable" is a different situation from "RED, fully measured", and a reader needs both. 3488 if ctr[1] > ctr[0] { 3489 gv_puts("\nNX-" as *u8) 3490 gv_puts(name) 3491 gv_puts(" passed " as *u8) 3492 gv_num(ctr[0]) 3493 gv_puts("/" as *u8) 3494 gv_num(ctr[1]) 3495 if ctr[2] > 0 { 3496 gv_puts(" (with " as *u8) 3497 gv_num(ctr[2]) 3498 gv_puts(" precondition(s) ALSO missing -- a failed check is evidence, so this is RED, not SKIP)" as *u8) 3499 } 3500 gv_puts(" verdict=RED\n" as *u8) 3501 gv_journal(name, ctr[0], ctr[1], 0) 3502 return 1 3503 } 3504 // silently collapsed SKIP into RED -- the very class of bug it was avoiding. 3505 if ctr[2] > 0 { 3506 gv_puts("\nNX-" as *u8) 3507 gv_puts(name) 3508 gv_puts(" verdict=SKIP -- " as *u8) 3509 gv_num(ctr[2]) 3510 gv_puts(" precondition(s) missing; ran " as *u8) 3511 gv_num(ctr[1]) 3512 gv_puts(" checks, proved NOTHING about the system under test.\n" as *u8) 3513 gv_puts(" SKIP is not a pass: it blocks any claim that this works, exactly as missing\n" as *u8) 3514 gv_puts(" evidence blocks a GO but never a NO-GO.\n" as *u8) 3515 gv_journal(name, ctr[0], ctr[1], 0) 3516 // EXIT 3 = SKIP, distinct from 1 = RED. This used to return 1 with a comment admitting the 3517 // distinction "lives in the output text until gate_run maps a third exit code". That was 3518 // hand-waving: a third state that collapses to RED at the transport is not a third state. 3519 // /api/gate_run now maps 3 -> SKIP, so "I could not look" is machine-readable fleet-wide. 3520 return 3 3521 } 3522 gv_puts("\nNX-" as *u8) 3523 gv_puts(name) 3524 gv_puts(" passed " as *u8) 3525 gv_num(ctr[0]) 3526 gv_puts("/" as *u8) 3527 gv_num(ctr[1]) 3528 if ctr[0] == ctr[1] { 3529 if ctr[1] > 0 { 3530 gv_puts(" verdict=GREEN (" as *u8) 3531 gv_puts(note) 3532 gv_puts(")\n" as *u8) 3533 // The journal write is a FILE side-effect only -- stdout, the PASS/FAIL vector and the 3534 // verdict line are byte-unchanged, so every migration already proven judge-equivalent 3535 // stays valid. Every inheriting gate now records its own outcome without being touched. 3536 gv_journal(name, ctr[0], ctr[1], 1) 3537 return 0 3538 } 3539 } 3540 gv_puts(" verdict=RED\n" as *u8) 3541 gv_journal(name, ctr[0], ctr[1], 0) 3542 return 1 3543} 3544 3545// ============================================================================================ 3546// THE RIGOR ENVELOPE (AD1, 2026-08-27): A PUBLISHED NUMBER IS A STRUCT, NEVER A BARE RATE. 3547// ============================================================================================ 3548// WHY IT LIVES IN THE BASE CLASS: /compare/autograde published "92 percent resolved" with no 3549// denominator (the July record says n=14: 13 of 14); /compare/gen's referee rows publish HPSv2 / 3550// PickScore / GenEval values with no n, no interval, no engine hash. Both boards were made to share 3551// ONE ruler by the operator's standing order (proven evidence mandatory on BOTH), and the only place 3552// every gate already inherits from is this file. A ruler in a sibling lib is adopted at advice 3553// rates; a ruler in the base class is adopted at 100 percent of the gates that import it. 3554// 3555// THE STRUCT (out[GV_ENV_FIELDS], every field an i64, rates in PERMIL): 3556// k n rate wilson_lo wilson_hi boot_lo boot_hi env_lo env_hi clusters B seed measured z_micro width target 3557// THE METHOD, with its sources named so a reader can refute it rather than trust it: 3558// * Wilson score interval (Wilson 1927, JASA 22:209-212), z = Phi^-1(0.975) = 1.959964 for a 3559// two-sided 95 percent interval. Chosen over the Wald interval because Wald collapses to zero width 3560// at k=0 and k=n, which is exactly where a small benchmark lives (0 of 14, 14 of 14). 3561// * A HIERARCHICAL (two-level) bootstrap over the task nesting (Miller et al. 2025, "Statistical 3562// Precipice" -- agent benchmarks nest runs inside tasks, and a flat interval under-covers): 3563// resample clusters with replacement, then each chosen cluster's outcomes with replacement, B 3564// replicates, percentile interval. The envelope is the WIDER of the two -- the bootstrap can only 3565// widen Wilson, never narrow it, so a caller cannot launder a small n through clustering. 3566// * A Park-Miller minimal-standard generator (Park and Miller 1988, CACM 31(10)): multiply-and-mod 3567// only, so no bitwise or shift semantics are relied on, seed-deterministic so two runs of the same 3568// ledger publish the same interval. 3569// * INTEGER ARITHMETIC THROUGHOUT (this is the no-float estate). Micro-units (1e6) carry z and the 3570// radicand; the square root is bisection on i64. The five reference values below were checked by 3571// hand against the closed form before this shipped: 7/14 -> [268,732], 13/14 -> [685,987], 3572// 0/14 -> [0,215], 14/14 -> [785,1000], 1/1 -> [207,1000] permil. 3573// THE REFUSAL: gv_bare_rate scans a text body for digits followed by "%" or " percent" whose enclosing 3574// JSON object carries no "n" field, and returns the offender's byte offset. A page emitted through 3575// gv_envelope_json never trips it, because that object always carries "n". A hand-typed percent does. 3576// THE PARAMETERS are ARGUMENTS, not literals read here: z_micro, B, seed and the width target come 3577// from knowledge/rigor.conf through nx_stage_path.sp_rigor_int (this lib deliberately imports nothing 3578// new -- `const EP_MAGIC_1024` already exists independently in nx_ecomat_put.nx:12, so importing 3579// nx_estate_path here would collide at some gate's next build). The bootstrap defaults below are 3580// the fallback when no conf row exists, and every printed line names the values it used. 3581// UNMEASURED IS ITS OWN STATE: n=0 returns 0 and prints UNMEASURED; a cluster partition that does not 3582// sum to (k,n) returns GV_ENV_REFUSED_PARTITION and prints REFUSED-PARTITION -- a partition is a claim. 3583const GV_ENV_FIELDS: i64 = 16 3584const GV_ENV_BYTES: i64 = 128 // GV_ENV_FIELDS * 8 3585const GV_ENV_K: i64 = 0 3586const GV_ENV_N: i64 = 1 3587const GV_ENV_RATE: i64 = 2 3588const GV_ENV_WLO: i64 = 3 3589const GV_ENV_WHI: i64 = 4 3590const GV_ENV_BLO: i64 = 5 3591const GV_ENV_BHI: i64 = 6 3592const GV_ENV_LO: i64 = 7 3593const GV_ENV_HI: i64 = 8 3594const GV_ENV_CLUSTERS: i64 = 9 3595const GV_ENV_B: i64 = 10 3596const GV_ENV_SEED: i64 = 11 3597const GV_ENV_MEASURED: i64 = 12 3598const GV_ENV_Z: i64 = 13 3599const GV_ENV_WIDTH: i64 = 14 3600const GV_ENV_TARGET: i64 = 15 3601const GV_ENV_REFUSED_PARTITION: i64 = 0 - 1 3602const GV_MICRO: i64 = 1000000 3603const GV_PERMIL: i64 = 1000 3604// Phi^-1(0.975) in micro-units: the two-sided 95 percent normal quantile (Wilson 1927). 3605const GV_Z95_MICRO: i64 = 1959964 3606// the radicand is carried in micro-units; multiplying by 100 before the integer sqrt makes the root 3607// come out scaled by 1e4 (sqrt(1e6 * 1e2) = 1e4), i.e. four decimals of the root survive the floor. 3608const GV_SQRT_IN_SCALE: i64 = 100 3609const GV_SQRT_OUT_SCALE: i64 = 10000 3610// B=1000 is the textbook floor for a bootstrap percentile interval (Efron and Tibshirani 1993, ch.13). 3611const GV_BOOT_B_DEFAULT: i64 = 1000 3612// any fixed seed is reproducible; this one names the day the envelope shipped, so a reader can date it. 3613const GV_BOOT_SEED_DEFAULT: i64 = 20260827 3614// Park-Miller minimal standard: x' = 16807 * x mod (2^31 - 1). Period 2^31-2, far above B * n here. 3615const GV_PM_A: i64 = 16807 3616const GV_PM_M: i64 = 2147483647 3617// n*n must fit an i64 for the radicand 4k(n-k): the exact limit is 3,037,000,499; floored with headroom. 3618const GV_ENV_N_MAX: i64 = 2000000000 3619// isqrt bisection upper bound: the largest v with v*v <= 2^63-1 is 3,037,000,499 (no mid*mid overflow). 3620const GV_ISQRT_HI: i64 = 3037000499 3621// replicate rates are permil, so a 1001-bin histogram makes the percentile walk O(B) with no sort. 3622const GV_HIST_BINS: i64 = 1001 3623const GV_HIST_BYTES: i64 = 8008 // GV_HIST_BINS * 8 3624// the 2.5th and 97.5th percentiles of the replicate distribution bound a two-sided 95 percent interval. 3625const GV_PCT_LO_PERMIL: i64 = 25 3626const GV_PCT_HI_PERMIL: i64 = 975 3627const GV_ENV_UNDECLARED: *u8 = "UNDECLARED" 3628 3629func gv_isqrt(v: i64) -> i64 { 3630 if v <= 0 { return 0 } 3631 var lo: i64 = 0 3632 var hi: i64 = v 3633 if hi > GV_ISQRT_HI { hi = GV_ISQRT_HI } 3634 while lo < hi { 3635 let mid: i64 = (lo + hi + 1) / 2 3636 if mid * mid <= v { lo = mid } else { hi = mid - 1 } 3637 } 3638 return lo 3639} 3640 3641// Wilson bounds in permil for k of n at z (micro). out[0]=lo out[1]=hi. Returns 1, or 0 UNMEASURED. 3642// bounds = (2k + z^2 +- z * sqrt(4k(n-k)/n + z^2)) / (2(n + z^2)) 3643func gv_wilson_permil(k: i64, n: i64, z_micro: i64, out: *i64) -> i64 { 3644 if n <= 0 { return 0 } 3645 if n > GV_ENV_N_MAX { return 0 } 3646 if k < 0 { return 0 } 3647 if k > n { return 0 } 3648 let z2: i64 = (z_micro * z_micro) / GV_MICRO 3649 let a: i64 = 2 * k * GV_MICRO + z2 3650 let d: i64 = 2 * (n * GV_MICRO + z2) 3651 let f: i64 = 4 * k * (n - k) 3652 let q: i64 = f / n 3653 let r: i64 = f - q * n 3654 let rad: i64 = q * GV_MICRO + (r * GV_MICRO) / n + z2 3655 let s: i64 = gv_isqrt(rad * GV_SQRT_IN_SCALE) 3656 let t: i64 = (z_micro * s) / GV_SQRT_OUT_SCALE 3657 var lo: i64 = ((a - t) * GV_PERMIL + d / 2) / d 3658 var hi: i64 = ((a + t) * GV_PERMIL + d / 2) / d 3659 if lo < 0 { lo = 0 } 3660 if hi > GV_PERMIL { hi = GV_PERMIL } 3661 out[0] = lo 3662 out[1] = hi 3663 return 1 3664} 3665 3666func gv_rng_next(state: *i64) -> i64 { 3667 var x: i64 = state[0] 3668 if x <= 0 { x = 1 } 3669 if x >= GV_PM_M { x = x % GV_PM_M } 3670 if x == 0 { x = 1 } 3671 x = (GV_PM_A * x) % GV_PM_M 3672 state[0] = x 3673 return x 3674} 3675 3676// Two-level bootstrap over clusters (ck[c] successes of cn[c] outcomes). out[0]=lo out[1]=hi permil. 3677// Returns 1, or 0 when there is nothing to resample. 3678func gv_boot_hier(ck: *i64, cn: *i64, nc: i64, b: i64, seed: i64, out: *i64) -> i64 { 3679 if nc <= 0 { return 0 } 3680 if b <= 0 { return 0 } 3681 var ntot: i64 = 0 3682 var c0: i64 = 0 3683 while c0 < nc { ntot = ntot + cn[c0]; c0 = c0 + 1 } 3684 if ntot <= 0 { return 0 } 3685 let hist: *i64 = sys_mmap(GV_HIST_BYTES) as *i64 3686 var hz: i64 = 0 3687 while hz < GV_HIST_BINS { hist[hz] = 0; hz = hz + 1 } 3688 let st: *i64 = sys_mmap(8) as *i64 3689 st[0] = seed 3690 var rep: i64 = 0 3691 while rep < b { 3692 var succ: i64 = 0 3693 var tot: i64 = 0 3694 var pick: i64 = 0 3695 while pick < nc { 3696 let c: i64 = gv_rng_next(st) % nc 3697 let kc: i64 = ck[c] 3698 let ncc: i64 = cn[c] 3699 var dd: i64 = 0 3700 while dd < ncc { 3701 if (gv_rng_next(st) % ncc) < kc { succ = succ + 1 } 3702 tot = tot + 1 3703 dd = dd + 1 3704 } 3705 pick = pick + 1 3706 } 3707 var rate: i64 = 0 3708 if tot > 0 { rate = (succ * GV_PERMIL + tot / 2) / tot } 3709 if rate < 0 { rate = 0 } 3710 if rate > GV_PERMIL { rate = GV_PERMIL } 3711 hist[rate] = hist[rate] + 1 3712 rep = rep + 1 3713 } 3714 let lo_need: i64 = (b * GV_PCT_LO_PERMIL + GV_PERMIL - 1) / GV_PERMIL 3715 let hi_need: i64 = (b * GV_PCT_HI_PERMIL + GV_PERMIL - 1) / GV_PERMIL 3716 var cum: i64 = 0 3717 var lo: i64 = 0 - 1 3718 var hi: i64 = 0 - 1 3719 var bin: i64 = 0 3720 while bin < GV_HIST_BINS { 3721 cum = cum + hist[bin] 3722 if lo < 0 { if cum >= lo_need { lo = bin } } 3723 if hi < 0 { if cum >= hi_need { hi = bin } } 3724 bin = bin + 1 3725 } 3726 if lo < 0 { lo = 0 } 3727 if hi < 0 { hi = GV_PERMIL } 3728 sys_munmap(hist as *u8, GV_HIST_BYTES) 3729 sys_munmap(st as *u8, 8) 3730 out[0] = lo 3731 out[1] = hi 3732 return 1 3733} 3734 3735// THE ENVELOPE. ck/cn/nc describe the task nesting (pass nc=0 for "no nesting known": the plain 3736// bootstrap is the degenerate hierarchy and clusters=1 is printed so the reader can see that). 3737// width_target is the conf row (permil); width_ok is derived from it and printed, never decided here. 3738// Returns 1 MEASURED, 0 UNMEASURED (n<=0 or k out of range), GV_ENV_REFUSED_PARTITION when the 3739// clusters do not sum to (k, n). 3740func gv_envelope(k: i64, n: i64, ck: *i64, cn: *i64, nc: i64, z_micro: i64, b: i64, seed: i64, width_target: i64, out: *i64) -> i64 { 3741 var i: i64 = 0 3742 while i < GV_ENV_FIELDS { out[i] = 0; i = i + 1 } 3743 out[GV_ENV_K] = k 3744 out[GV_ENV_N] = n 3745 out[GV_ENV_Z] = z_micro 3746 out[GV_ENV_B] = b 3747 out[GV_ENV_SEED] = seed 3748 out[GV_ENV_TARGET] = width_target 3749 out[GV_ENV_CLUSTERS] = nc 3750 if n <= 0 { return 0 } 3751 if k < 0 { return 0 } 3752 if k > n { return 0 } 3753 if nc > 0 { 3754 var sk: i64 = 0 3755 var sn: i64 = 0 3756 var c: i64 = 0 3757 while c < nc { sk = sk + ck[c]; sn = sn + cn[c]; c = c + 1 } 3758 if sk != k { return GV_ENV_REFUSED_PARTITION } 3759 if sn != n { return GV_ENV_REFUSED_PARTITION } 3760 } 3761 out[GV_ENV_RATE] = (k * GV_PERMIL + n / 2) / n 3762 let w: *i64 = sys_mmap(16) as *i64 3763 if gv_wilson_permil(k, n, z_micro, w) == 0 { return 0 } 3764 out[GV_ENV_WLO] = w[0] 3765 out[GV_ENV_WHI] = w[1] 3766 var kk: *i64 = ck 3767 var nn: *i64 = cn 3768 var ncl: i64 = nc 3769 if ncl <= 0 { 3770 let one: *i64 = sys_mmap(16) as *i64 3771 one[0] = k 3772 one[1] = n 3773 kk = one 3774 nn = ((one as i64) + 8) as *i64 3775 ncl = 1 3776 } 3777 out[GV_ENV_CLUSTERS] = ncl 3778 let bb: *i64 = sys_mmap(16) as *i64 3779 if gv_boot_hier(kk, nn, ncl, b, seed, bb) == 1 { 3780 out[GV_ENV_BLO] = bb[0] 3781 out[GV_ENV_BHI] = bb[1] 3782 } else { 3783 out[GV_ENV_BLO] = w[0] 3784 out[GV_ENV_BHI] = w[1] 3785 } 3786 var lo: i64 = w[0] 3787 if out[GV_ENV_BLO] < lo { lo = out[GV_ENV_BLO] } 3788 var hi: i64 = w[1] 3789 if out[GV_ENV_BHI] > hi { hi = out[GV_ENV_BHI] } 3790 out[GV_ENV_LO] = lo 3791 out[GV_ENV_HI] = hi 3792 out[GV_ENV_WIDTH] = hi - lo 3793 out[GV_ENV_MEASURED] = 1 3794 return 1 3795} 3796 3797// width_ok: 1 when a target was given and the envelope is no wider than it; 0 otherwise. A target of 3798// 0 means "none declared" and reads as 0 -- an undeclared bar can never be met. 3799func gv_envelope_width_ok(out: *i64) -> i64 { 3800 if out[GV_ENV_MEASURED] != 1 { return 0 } 3801 if out[GV_ENV_TARGET] <= 0 { return 0 } 3802 if out[GV_ENV_WIDTH] <= out[GV_ENV_TARGET] { return 1 } 3803 return 0 3804} 3805 3806func gv_env_pair(a: i64, b: i64) -> i64 { 3807 gv_puts("[" as *u8); gv_num(a); gv_puts("," as *u8); gv_num(b); gv_puts("]" as *u8) 3808 return 0 3809} 3810 3811// the canonical printed line: one row, every parameter on it, so the number can be re-derived. 3812func gv_envelope_print(label: *u8, out: *i64, harness: *u8) -> i64 { 3813 gv_puts(" ENVELOPE " as *u8) 3814 gv_puts(label) 3815 gv_puts(" k=" as *u8); gv_num(out[GV_ENV_K]) 3816 gv_puts(" n=" as *u8); gv_num(out[GV_ENV_N]) 3817 if out[GV_ENV_MEASURED] == 1 { 3818 gv_puts(" rate_permil=" as *u8); gv_num(out[GV_ENV_RATE]) 3819 gv_puts(" ci_permil=" as *u8); gv_env_pair(out[GV_ENV_LO], out[GV_ENV_HI]) 3820 gv_puts(" wilson=" as *u8); gv_env_pair(out[GV_ENV_WLO], out[GV_ENV_WHI]) 3821 gv_puts(" boot=" as *u8); gv_env_pair(out[GV_ENV_BLO], out[GV_ENV_BHI]) 3822 gv_puts(" width=" as *u8); gv_num(out[GV_ENV_WIDTH]) 3823 gv_puts(" target=" as *u8); gv_num(out[GV_ENV_TARGET]) 3824 gv_puts(" width_ok=" as *u8); gv_num(gv_envelope_width_ok(out)) 3825 } else { 3826 gv_puts(" UNMEASURED (no population -- a rate over nothing is not a rate)" as *u8) 3827 } 3828 gv_puts(" clusters=" as *u8); gv_num(out[GV_ENV_CLUSTERS]) 3829 gv_puts(" B=" as *u8); gv_num(out[GV_ENV_B]) 3830 gv_puts(" seed=" as *u8); gv_num(out[GV_ENV_SEED]) 3831 gv_puts(" z_micro=" as *u8); gv_num(out[GV_ENV_Z]) 3832 gv_puts(" harness=" as *u8) 3833 if (harness as i64) == 0 { gv_puts(GV_ENV_UNDECLARED) } else { if harness[0] == (0 as u8) { gv_puts(GV_ENV_UNDECLARED) } else { gv_puts(harness) } } 3834 gv_puts(" method=wilson+hierarchical-bootstrap\n" as *u8) 3835 return 0 3836} 3837 3838// JSON object for a published page. Always carries "n" (the field the refusal looks for). 3839func gv_envelope_json(d: *u8, o0: i64, out: *i64, harness: *u8) -> i64 { 3840 var o: i64 = o0 3841 o = gv_cat(d, o, "{\"k\":" as *u8); o = gv_catn(d, o, out[GV_ENV_K]) 3842 o = gv_cat(d, o, ",\"n\":" as *u8); o = gv_catn(d, o, out[GV_ENV_N]) 3843 if out[GV_ENV_MEASURED] == 1 { 3844 o = gv_cat(d, o, ",\"rate_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_RATE]) 3845 o = gv_cat(d, o, ",\"ci_lo_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_LO]) 3846 o = gv_cat(d, o, ",\"ci_hi_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_HI]) 3847 o = gv_cat(d, o, ",\"wilson_lo_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_WLO]) 3848 o = gv_cat(d, o, ",\"wilson_hi_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_WHI]) 3849 o = gv_cat(d, o, ",\"boot_lo_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_BLO]) 3850 o = gv_cat(d, o, ",\"boot_hi_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_BHI]) 3851 o = gv_cat(d, o, ",\"width_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_WIDTH]) 3852 o = gv_cat(d, o, ",\"width_target_permil\":" as *u8); o = gv_catn(d, o, out[GV_ENV_TARGET]) 3853 o = gv_cat(d, o, ",\"width_ok\":" as *u8); o = gv_catn(d, o, gv_envelope_width_ok(out)) 3854 o = gv_cat(d, o, ",\"verdict\":\"MEASURED\"" as *u8) 3855 } else { 3856 o = gv_cat(d, o, ",\"rate_permil\":null,\"ci_lo_permil\":null,\"ci_hi_permil\":null,\"verdict\":\"UNMEASURED\"" as *u8) 3857 } 3858 o = gv_cat(d, o, ",\"clusters\":" as *u8); o = gv_catn(d, o, out[GV_ENV_CLUSTERS]) 3859 o = gv_cat(d, o, ",\"boot_reps\":" as *u8); o = gv_catn(d, o, out[GV_ENV_B]) 3860 o = gv_cat(d, o, ",\"seed\":" as *u8); o = gv_catn(d, o, out[GV_ENV_SEED]) 3861 o = gv_cat(d, o, ",\"z_micro\":" as *u8); o = gv_catn(d, o, out[GV_ENV_Z]) 3862 o = gv_cat(d, o, ",\"harness\":\"" as *u8) 3863 if (harness as i64) == 0 { o = gv_cat(d, o, GV_ENV_UNDECLARED) } else { if harness[0] == (0 as u8) { o = gv_cat(d, o, GV_ENV_UNDECLARED) } else { o = gv_cat(d, o, harness) } } 3864 o = gv_cat(d, o, "\",\"method\":\"wilson+hierarchical-bootstrap\"}" as *u8) 3865 return o 3866} 3867 3868// does buf[at..] begin with lit (NUL-terminated), inside n? 3869func gv_at(buf: *u8, n: i64, at: i64, lit: *u8) -> i64 { 3870 var i: i64 = 0 3871 while lit[i] != (0 as u8) { 3872 if at + i >= n { return 0 } 3873 if buf[at + i] != lit[i] { return 0 } 3874 i = i + 1 3875 } 3876 return 1 3877} 3878 3879// does the JSON object enclosing position `at` (nearest '{' before, nearest '}' after) carry "n":? 3880func gv_obj_has_n(buf: *u8, n: i64, at: i64) -> i64 { 3881 var s: i64 = at 3882 var go: i64 = 1 3883 while go == 1 { 3884 if s <= 0 { s = 0; go = 0 } else { 3885 if buf[s] == (123 as u8) { go = 0 } else { s = s - 1 } 3886 } 3887 } 3888 var e: i64 = at 3889 go = 1 3890 while go == 1 { 3891 if e >= n { e = n; go = 0 } else { 3892 if buf[e] == (125 as u8) { go = 0 } else { e = e + 1 } 3893 } 3894 } 3895 var i: i64 = s 3896 while i < e { 3897 if gv_at(buf, e, i, "\"n\":" as *u8) == 1 { return 1 } 3898 i = i + 1 3899 } 3900 return 0 3901} 3902 3903// THE REFUSAL. Returns the byte offset of the first bare rate (digits + "%" or " percent" with no "n" 3904// in the enclosing object), or -1 when the body is clean. 3905func gv_bare_rate(buf: *u8, n: i64) -> i64 { 3906 var i: i64 = 0 3907 while i < n { 3908 let c: i64 = buf[i] as i64 3909 var isd: i64 = 0 3910 if c >= 48 { if c <= 57 { isd = 1 } } 3911 if isd == 1 { 3912 var j: i64 = i 3913 var scan: i64 = 1 3914 while scan == 1 { 3915 if j >= n { scan = 0 } else { 3916 let dch: i64 = buf[j] as i64 3917 var dd: i64 = 0 3918 if dch >= 48 { if dch <= 57 { dd = 1 } } 3919 if dd == 1 { j = j + 1 } else { scan = 0 } 3920 } 3921 } 3922 var hit: i64 = 0 3923 if j < n { if buf[j] == (37 as u8) { hit = 1 } } 3924 if hit == 0 { if gv_at(buf, n, j, " percent" as *u8) == 1 { hit = 1 } } 3925 if hit == 1 { if gv_obj_has_n(buf, n, i) == 0 { return i } } 3926 i = j 3927 } else { i = i + 1 } 3928 } 3929 return 0 - 1 3930} 3931 3932// AD2's half of the contract: two envelopes are directly comparable only when their harness manifests 3933// are the SAME declared hash. UNDECLARED (or empty) on either side refuses -- an undeclared harness is 3934// not a matching one. 3935func gv_envelope_comparable(ha: *u8, hb: *u8) -> i64 { 3936 if (ha as i64) == 0 { return 0 } 3937 if (hb as i64) == 0 { return 0 } 3938 if ha[0] == (0 as u8) { return 0 } 3939 if hb[0] == (0 as u8) { return 0 } 3940 if gv_at(ha, GV_LINE, 0, GV_ENV_UNDECLARED) == 1 { return 0 } 3941 if gv_at(hb, GV_LINE, 0, GV_ENV_UNDECLARED) == 1 { return 0 } 3942 var i: i64 = 0 3943 while ha[i] != (0 as u8) { 3944 if ha[i] != hb[i] { return 0 } 3945 i = i + 1 3946 } 3947 if hb[i] != (0 as u8) { return 0 } 3948 return 1 3949} 3950 3951// a tooth that binds a published rate to its denominator: passes only when the envelope MEASURED, and 3952// prints the envelope line beside the PASS/FAIL so the reader sees the numbers, not a boolean. 3953func gv_envelope_check(name: *u8, out: *i64, harness: *u8, ctr: *i64) -> i64 { 3954 gv_envelope_print(name, out, harness) 3955 var ok: i64 = 0 3956 if out[GV_ENV_MEASURED] == 1 { ok = 1 } 3957 return gv_check(name, ok, ctr) 3958} 3959 3960func cg_owned(mode:*u8,b:*NxBufOwned,limit:i64,tmo:i64)->i64{ 3961 let path:*u8="/volume1/homes/elderwesto/nishihost/buildroot/_build/nx_capture_fixture_t218.sov.elf" 3962 let av:*i64=sys_mmap_try(24) as *i64;if (av as i64)<=0{return TR_ERR_ALLOC};av[0]=path as i64;av[1]=mode as i64;av[2]=0 3963 let rc:i64=tr_run_capture_owned(path,av,b,tmo,limit,0 as *u8);sys_munmap_direct(av as *u8,24);return rc 3964} 3965func main()->i64{ 3966 let c:*i64=gv_ctr();let b:*NxBufOwned=sys_mmap_try(24) as *NxBufOwned;if (b as i64)<=0{return 2} 3967 b.buf=0 as *u8;b.len=0;b.cap=0 3968 let rc:i64=cg_owned("large",b,0,8000);gv_check("owned producer completes once beyond former64KiB",rc==0&&b.len==196613,c) 3969 var exact:i64=1;var i:i64=0;while i<b.len{if b.buf[i]!=(i%251) as u8{exact=0};i=i+1};gv_check("entire retained binary stdout matches producer pattern",exact==1&&b.len==196613,c) 3970 gv_check("nonempty owner refuses another execution",cg_owned("large",b,0,8000)==TR_ERR_DRAIN,c) 3971 gv_check("owned release clears all allocation state",nx_bo_release(b)==0&&(b.buf as i64)==0&&b.len==0&&b.cap==0,c) 3972 gv_check("explicit caller budget refuses rather than false success",cg_owned("large",b,65536,8000)==NX_BO_CAPACITY,c) 3973 gv_check("budget partial evidence stays within declared limit",b.len<=65536&&b.cap<=65536,c);nx_bo_release(b) 3974 gv_check("child nonzero status retained",cg_owned("fail",b,0,8000)==7&&b.len==7,c);nx_bo_release(b) 3975 gv_check("empty successful child has no allocation",cg_owned("empty",b,0,8000)==0&&b.len==0&&b.cap==0,c);nx_bo_release(b) 3976 gv_check("timeout uses existing process lifecycle",cg_owned("timeout",b,0,100)==TR_ERR_TIMEOUT,c);nx_bo_release(b) 3977 let path:*u8="/volume1/homes/elderwesto/nishihost/buildroot/_build/nx_capture_fixture_t218.sov.elf" 3978 let av:*i64=sys_mmap_try(24) as *i64;let out:*u8=sys_mmap_try(65536);if (av as i64)<=0||(out as i64)<=0{return 3} 3979 av[0]=path as i64;av[1]="large" as *u8 as i64;av[2]=0;var n:i64=0;var cut:i64=0 3980 gv_check("legacy fixed capture retains truncation contract",tr_run_capture_tr(path,av,out,65536,&n,8000,&cut)==0&&n==65536&&cut==TR_CUT,c) 3981 sys_munmap_direct(out,65536);sys_munmap_direct(av as *u8,24);sys_munmap_direct(b as *u8,24) 3982 return gv_verdict("owned-process-capture",c,"Actual child, complete binary output, caller budget, nonzero, empty, timeout and legacy truncation") 3983}