code wiki / (root) / nx_shader_capture_owned_expanded_t218.nx

nx_shader_capture_owned_expanded_t218.nx source

↩ module page · 5364 lines · 262144 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// One shader-capture owner for normal and explicitly selected staged builds. 3061// Digest checks bracket each execution; they detect changed files, not hostile swap-and-restore races. 3062// Native packaging by artifact reference; no publication or engine-acceptance claim. 3063// nx_fsops_lib.nx -- CONSOLIDATED filesystem tool (MCP name: nx_fs, tool #4 of the 15), LIBRARY half. 3064// (Source is named nx_fsops because nx_fs.nx is the safety-enveloped file-I/O STDLIB -- a different thing.) 3065// READ-ONLY first increment: `read` (bounded file read) + `ls` (typed dir listing). Retires ssh-cat for 3066// remote reads per rule 27 (api-first, no shell plumbing). 3067// 3068// BOUNDARY DEFENSE (rule 12 -- MCP callers are EXTERNAL input): `read` REFUSES any path that matches the 3069// secret DENY-LIST: compiled-in default needles (secret/key/token/passw/.pem, matched case-insensitively 3070// against the WHOLE path) plus data-driven extras from fs_read_deny.conf (one lowercase needle per line, 3071// CWD-relative -- rule 11: policy in data, not code). The tools-api runs where key material lives; an 3072// arbitrary-read tool that could return opaque_keys.bin or tools_cap_secret.key would convert a read-cap 3073// into a key-theft primitive. Over-blocking is the SAFE failure direction for v1. 3074// WRITE/EDIT increment (2026-07-16): fsx_write (ATOMIC tmp+fsync+rename) + fsx_edit (exact-string replace 3075// with the Claude-Edit UNIQUENESS contract). Exposed as the SEPARATE tools-api name `nx_fs_write` (its own 3076// cap class per knowledge/mcp/exposure_policy.txt: read=broad, write=cap) -- the `nx_fs` name stays read-only. 3077// The write DENY is a superset of the read deny (never clobber key material) PLUS the OS device/kernel/ 3078// firmware namespace via the nx_os_fs seam (rule 26 never-brick BY CONSTRUCTION -- not config-disableable) 3079// PLUS the tool-registry escalation surface ("allowlist") PLUS data-driven extras (fs_write_deny.conf). 3080// license_tier: ORIGINAL 3081 3082// nx_fio.nx -- canonical sovereign file operations: unlink (delete) + existence check. Importable (no main). 3083// Retires Remove-Item / rm. rename is already canonical (sys_renameat in nx_syscalls). unlinkat x86_64=263 is passed 3084// DIRECTLY (the fsync-74 / fstatat-262 / unlinkat-263 precedent: a raw x86_64 number not in the rv64->x86 swap table 3085// passes through untranslated). AT_FDCWD=-100, flags=0. Returns 0 on success, -errno on failure. license_tier: ORIGINAL 3086 3087// sha256.nx -- SHA-256 in pure NishiLang (Phase G9, FIPS 180-4). 3088// 3089// Canonical: this is the substrate-wide canonical SHA-256 3090// implementation per [[feedback-no-tool-proliferation-bit-level]]. 3091// HMAC-SHA256 / HKDF-SHA256 / DRBG-SHA256 etc. compose THIS file's 3092// sha256 primitive; they're distinct primitives (different specs: 3093// FIPS 198-1 HMAC, RFC 5869 HKDF, NIST SP 800-90A DRBG) but all 3094// share THIS sha256 as their SHA-256 backbone. Re-implementing 3095// the SHA-256 K-table or round function inline is refused. 3096// 3097// license_tier: INDEPENDENT_REDERIVE 3098// genealogy_id: international-research-sources/nist/fips_180_4 3099// 3100// Used for: content-addressed build artifacts (F6), session tokens 3101// (rand.nx + sha256 = HMAC), TLS 1.3 handshake (G15), Git-style 3102// object addressing, reproducible-build attestation. 3103// 3104// Implementation follows FIPS 180-4 section 6.2 exactly -- no 3105// precomputed tables beyond the standard K[0..63] round constants. 3106// Pure i64 arithmetic; all 32-bit ops masked with 0xFFFFFFFF. 3107// 3108// API: 3109// sha256_init(*ctx) — reset a fresh Sha256 context 3110// sha256_update(*ctx, *u8 bytes, len) — feed input chunks 3111// sha256_final(*ctx, *u8 out32) — write 32-byte digest 3112// sha256_digest(*u8 bytes, len, *u8 out32) — one-shot convenience 3113// 3114// The context is ~128 bytes: 8 words of hash state + 64-byte partial 3115// block buffer + 8-byte length counter + an index. Caller allocates 3116// (stack or heap) and passes pointer. 3117// 3118// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 3119// intended_use: "SHA-256 cryptographic hash -- HMAC + HKDF 3120// + content-addressed storage + digital 3121// signatures + Wheeler-DDC integrity chain" 3122// sil_target: SIL3 (integrity primitive; collision or 3123// preimage attack = signature forgery) 3124// asil_target: QM 3125// dal_target: DAL B 3126// iec_62304_class: B 3127// evidence: [no_floating_point, no_table_lookup, 3128// bit_equal_reproducible, 3129// FIPS_180-4_Sec_5_3_3_init_vector, 3130// NIST_CAVP_test_vectors_VERIFIED, 3131// constant_time_by_construction, 3132// license_tier_INDEPENDENT_REDERIVE] 3133// hazard_register: [bug-tape-length-extension-attack, 3134// bug-tape-implementation-skipping-final-block, 3135// bug-tape-state-not-cleared-after-use] 3136// residual_risk: "Length-extension attack applies to raw 3137// SHA-256. Callers MUST use HMAC-SHA-256 3138// (nx_hmac) for keyed scenarios; never raw 3139// SHA-256(key || msg). Substrate cannot 3140// enforce this from the hash primitive's 3141// boundary; it's a composition responsibility." 3142// verdict: NOT_YET_EVALUATED 3143 3144 3145// nx_bits.nx -- bit-manipulation primitives, dispatching to hardware 3146// intrinsics on supported backends with portable software fallbacks. 3147// 3148// Inspired by Hacker's Delight (Henry S. Warren Jr.) -- the canonical 3149// reference for bit-twiddling. Every soft path is BRANCHLESS or 3150// minimally-branched, FIXED-CYCLE, and CROSS-ARCH PORTABLE. 3151// 3152// Dispatch model: 3153// nx_bits_popcount64 / nx_bits_clz32 / nx_bits_ctz32 -> backend 3154// intrinsic on x86_64 (popcntq/bsrl+xor/bsfl) and rv64 with Zbb 3155// (cpop/clzw/ctzw). One machine instruction. Used by hot paths 3156// (sketches, hashing, bitmap iteration). 3157// 3158// nx_bits_popcount64_soft / nx_bits_clz32_soft / nx_bits_ctz32_soft 3159// -- pure-NishiLang SWAR + binary-search variants. Cross-arch 3160// portable to backends without bit-count opcodes. Used by paired 3161// correctness oracles and any caller targeting an exotic ISA. 3162// 3163// Substrate "get off C" trajectory: this module is pure NishiLang. 3164 3165 3166 3167// === popcount FAST: dispatches to backend intrinsic ============== 3168 3169func nx_bits_popcount64(x: i64) -> i64 { 3170 return __popcnt64(x) 3171} 3172 3173func nx_bits_popcount32(x: i64) -> i64 { 3174 return __popcnt64(x & 0xFFFFFFFF) 3175} 3176 3177// === clz32 / ctz32 FAST: backend intrinsic ======================= 3178// __clz32(0) and __ctz32(0) both return 32 on both backends (x86 3179// uses a tested fallback to set the result; rv64 Zbb returns 32 by 3180// spec) so the wrapper is a thin pass-through. 3181 3182func nx_bits_clz32(x: i64) -> i64 { 3183 return __clz32(x) 3184} 3185 3186func nx_bits_ctz32(x: i64) -> i64 { 3187 return __ctz32(x) 3188} 3189 3190// 64-bit clz / ctz: composed from two 32-bit intrinsics. Until the 3191// backend grows OP_CLZ64 / OP_CTZ64 these are still ~3-instruction 3192// hot paths vs the legacy 64-iteration loops, so they replace those 3193// substrate-wide. clz(0) = 64; ctz(0) = 64. 3194 3195func nx_bits_clz64(x: i64) -> i64 { 3196 let hi: i64 = (x >> 32) & 0xFFFFFFFF 3197 if hi != 0 { return __clz32(hi) } 3198 return 32 + __clz32(x & 0xFFFFFFFF) 3199} 3200 3201func nx_bits_ctz64(x: i64) -> i64 { 3202 let lo: i64 = x & 0xFFFFFFFF 3203 if lo != 0 { return __ctz32(lo) } 3204 if x == 0 { return 64 } 3205 return 32 + __ctz32((x >> 32) & 0xFFFFFFFF) 3206} 3207 3208// === rotate left / right FAST: backend intrinsic ================ 3209// Hardware native via rolq/rorq (x86_64, 1985) and rol/ror (rv64 3210// Zbb). Mask the count to 0..63 so the substrate exposes 3211// "rotate-mod-64" semantics on both ISAs (x86_64 already masks; rv64 3212// behaviour is identical with the explicit mask). 3213 3214func nx_bits_rotl64(x: i64, n: i64) -> i64 { 3215 return __rotl64(x, n & 63) 3216} 3217 3218func nx_bits_rotr64(x: i64, n: i64) -> i64 { 3219 return __rotr64(x, n & 63) 3220} 3221 3222// 32-bit rotate (no native intrinsic emitted; we pre-mask the value 3223// to its low 32 bits so the i64 arithmetic shift right doesn't 3224// contaminate with sign bits, then mask the result back to 32 bits). 3225// ~3 ops vs 5-7 in inline rotr32/rotl32 callsites scattered across 3226// crypto modules (SHA-256, ChaCha20, MurmurHash, etc.). 3227 3228func nx_bits_rotl32(x: i64, n: i64) -> i64 { 3229 let v: i64 = x & 0xFFFFFFFF 3230 let nn: i64 = n & 31 3231 if nn == 0 { return v } 3232 return ((v << nn) | (v >> (32 - nn))) & 0xFFFFFFFF 3233} 3234 3235func nx_bits_rotr32(x: i64, n: i64) -> i64 { 3236 let v: i64 = x & 0xFFFFFFFF 3237 let nn: i64 = n & 31 3238 if nn == 0 { return v } 3239 return ((v >> nn) | (v << (32 - nn))) & 0xFFFFFFFF 3240} 3241 3242// === byte-reverse FAST: backend intrinsic ========================= 3243// bswapq (x86_64, i486 1989+, universal) and rev8 (rv64 Zbb). 1 3244// cycle vs the 13-op SWAR phrasing. Used by every endian flip, 3245// every network/header parse, SHA-256 big-endian word loads. 3246 3247func nx_bits_bswap64(x: i64) -> i64 { 3248 return __bswap64(x) 3249} 3250 3251// 32-bit byte-reverse: mask to low 32 (zero-extends the i64), bswap 3252// the whole register -- the four low bytes get reversed into the top 3253// half -- then shift down to recover them. Mask after shift to 3254// discard the sign extension on inputs where bit 31 of the bswapped 3255// low half is set (which becomes bit 63 of the 64-bit register). 3256func nx_bits_bswap32(x: i64) -> i64 { 3257 return (__bswap64(x & 0xFFFFFFFF) >> 32) & 0xFFFFFFFF 3258} 3259 3260// === SOFT fallbacks: pure NishiLang, cross-arch portable ========= 3261 3262func nx_bits_popcount64_soft(x: i64) -> i64 { 3263 var v: i64 = x 3264 v = v - ((v >> 1) & 0x5555555555555555) 3265 v = (v & 0x3333333333333333) + ((v >> 2) & 0x3333333333333333) 3266 v = (v + (v >> 4)) & 0x0F0F0F0F0F0F0F0F 3267 return ((v * 0x0101010101010101) >> 56) & 0xFF 3268} 3269 3270func nx_bits_popcount32_soft(x: i64) -> i64 { 3271 var v: i64 = x & 0xFFFFFFFF 3272 v = v - ((v >> 1) & 0x55555555) 3273 v = (v & 0x33333333) + ((v >> 2) & 0x33333333) 3274 v = (v + (v >> 4)) & 0x0F0F0F0F 3275 return ((v * 0x01010101) >> 24) & 0xFF 3276} 3277 3278func nx_bits_clz32_soft(x: i64) -> i64 { 3279 let lo: i64 = x & 0xFFFFFFFF 3280 if lo == 0 { return 32 } 3281 var t: i64 = lo 3282 var n: i64 = 0 3283 if (t & 0xFFFF0000) == 0 { n = n + 16; t = t << 16; t = t & 0xFFFFFFFF } 3284 if (t & 0xFF000000) == 0 { n = n + 8; t = t << 8; t = t & 0xFFFFFFFF } 3285 if (t & 0xF0000000) == 0 { n = n + 4; t = t << 4; t = t & 0xFFFFFFFF } 3286 if (t & 0xC0000000) == 0 { n = n + 2; t = t << 2; t = t & 0xFFFFFFFF } 3287 if (t & 0x80000000) == 0 { n = n + 1 } 3288 return n 3289} 3290 3291// 64-bit rotate soft fallback (pure NishiLang -- shift+or, ~5 ops). 3292// Used by paired correctness oracle and by backends without rotate 3293// opcodes. Note: shifting by 0 is the identity; explicit branch 3294// avoids the undefined-behaviour case of `x >> 64` on some ISAs. 3295 3296// The signed >> arithmetic-shifts sign bits in for negative x, so the 3297// shifted-right half must be masked to the actual m / (64-m) low bits 3298// to discard the sign extension. 3299 3300func nx_bits_rotl64_soft(x: i64, n: i64) -> i64 { 3301 let m: i64 = n & 63 3302 if m == 0 { return x } 3303 let top: i64 = (x >> (64 - m)) & ((1 << m) - 1) 3304 return (x << m) | top 3305} 3306 3307func nx_bits_rotr64_soft(x: i64, n: i64) -> i64 { 3308 let m: i64 = n & 63 3309 if m == 0 { return x } 3310 let low: i64 = (x >> m) & ((1 << (64 - m)) - 1) 3311 return low | (x << (64 - m)) 3312} 3313 3314// bswap SOFT (Hacker's Delight 7-1, 13-op SWAR). Used by paired 3315// oracle and exotic backends. 3316 3317func nx_bits_bswap64_soft(x: i64) -> i64 { 3318 var v: i64 = x 3319 v = ((v & 0x00FF00FF00FF00FF) << 8) | ((v >> 8) & 0x00FF00FF00FF00FF) 3320 v = ((v & 0x0000FFFF0000FFFF) << 16) | ((v >> 16) & 0x0000FFFF0000FFFF) 3321 v = ((v & 0x00000000FFFFFFFF) << 32) | ((v >> 32) & 0x00000000FFFFFFFF) 3322 return v 3323} 3324 3325func nx_bits_bswap32_soft(x: i64) -> i64 { 3326 let v: i64 = x & 0xFFFFFFFF 3327 let b0: i64 = (v >> 24) & 0xFF 3328 let b1: i64 = (v >> 16) & 0xFF 3329 let b2: i64 = (v >> 8) & 0xFF 3330 let b3: i64 = (v ) & 0xFF 3331 return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0 3332} 3333 3334// 32-bit rotate SOFT (identical body to FAST; no separate intrinsic 3335// path) -- kept as the named-soft for the consolidation paired-oracle 3336// convention. 3337func nx_bits_rotl32_soft(x: i64, n: i64) -> i64 { 3338 return nx_bits_rotl32(x, n) 3339} 3340func nx_bits_rotr32_soft(x: i64, n: i64) -> i64 { 3341 return nx_bits_rotr32(x, n) 3342} 3343 3344// 64-bit soft fallbacks (Knuth TAOCP 4A linear-scan). O(64) iterations 3345// in the worst case; used by the paired oracle and by exotic backends. 3346 3347func nx_bits_clz64_soft(x: i64) -> i64 { 3348 if x == 0 { return 64 } 3349 var v: i64 = x 3350 var n: i64 = 0 3351 var mask: i64 = 0x8000000000000000 3352 var done: i64 = 0 3353 while done == 0 { 3354 if (v & mask) != 0 { done = 1 } 3355 if done == 0 { 3356 n = n + 1 3357 mask = mask >> 1 3358 if mask == 0 { done = 1 } 3359 } 3360 } 3361 return n 3362} 3363 3364func nx_bits_ctz64_soft(x: i64) -> i64 { 3365 if x == 0 { return 64 } 3366 var v: i64 = x 3367 var n: i64 = 0 3368 var done: i64 = 0 3369 while done == 0 { 3370 if (v & 1) != 0 { done = 1 } 3371 if done == 0 { 3372 n = n + 1 3373 v = v >> 1 3374 if n >= 64 { done = 1 } 3375 } 3376 } 3377 return n 3378} 3379 3380func nx_bits_ctz32_soft(x: i64) -> i64 { 3381 let lo: i64 = x & 0xFFFFFFFF 3382 if lo == 0 { return 32 } 3383 var t: i64 = lo 3384 var n: i64 = 0 3385 if (t & 0x0000FFFF) == 0 { n = n + 16; t = t >> 16 } 3386 if (t & 0x000000FF) == 0 { n = n + 8; t = t >> 8 } 3387 if (t & 0x0000000F) == 0 { n = n + 4; t = t >> 4 } 3388 if (t & 0x00000003) == 0 { n = n + 2; t = t >> 2 } 3389 if (t & 0x00000001) == 0 { n = n + 1 } 3390 return n 3391} 3392 3393// === isolate lowest set bit (Hacker's Delight 2-1) ================ 3394// 3395// x & -x selects only the lowest 1-bit of x. Useful for iterating 3396// set bits in a bitmap (faster than testing each bit). 3397// for bitmap != 0: 3398// bit = nx_bits_lowest(bitmap) 3399// // process bit 3400// bitmap = bitmap ^ bit // clear it 3401 3402func nx_bits_lowest(x: i64) -> i64 { 3403 return x & (0 - x) 3404} 3405 3406// === reset lowest set bit (Hacker's Delight 2-1) ================== 3407// 3408// x & (x-1) clears the lowest 1-bit. When combined with popcount, 3409// gives O(popcount) bit-traversal loops -- faster than O(width) 3410// when the bitmap is sparse. 3411 3412func nx_bits_clear_lowest(x: i64) -> i64 { 3413 return x & (x - 1) 3414} 3415 3416// === is power of 2 (Hacker's Delight 2-1) ========================= 3417// 3418// x > 0 AND (x & (x-1)) == 0. One subtract + one and + one compare. 3419 3420func nx_bits_is_pow2(x: i64) -> i64 { 3421 if x <= 0 { return 0 } 3422 if (x & (x - 1)) == 0 { return 1 } 3423 return 0 3424} 3425 3426// === next power of 2 (Hacker's Delight 3-2) ======================= 3427// 3428// Round up to next power of 2. For x already pow2, returns x. 3429// For x = 0, returns 1. Standard "smear high bit" pattern. 3430 3431func nx_bits_next_pow2_32(x: i64) -> i64 { 3432 if x <= 1 { return 1 } 3433 var v: i64 = (x - 1) & 0xFFFFFFFF 3434 v = v | (v >> 1) 3435 v = v | (v >> 2) 3436 v = v | (v >> 4) 3437 v = v | (v >> 8) 3438 v = v | (v >> 16) 3439 return (v + 1) & 0xFFFFFFFF 3440} 3441 3442// === parity (Hacker's Delight 5-1) ================================ 3443// 3444// Returns 1 if odd number of set bits, 0 if even. Two-and-XOR 3445// reduction, branchless. 3446 3447func nx_bits_parity64(x: i64) -> i64 { 3448 var v: i64 = x 3449 v = v ^ (v >> 32) 3450 v = v ^ (v >> 16) 3451 v = v ^ (v >> 8) 3452 v = v ^ (v >> 4) 3453 return (0x6996 >> (v & 15)) & 1 3454} 3455 3456// === floor(log2(x)) =============================================== 3457// 3458// Equivalent to (31 - clz(x)) for x > 0. Returns -1 for x <= 0. 3459 3460func nx_bits_floor_log2(x: i64) -> i64 { 3461 if x <= 0 { return -1 } 3462 if x <= 0xFFFFFFFF { 3463 return 31 - nx_bits_clz32(x) 3464 } 3465 // High 32 bits set: 32 + log2(x >> 32) 3466 return 63 - nx_bits_clz32(x >> 32) 3467} 3468 3469// === bit-field extract (BMI BEXTR semantics) ====================== 3470// 3471// Extract `len` bits starting at `start` from x. 3472// Equivalent to (x >> start) & ((1 << len) - 1). 3473 3474func nx_bits_bextr(x: i64, start: i64, len: i64) -> i64 { 3475 if len <= 0 { return 0 } 3476 if len >= 64 { return x >> start } 3477 let mask: i64 = (1 << len) - 1 3478 return (x >> start) & mask 3479} 3480 3481const K_MAGIC_536870912: i64 = 536870912 3482 3483struct Sha256 { 3484 // Hash state H[0..7] as i64 (low 32 bits used). 3485 h0: i64, h1: i64, h2: i64, h3: i64, 3486 h4: i64, h5: i64, h6: i64, h7: i64, 3487 3488 // Legacy byte-packed block fields (retained for struct-size 3489 // compatibility; no longer the active buffer -- see bufptr below). 3490 b0: i64, b1: i64, b2: i64, b3: i64, 3491 b4: i64, b5: i64, b6: i64, b7: i64, 3492 3493 // Byte index into the 64-byte block (0..63) and total bits 3494 // processed (for final padding). 3495 idx: i64, 3496 bit_len: i64, 3497 3498 // Scratch buffers allocated ONCE per context in sha256_init (the perf rewrite, 3499 // 2026-06-10: kills the per-block mmap syscall + the per-round K if-chain + 3500 // the linear-scan byte access). ALL three are indexed only by PUBLIC counters 3501 // (byte position / round number 0..63 / schedule index) -- never by secret data -- 3502 // so constant_time_by_construction is preserved (no secret-indexed table access). 3503 bufptr: i64, // -> 64-byte contiguous block buffer (O(1) byte access) 3504 kptr: i64, // -> 64 round constants, materialized once from sha256_k() 3505 wptr: i64, // -> 64-word message schedule, reused every block 3506 3507 // Hardware SHA-NI scratch (the perf path, 2026-07-02). Allocated once per ctx. 3508 // k32ptr -> 64 CONTIGUOUS i32 round constants (the SHA-NI intrinsic wants packed 32-bit 3509 // K, whereas kptr above is 64 i64 for the software loop). st8ptr -> 8 CONTIGUOUS i32 3510 // working state a..h, marshalled from h0..h7 around each __sha256_ni_block call. ni_ok 3511 // caches the CPUID SHA-feature probe (1=use hardware, 0=software fallback) so cpuid runs 3512 // once per hash, not once per block. All three are indexed only by PUBLIC counters -> 3513 // constant_time_by_construction is preserved. 3514 k32ptr: i64, // -> 64 i32 round constants (packed), for the SHA-NI intrinsic 3515 st8ptr: i64, // -> 8 i32 working state a..h, marshalled around __sha256_ni_block 3516 ni_ok: i64, // 1 = CPU has SHA-NI (cpuid(7,0):EBX bit-29) -> hardware compress; 0 = software 3517} 3518 3519// Mask utilities. 3520const M32: i64 = 0xFFFFFFFF 3521 3522// Delegated to nx_bits_rotr32. SHA-256 round does 6 rotates per 3523// word * 64 rounds = 384 rotates per block. 3524func rotr32(x: i64, n: i64) -> i64 { 3525 return nx_bits_rotr32(x, n) 3526} 3527 3528func shr32(x: i64, n: i64) -> i64 { 3529 return (x >> n) & M32 3530} 3531 3532// SHA-256 round constants K[0..63]. Standard cube-roots-of-primes. 3533// We encode as a simple index -> constant lookup; each returns the 3534// i64 with the 32-bit constant in the low bits. 3535func sha256_k(i: i64) -> i64 { 3536 if i == 0 { return 0x428a2f98 } 3537 if i == 1 { return 0x71374491 } 3538 if i == 2 { return 0xb5c0fbcf } 3539 if i == 3 { return 0xe9b5dba5 } 3540 if i == 4 { return 0x3956c25b } 3541 if i == 5 { return 0x59f111f1 } 3542 if i == 6 { return 0x923f82a4 } 3543 if i == 7 { return 0xab1c5ed5 } 3544 if i == 8 { return 0xd807aa98 } 3545 if i == 9 { return 0x12835b01 } 3546 if i == 10 { return 0x243185be } 3547 if i == 11 { return 0x550c7dc3 } 3548 if i == 12 { return 0x72be5d74 } 3549 if i == 13 { return 0x80deb1fe } 3550 if i == 14 { return 0x9bdc06a7 } 3551 if i == 15 { return 0xc19bf174 } 3552 if i == 16 { return 0xe49b69c1 } 3553 if i == 17 { return 0xefbe4786 } 3554 if i == 18 { return 0x0fc19dc6 } 3555 if i == 19 { return 0x240ca1cc } 3556 if i == 20 { return 0x2de92c6f } 3557 if i == 21 { return 0x4a7484aa } 3558 if i == 22 { return 0x5cb0a9dc } 3559 if i == 23 { return 0x76f988da } 3560 if i == 24 { return 0x983e5152 } 3561 if i == 25 { return 0xa831c66d } 3562 if i == 26 { return 0xb00327c8 } 3563 if i == 27 { return 0xbf597fc7 } 3564 if i == 28 { return 0xc6e00bf3 } 3565 if i == 29 { return 0xd5a79147 } 3566 if i == 30 { return 0x06ca6351 } 3567 if i == 31 { return 0x14292967 } 3568 if i == 32 { return 0x27b70a85 } 3569 if i == 33 { return 0x2e1b2138 } 3570 if i == 34 { return 0x4d2c6dfc } 3571 if i == 35 { return 0x53380d13 } 3572 if i == 36 { return 0x650a7354 } 3573 if i == 37 { return 0x766a0abb } 3574 if i == 38 { return 0x81c2c92e } 3575 if i == 39 { return 0x92722c85 } 3576 if i == 40 { return 0xa2bfe8a1 } 3577 if i == 41 { return 0xa81a664b } 3578 if i == 42 { return 0xc24b8b70 } 3579 if i == 43 { return 0xc76c51a3 } 3580 if i == 44 { return 0xd192e819 } 3581 if i == 45 { return 0xd6990624 } 3582 if i == 46 { return 0xf40e3585 } 3583 if i == 47 { return 0x106aa070 } 3584 if i == 48 { return 0x19a4c116 } 3585 if i == 49 { return 0x1e376c08 } 3586 if i == 50 { return 0x2748774c } 3587 if i == 51 { return 0x34b0bcb5 } 3588 if i == 52 { return 0x391c0cb3 } 3589 if i == 53 { return 0x4ed8aa4a } 3590 if i == 54 { return 0x5b9cca4f } 3591 if i == 55 { return 0x682e6ff3 } 3592 if i == 56 { return 0x748f82ee } 3593 if i == 57 { return 0x78a5636f } 3594 if i == 58 { return 0x84c87814 } 3595 if i == 59 { return 0x8cc70208 } 3596 if i == 60 { return 0x90befffa } 3597 if i == 61 { return 0xa4506ceb } 3598 if i == 62 { return 0xbef9a3f7 } 3599 if i == 63 { return 0xc67178f2 } 3600 return 0 3601} 3602 3603// Access byte n (0..63) of the current block buffer. O(1) -- the buffer is contiguous 3604// (was a linear scan over 8 byte-packed fields per access; n is a public position). 3605func blk_byte(c: *Sha256, n: i64) -> i64 { 3606 let p: *u8 = c.bufptr as *u8 3607 return p[n] as i64 3608} 3609 3610// Set byte n (0..63) of the current block buffer. O(1) contiguous store. 3611func blk_set_byte(c: *Sha256, n: i64, v: i64) -> i64 { 3612 let p: *u8 = c.bufptr as *u8 3613 p[n] = (v & 0xFF) as u8 3614 return 0 3615} 3616 3617// Pack bytes [4*i .. 4*i+4) of the current block into a 32-bit 3618// big-endian word (SHA-256 spec is big-endian). 3619func blk_word(c: *Sha256, i: i64) -> i64 { 3620 let off: i64 = i * 4 3621 let b0: i64 = blk_byte(c, off + 0) 3622 let b1: i64 = blk_byte(c, off + 1) 3623 let b2: i64 = blk_byte(c, off + 2) 3624 let b3: i64 = blk_byte(c, off + 3) 3625 return ((b0 << 24) | (b1 << 16) | (b2 << 8) | b3) & M32 3626} 3627 3628// Hardware SHA-NI block compression: marshal the working state h0..h7 into the 8-word i32 3629// buffer, run one full SHA-256 block via the fused __sha256_ni_block intrinsic (which reads 3630// the 64 raw big-endian bytes at bufptr and the packed i32 K table), marshal the updated 3631// state back. Bit-identical to sha256_compress_sw (validated by nx_shani_block_probe against 3632// the software oracle for many blocks + the NIST KAT). ~hardware speed vs the ~40 MB/s soft path. 3633func sha256_compress_ni(c: *Sha256) -> i64 { 3634 let st: *i32 = c.st8ptr as *i32 3635 st[0] = (c.h0 & M32) as i32; st[1] = (c.h1 & M32) as i32 3636 st[2] = (c.h2 & M32) as i32; st[3] = (c.h3 & M32) as i32 3637 st[4] = (c.h4 & M32) as i32; st[5] = (c.h5 & M32) as i32 3638 st[6] = (c.h6 & M32) as i32; st[7] = (c.h7 & M32) as i32 3639 let _r: i64 = __sha256_ni_block(c.st8ptr as *u8, c.bufptr as *u8, c.k32ptr as *u8) 3640 c.h0 = (st[0] as i64) & M32; c.h1 = (st[1] as i64) & M32 3641 c.h2 = (st[2] as i64) & M32; c.h3 = (st[3] as i64) & M32 3642 c.h4 = (st[4] as i64) & M32; c.h5 = (st[5] as i64) & M32 3643 c.h6 = (st[6] as i64) & M32; c.h7 = (st[7] as i64) & M32 3644 return 0 3645} 3646 3647// MULTI-BLOCK SHA-NI (2026-07-02, organ-level, NO new intrinsic): marshal state -> i32 buffer ONCE, 3648// run __sha256_ni_block over `nblk` consecutive 64-byte blocks read DIRECTLY from `blocks` (state 3649// stays resident in st8ptr between calls), marshal back ONCE. Eliminates the per-block marshal AND 3650// the per-byte blk_set_byte buffering of the byte-at-a-time path -> the SHA-NI GB/s lever. Each 3651// __sha256_ni_block is the identical proven compression -> bit-identical to N separate compresses. 3652func sha256_compress_ni_blocks(c: *Sha256, blocks: i64, nblk: i64) -> i64 { 3653 let st: *i32 = c.st8ptr as *i32 3654 st[0] = (c.h0 & M32) as i32; st[1] = (c.h1 & M32) as i32 3655 st[2] = (c.h2 & M32) as i32; st[3] = (c.h3 & M32) as i32 3656 st[4] = (c.h4 & M32) as i32; st[5] = (c.h5 & M32) as i32 3657 st[6] = (c.h6 & M32) as i32; st[7] = (c.h7 & M32) as i32 3658 var b: i64 = 0 3659 while b < nblk { 3660 let blkp: i64 = blocks + b * 64 3661 let _r: i64 = __sha256_ni_block(c.st8ptr as *u8, blkp as *u8, c.k32ptr as *u8) 3662 b = b + 1 3663 } 3664 c.h0 = (st[0] as i64) & M32; c.h1 = (st[1] as i64) & M32 3665 c.h2 = (st[2] as i64) & M32; c.h3 = (st[3] as i64) & M32 3666 c.h4 = (st[4] as i64) & M32; c.h5 = (st[5] as i64) & M32 3667 c.h6 = (st[6] as i64) & M32; c.h7 = (st[7] as i64) & M32 3668 return 0 3669} 3670 3671// One compression function call: process the 64 bytes currently in the block buffer. 3672// Mutates c.h0..c.h7. Routes to hardware SHA-NI when the CPU supports it (probed once in 3673// sha256_init -> c.ni_ok); the pure-integer software path below stays the ORACLE/fallback. 3674func sha256_compress(c: *Sha256) -> i64 { 3675 if c.ni_ok == 1 { return sha256_compress_ni(c) } 3676 // Message schedule W[0..63] + round constants K[0..63] -- both per-ctx scratch 3677 // (allocated once in sha256_init), so no per-block mmap syscall and no K if-chain. 3678 let w: *i64 = c.wptr as *i64 3679 let k: *i64 = c.kptr as *i64 3680 var i: i64 = 0 3681 while i < 16 { 3682 w[i] = blk_word(c, i) 3683 i = i + 1 3684 } 3685 i = 16 3686 while i < 64 { 3687 let x15: i64 = w[i - 15] 3688 let x2: i64 = w[i - 2] 3689 // sigma0/sigma1 with the rotates inlined (was 4 rotr32 calls/iter -> pure arithmetic) 3690 let s0: i64 = (((x15 >> 7) | (x15 << 25)) ^ ((x15 >> 18) | (x15 << 14)) ^ (x15 >> 3)) & M32 3691 let s1: i64 = (((x2 >> 17) | (x2 << 15)) ^ ((x2 >> 19) | (x2 << 13)) ^ (x2 >> 10)) & M32 3692 w[i] = (w[i - 16] + s0 + w[i - 7] + s1) & M32 3693 i = i + 1 3694 } 3695 var a: i64 = c.h0 3696 var b: i64 = c.h1 3697 var cc: i64 = c.h2 3698 var d: i64 = c.h3 3699 var e: i64 = c.h4 3700 var ff: i64 = c.h5 3701 var g: i64 = c.h6 3702 var h: i64 = c.h7 3703 i = 0 3704 while i < 64 { 3705 // Sigma1(e), Sigma0(a) with rotates inlined (was 6 rotr32 calls/round) 3706 let S1: i64 = (((e >> 6) | (e << 26)) ^ ((e >> 11) | (e << 21)) ^ ((e >> 25) | (e << 7))) & M32 3707 let ch: i64 = ((e & ff) ^ ((e ^ M32) & g)) & M32 3708 let t1: i64 = (h + S1 + ch + k[i] + w[i]) & M32 3709 let S0: i64 = (((a >> 2) | (a << 30)) ^ ((a >> 13) | (a << 19)) ^ ((a >> 22) | (a << 10))) & M32 3710 let mj: i64 = ((a & b) ^ (a & cc) ^ (b & cc)) & M32 3711 let t2: i64 = (S0 + mj) & M32 3712 h = g 3713 g = ff 3714 ff = e 3715 e = (d + t1) & M32 3716 d = cc 3717 cc = b 3718 b = a 3719 a = (t1 + t2) & M32 3720 i = i + 1 3721 } 3722 c.h0 = (c.h0 + a) & M32 3723 c.h1 = (c.h1 + b) & M32 3724 c.h2 = (c.h2 + cc) & M32 3725 c.h3 = (c.h3 + d) & M32 3726 c.h4 = (c.h4 + e) & M32 3727 c.h5 = (c.h5 + ff) & M32 3728 c.h6 = (c.h6 + g) & M32 3729 c.h7 = (c.h7 + h) & M32 3730 return 0 3731} 3732 3733// Initialise state. H[0..7] values from FIPS 180-4 section 5.3.3 3734// (first 32 bits of fractional parts of square roots of first 8 3735// primes). 3736// Shared allocation-free initializer: all five scratch pointers are supplied by the owning path. 3737func sha256_seed_allocated(c: *Sha256) -> i64 { 3738 c.h0 = 0x6a09e667; c.h1 = 0xbb67ae85; c.h2 = 0x3c6ef372; c.h3 = 0xa54ff53a 3739 c.h4 = 0x510e527f; c.h5 = 0x9b05688c; c.h6 = 0x1f83d9ab; c.h7 = 0x5be0cd19 3740 // Per-ctx scratch, allocated once (amortized over every block of this hash): 3741 // Materialize the canonical K table once (sha256_k stays the single source of the 3742 // constants -- DRY; the if-chain now runs 64x per HASH, not 64x per BLOCK). 3743 let kp: *i64 = c.kptr as *i64 3744 var i: i64 = 0 3745 while i < 64 { kp[i] = sha256_k(i); i = i + 1 } 3746 let bp: *u8 = c.bufptr as *u8 3747 i = 0 3748 while i < 64 { bp[i] = 0 as u8; i = i + 1 } 3749 c.idx = 0 3750 c.bit_len = 0 3751 3752 // ---- Hardware SHA-NI setup (additive; software path is the oracle/fallback) ---- 3753 // Packed i32 K table for the intrinsic + an 8-word i32 state marshalling buffer. 3754 let k32: *i32 = c.k32ptr as *i32 3755 i = 0 3756 while i < 64 { k32[i] = (sha256_k(i) & M32) as i32; i = i + 1 } 3757 // Probe CPU SHA support ONCE per context: cpuid(leaf=7, subleaf=0):EBX bit-29 = SHA. 3758 // 1<<29 = 0x20000000 = 536870912. Gate the compress path on this; a CPU without SHA-NI 3759 // transparently uses the software compression (byte-identical result, just slower). 3760 if (__cpuid_ebx(7, 0) & K_MAGIC_536870912) != 0 { c.ni_ok = 1 } else { c.ni_ok = 0 } 3761 return 0 3762} 3763 3764func sha256_init(c: *Sha256) -> i64 { 3765 c.bufptr = sys_mmap(64) as i64 3766 c.kptr = sys_mmap(64 * 8) as i64 3767 c.wptr = sys_mmap(64 * 8) as i64 3768 c.k32ptr = sys_mmap(64 * 4) as i64 3769 c.st8ptr = sys_mmap(8 * 4) as i64 3770 return sha256_seed_allocated(c) 3771} 3772 3773// Feed `n` bytes. Buffers partial blocks; compresses full blocks 3774// as soon as they fill. 3775func sha256_update(c: *Sha256, bytes: *u8, n: i64) -> i64 { 3776 var i: i64 = 0 3777 // BULK FAST PATH: when block-aligned (idx==0) and SHA-NI is available, process all full 64-byte 3778 // blocks straight from the input via the resident-state multi-block compress -- skipping both the 3779 // byte-at-a-time blk_set_byte buffering and the per-block state marshalling. Bit-identical. 3780 if c.idx == 0 { 3781 if c.ni_ok == 1 { 3782 let nblk: i64 = n / 64 3783 if nblk > 0 { 3784 sha256_compress_ni_blocks(c, (bytes as i64) + i, nblk) 3785 c.bit_len = c.bit_len + nblk * 512 3786 i = i + nblk * 64 3787 } 3788 } 3789 } 3790 while i < n { 3791 blk_set_byte(c, c.idx, bytes[i]) 3792 c.idx = c.idx + 1 3793 c.bit_len = c.bit_len + 8 3794 if c.idx == 64 { 3795 sha256_compress(c) 3796 c.idx = 0 3797 } 3798 i = i + 1 3799 } 3800 return 0 3801} 3802 3803// Finalise: append 0x80, pad with zeros, append 8-byte bit length, 3804// then do one or two final compressions. Writes 32 bytes to `out`. 3805func sha256_final(c: *Sha256, out: *u8) -> i64 { 3806 // Remember total bit length before padding. 3807 let total_bits: i64 = c.bit_len 3808 // Append 0x80. 3809 blk_set_byte(c, c.idx, 0x80) 3810 c.idx = c.idx + 1 3811 // If not enough room for 8-byte length in this block, pad rest 3812 // with zeros + compress. 3813 if c.idx > 56 { 3814 while c.idx < 64 { 3815 blk_set_byte(c, c.idx, 0) 3816 c.idx = c.idx + 1 3817 } 3818 sha256_compress(c) 3819 c.idx = 0 3820 } 3821 // Pad zeros up to byte 56. 3822 while c.idx < 56 { 3823 blk_set_byte(c, c.idx, 0) 3824 c.idx = c.idx + 1 3825 } 3826 // Write 64-bit big-endian length in bytes 56..63. 3827 blk_set_byte(c, 56, (total_bits >> 56) & 0xFF) 3828 blk_set_byte(c, 57, (total_bits >> 48) & 0xFF) 3829 blk_set_byte(c, 58, (total_bits >> 40) & 0xFF) 3830 blk_set_byte(c, 59, (total_bits >> 32) & 0xFF) 3831 blk_set_byte(c, 60, (total_bits >> 24) & 0xFF) 3832 blk_set_byte(c, 61, (total_bits >> 16) & 0xFF) 3833 blk_set_byte(c, 62, (total_bits >> 8) & 0xFF) 3834 blk_set_byte(c, 63, total_bits & 0xFF) 3835 sha256_compress(c) 3836 // Emit H[0..7] as big-endian 4-byte words. 3837 out[0] = (c.h0 >> 24) & 0xFF 3838 out[1] = (c.h0 >> 16) & 0xFF 3839 out[2] = (c.h0 >> 8) & 0xFF 3840 out[3] = c.h0 & 0xFF 3841 out[4] = (c.h1 >> 24) & 0xFF 3842 out[5] = (c.h1 >> 16) & 0xFF 3843 out[6] = (c.h1 >> 8) & 0xFF 3844 out[7] = c.h1 & 0xFF 3845 out[8] = (c.h2 >> 24) & 0xFF 3846 out[9] = (c.h2 >> 16) & 0xFF 3847 out[10] = (c.h2 >> 8) & 0xFF 3848 out[11] = c.h2 & 0xFF 3849 out[12] = (c.h3 >> 24) & 0xFF 3850 out[13] = (c.h3 >> 16) & 0xFF 3851 out[14] = (c.h3 >> 8) & 0xFF 3852 out[15] = c.h3 & 0xFF 3853 out[16] = (c.h4 >> 24) & 0xFF 3854 out[17] = (c.h4 >> 16) & 0xFF 3855 out[18] = (c.h4 >> 8) & 0xFF 3856 out[19] = c.h4 & 0xFF 3857 out[20] = (c.h5 >> 24) & 0xFF 3858 out[21] = (c.h5 >> 16) & 0xFF 3859 out[22] = (c.h5 >> 8) & 0xFF 3860 out[23] = c.h5 & 0xFF 3861 out[24] = (c.h6 >> 24) & 0xFF 3862 out[25] = (c.h6 >> 16) & 0xFF 3863 out[26] = (c.h6 >> 8) & 0xFF 3864 out[27] = c.h6 & 0xFF 3865 out[28] = (c.h7 >> 24) & 0xFF 3866 out[29] = (c.h7 >> 16) & 0xFF 3867 out[30] = (c.h7 >> 8) & 0xFF 3868 out[31] = c.h7 & 0xFF 3869 return 0 3870} 3871 3872// One-shot: hash `n` bytes, write 32-byte digest to `out`. 3873// Release only scratch owned by this initialized context; the caller owns c. 3874// Reset pointers so explicit cleanup is safe to repeat after completion/failure. 3875func sha256_destroy(c: *Sha256) -> i64 { 3876 if c.bufptr!=0 { sys_munmap(c.bufptr as *u8,64);c.bufptr=0 } 3877 if c.kptr!=0 { sys_munmap(c.kptr as *u8,64*8);c.kptr=0 } 3878 if c.wptr!=0 { sys_munmap(c.wptr as *u8,64*8);c.wptr=0 } 3879 if c.k32ptr!=0 { sys_munmap(c.k32ptr as *u8,64*4);c.k32ptr=0 } 3880 if c.st8ptr!=0 { sys_munmap(c.st8ptr as *u8,8*4);c.st8ptr=0 } 3881 return 0 3882} 3883 3884func sha256_digest(bytes: *u8, n: i64, out: *u8) -> i64 { 3885 let ctx_raw: *u8 = sys_mmap(__size_of(Sha256)) 3886 let ctx: *Sha256 = ctx_raw as *Sha256 3887 sha256_init(ctx) 3888 sha256_update(ctx, bytes, n) 3889 sha256_final(ctx, out) 3890 sha256_destroy(ctx) 3891 sys_munmap(ctx_raw,__size_of(Sha256)) 3892 return 0 3893} 3894 3895// Native Linux x86-64 checked observation path using the existing shared allocator owner. 3896// No cross-backend portability claim: sys_munmap currently uses the native x86-64 release ABI. 3897// Synchronous caller-owned scratch: do not publish it or pass it to forked children. 3898const SHA256_WORD_ALIGN: i64 = 8 3899const SHA256_BLOCK_BYTES: i64 = 64 3900const SHA256_ROUND_WORDS: i64 = 64 3901const SHA256_WIDE_WORD: i64 = 8 3902const SHA256_PACKED_WORD: i64 = 4 3903const SHA256_STATE_WORDS: i64 = 8 3904const SHA256_DIGEST_BYTES: i64 = 32 3905const SHA256_SIGNED_MAX: i64 = 9223372036854775807 3906const SHA256_BITS_PER_BYTE: i64 = 8 3907const SHA256_E_INPUT: i64 = 0-1 3908const SHA256_E_WORKSPACE: i64 = 0-2 3909const SHA256_E_MAPPING: i64 = 0-3 3910const SHA256_E_RELEASE: i64 = 0-4 3911 3912func sha256_context_aligned_bytes() -> i64 { 3913 return ((__size_of(Sha256)+SHA256_WORD_ALIGN-1)/SHA256_WORD_ALIGN)*SHA256_WORD_ALIGN 3914} 3915func sha256_workspace_bytes() -> i64 { 3916 return sha256_context_aligned_bytes()+SHA256_BLOCK_BYTES+2*SHA256_ROUND_WORDS*SHA256_WIDE_WORD+SHA256_ROUND_WORDS*SHA256_PACKED_WORD+SHA256_STATE_WORDS*SHA256_PACKED_WORD 3917} 3918func sha256_checked_input(bytes: *u8, n: i64, out: *u8) -> i64 { 3919 if n < 0 || n > SHA256_SIGNED_MAX/SHA256_BITS_PER_BYTE { return 0 } 3920 let source: i64=bytes as i64; let target: i64=out as i64 3921 if source < 0 || (n > 0 && source == 0) || source > SHA256_SIGNED_MAX-n { return 0 } 3922 if target <= 0 || target > SHA256_SIGNED_MAX-SHA256_DIGEST_BYTES { return 0 } 3923 return 1 3924} 3925func sha256_ranges_overlap(a: i64, an: i64, b: i64, bn: i64) -> i64 { 3926 if an == 0 || bn == 0 { return 0 }; return a < b+bn && b < a+an 3927} 3928// Borrowed scratch. Never call sha256_destroy: the buffers share one allocation. 3929// No allocation/release occurs here. Refused boundary inputs leave output unchanged. 3930// Initialize caller-owned scratch for incremental update/final; never call destroy on it. 3931func sha256_init_workspace(workspace: *u8, capacity: i64) -> i64 { 3932 let base: i64=workspace as i64; let needed: i64=sha256_workspace_bytes() 3933 if base <= 0 || capacity < needed || base > SHA256_SIGNED_MAX-needed || base%SHA256_WORD_ALIGN != 0 { return SHA256_E_WORKSPACE } 3934 let ctx: *Sha256=workspace as *Sha256; var p: i64=base+sha256_context_aligned_bytes() 3935 ctx.bufptr=p; p=p+SHA256_BLOCK_BYTES 3936 ctx.kptr=p; p=p+SHA256_ROUND_WORDS*SHA256_WIDE_WORD 3937 ctx.wptr=p; p=p+SHA256_ROUND_WORDS*SHA256_WIDE_WORD 3938 ctx.k32ptr=p; p=p+SHA256_ROUND_WORDS*SHA256_PACKED_WORD 3939 ctx.st8ptr=p 3940 return sha256_seed_allocated(ctx) 3941} 3942func sha256_digest_workspace(bytes: *u8, n: i64, out: *u8, workspace: *u8, capacity: i64) -> i64 { 3943 if sha256_checked_input(bytes,n,out) != 1 { return SHA256_E_INPUT } 3944 let base: i64=workspace as i64; let needed: i64=sha256_workspace_bytes() 3945 if base <= 0 || capacity < needed || base > SHA256_SIGNED_MAX-needed || base%SHA256_WORD_ALIGN != 0 { return SHA256_E_WORKSPACE } 3946 if sha256_ranges_overlap(base,needed,bytes as i64,n) == 1 || sha256_ranges_overlap(base,needed,out as i64,SHA256_DIGEST_BYTES) == 1 { return SHA256_E_WORKSPACE } 3947 let initialized:i64=sha256_init_workspace(workspace,capacity) 3948 if initialized != 0 { return initialized } 3949 let ctx:*Sha256=workspace as *Sha256 3950 sha256_update(ctx,bytes,n); sha256_final(ctx,out) 3951 return 0 3952} 3953// Takes ownership of an actual whole sys_mmap_shared(workspace_bytes()) result. 3954// A failed mapping leaves output unchanged. A release failure may follow computed output; 3955// callers must accept output only on0. Never supply an arena pointer or undersized mapping. 3956func sha256_digest_mapping_native(bytes: *u8, n: i64, out: *u8, mapping: i64) -> i64 { 3957 if mapping <= 0 { return SHA256_E_MAPPING } 3958 let size: i64=sha256_workspace_bytes() 3959 let result: i64=sha256_digest_workspace(bytes,n,out,mapping as *u8,size) 3960 let released: i64=sys_munmap(mapping as *u8,size) 3961 if result != 0 { return result } 3962 if released != 0 { return SHA256_E_RELEASE } 3963 return 0 3964} 3965func sha256_digest_checked_native(bytes: *u8, n: i64, out: *u8) -> i64 { 3966 if sha256_checked_input(bytes,n,out) != 1 { return SHA256_E_INPUT } 3967 // The existing shared wrapper returns errno; sys_mmap's failure policy is fatal. 3968 let mapping: i64=sys_mmap_shared(sha256_workspace_bytes()) as i64 3969 return sha256_digest_mapping_native(bytes,n,out,mapping) 3970} 3971 3972 3973// Linux syscall ABI results, not admission or retry policy. 3974const FIO_EINTR: i64 = 0 - 4 3975const FIO_EIO: i64 = 0 - 5 3976const FIO_EINVAL: i64 = 0 - 22 3977const FIO_EEXIST: i64 = 0 - 17 3978const FIO_EBADMSG: i64 = 0 - 74 // Linux ABI: artifact digest mismatch. 3979 3980struct NxFileWriteResult { 3981 stage: *u8, 3982 code: i64, 3983 written: i64, 3984 close_code: i64 3985} 3986 3987// Owns fd until close. Preserve the first failure and the independent close 3988// result; never retry close because Linux may already have released the fd. 3989func fio_write_sync_fd(fd: i64, body: *u8, n: i64, result: *NxFileWriteResult) -> i64 { 3990 result.stage = "write" as *u8 3991 result.code = 0 3992 result.written = 0 3993 result.close_code = 0 3994 if n < 0 || ((body as i64) == 0 && n > 0) { 3995 result.stage = "input" as *u8 3996 result.code = FIO_EINVAL 3997 } 3998 while result.code == 0 && result.written < n { 3999 let w: i64 = sys_write(fd, body + result.written, n - result.written) 4000 if w == FIO_EINTR { continue } 4001 if w < 0 { result.code = w; break } 4002 if w == 0 { result.code = FIO_EIO; break } 4003 result.written = result.written + w 4004 } 4005 if result.code == 0 { 4006 result.stage = "fsync" as *u8 4007 var synced: i64 = sys_fsync(fd) 4008 while synced == FIO_EINTR { synced = sys_fsync(fd) } 4009 result.code = synced 4010 } 4011 result.close_code = sys_close(fd) 4012 if result.code == 0 { 4013 result.stage = "close" as *u8 4014 result.code = result.close_code 4015 } 4016 if result.code == 0 { result.stage = "complete" as *u8 } 4017 return result.code 4018} 4019 4020// Persist the directory entry after rename. A failure here occurs after the 4021// visible update: the caller must retain that publication state in its receipt. 4022func fio_sync_parent(path: *u8, result: *NxFileWriteResult) -> i64 { 4023 var length: i64=0 4024 var slash: i64=0-1 4025 while path[length]!=(0 as u8) { if path[length]==(47 as u8) { slash=length }; length=length+1 } 4026 let parent: *u8=sys_mmap(length+2) 4027 if slash<0 { parent[0]=46 as u8; parent[1]=0 as u8 } 4028 else { 4029 var end: i64=slash 4030 if end==0 { end=1 } 4031 var i: i64=0 4032 while i<end { parent[i]=path[i]; i=i+1 } 4033 parent[end]=0 as u8 4034 } 4035 result.stage="directory-open" as *u8 4036 result.close_code=0 4037 let fd: i64=sys_openat_directory(parent) 4038 sys_munmap(parent,length+2) 4039 if fd<0 { result.code=fd; return fd } 4040 result.stage="directory-fsync" as *u8 4041 var synced: i64=sys_fsync(fd) 4042 while synced==FIO_EINTR { synced=sys_fsync(fd) } 4043 result.close_code=sys_close(fd) 4044 result.code=synced 4045 if synced==0 { result.stage="directory-close" as *u8; result.code=result.close_code } 4046 if result.code==0 { result.stage="complete" as *u8 } 4047 return result.code 4048} 4049 4050// delete a file (unlinkat). Returns 0 on success. 4051func fio_unlink(path: *u8) -> i64 { return __syscall(263, 0 - 100, path as i64, 0, 0, 0, 0) } 4052 4053// 1 if `path` exists (fstatat succeeds), else 0. 4054func fio_exists(path: *u8) -> i64 { 4055 let st: *u8 = sys_mmap(160) 4056 if sys_fstatat(path, st) == 0 { return 1 } 4057 return 0 4058} 4059 4060// A caller-owned, single-use read session. Initialize once before open; never 4061// reinitialize an open session. Atomic pathname replacement does not change its fd. 4062struct NxFileReadRegion { 4063 fd: i64, 4064 total: i64, 4065 start: i64, 4066 length: i64, 4067 read_bytes: i64, 4068 last_read: i64, 4069 stage: *u8, 4070 code: i64, 4071 close_code: i64, 4072} 4073func fio_region_init(r: *NxFileReadRegion) -> i64 { 4074 r.fd=0-1;r.total=0;r.start=0;r.length=0;r.read_bytes=0;r.last_read=0 4075 r.stage="initialized";r.code=0;r.close_code=0 4076 return 0 4077} 4078// Linux close consumes ownership even when it reports an error; do not retry it. 4079func fio_region_close(r: *NxFileReadRegion) -> i64 { 4080 if r.fd>=0 { 4081 let fd: i64=r.fd;r.fd=0-1 4082 r.close_code=sys_close(fd) 4083 if r.code==0 && r.close_code<0 { r.code=r.close_code;r.stage="close" } 4084 } 4085 return r.code 4086} 4087func fio_region_fail(r: *NxFileReadRegion,stage: *u8,code: i64) -> i64 { 4088 r.stage=stage;r.code=code 4089 fio_region_close(r) 4090 return code 4091} 4092func fio_region_open(path: *u8,r: *NxFileReadRegion) -> i64 { 4093 if r.fd>=0 { return FIO_EEXIST } 4094 fio_region_init(r) 4095 if (path as i64)==0 { return fio_region_fail(r,"path",FIO_EINVAL) } 4096 if path[0]==(0 as u8) { return fio_region_fail(r,"path",FIO_EINVAL) } 4097 r.stage="open";r.fd=sys_openat_rd(path) 4098 if r.fd<0 { r.code=r.fd;return r.code } 4099 // SEEK_END/SEEK_SET are platform ABI selectors, not transfer-size policy. 4100 let size: i64=sys_lseek(r.fd,0,2) 4101 if size<0 { return fio_region_fail(r,"size-seek",size) } 4102 r.total=size;r.length=size 4103 let back: i64=sys_lseek(r.fd,0,0) 4104 if back!=0 { if back<0 { return fio_region_fail(r,"initial-seek",back) };return fio_region_fail(r,"initial-seek",FIO_EIO) } 4105 r.stage="ready";return 0 4106} 4107// Bounds use subtraction, so start+length can never wrap before validation. 4108func fio_region_select(r: *NxFileReadRegion,start: i64,length: i64) -> i64 { 4109 if r.fd<0 || r.code!=0 || r.read_bytes!=0 { return FIO_EINVAL } 4110 if start<0 || length<0 || start>r.total { return FIO_EINVAL } 4111 if length>r.total-start { return FIO_EINVAL } 4112 let at: i64=sys_lseek(r.fd,start,0) 4113 if at!=start { if at<0 { return fio_region_fail(r,"region-seek",at) };return fio_region_fail(r,"region-seek",FIO_EIO) } 4114 r.start=start;r.length=length;r.stage="ready";return 0 4115} 4116// The caller supplies its reusable transport buffer. No allocation depends on 4117// file size; each read is at most min(buffer capacity, remaining region bytes). 4118func fio_region_next(r: *NxFileReadRegion,out: *u8,cap: i64) -> i64 { 4119 r.last_read=0 4120 if r.code!=0 { return r.code } 4121 if r.fd<0 { if r.read_bytes==r.length { return 0 };return FIO_EINVAL } 4122 if cap<=0 || (out as i64)==0 { return FIO_EINVAL } 4123 let remaining: i64=r.length-r.read_bytes 4124 if remaining==0 { r.stage="complete";return fio_region_close(r) } 4125 var want: i64=remaining;if want>cap { want=cap } 4126 r.stage="read" 4127 while r.last_read<want { 4128 let got: i64=sys_read(r.fd,out+r.last_read,want-r.last_read) 4129 if got==FIO_EINTR { continue } 4130 if got<0 { return fio_region_fail(r,"read",got) } 4131 if got==0 { return fio_region_fail(r,"read-premature-eof",FIO_EIO) } 4132 r.last_read=r.last_read+got;r.read_bytes=r.read_bytes+got 4133 } 4134 if r.read_bytes==r.length { 4135 r.stage="complete" 4136 if fio_region_close(r)<0 { return r.code } 4137 } else { r.stage="ready" } 4138 return r.last_read 4139} 4140 4141 4142// Preparation never replaces a pathname. The caller owns an exclusive candidate 4143// path and keeps it for diagnosis on failure; publication is a separate operation. 4144struct NxFilePrepareResult { 4145 stage: *u8, 4146 code: i64, 4147 copied: i64, 4148 created: i64, 4149 source_close: i64, 4150 destination_close: i64, 4151 durable: i64, 4152} 4153func fio_prepare_copy(source: *u8,candidate: *u8,mode: i64,buffer: *u8,capacity: i64,out: *NxFilePrepareResult) -> i64 { 4154 out.stage="input";out.code=FIO_EINVAL;out.copied=0;out.created=0 4155 out.source_close=0;out.destination_close=0;out.durable=0 4156 if (source as i64)==0 || (candidate as i64)==0 || (buffer as i64)==0 || capacity<=0 { return out.code } 4157 if source[0]==(0 as u8) || candidate[0]==(0 as u8) || mode<0 || mode>0x1ff { return out.code } 4158 let input: *NxFileReadRegion=sys_mmap(__size_of(NxFileReadRegion)) as *NxFileReadRegion 4159 fio_region_init(input) 4160 out.code=fio_region_open(source,input);out.stage="source-open" 4161 var fd: i64=0-1 4162 if out.code==0 { 4163 out.stage="candidate-create" 4164 fd=sys_openat_exclusive(candidate,mode) 4165 if fd<0 { out.code=fd } else { out.created=1 } 4166 } 4167 while out.code==0 && input.read_bytes<input.length { 4168 let n: i64=fio_region_next(input,buffer,capacity) 4169 if n<0 { out.stage="source-read";out.code=n;break } 4170 var sent: i64=0 4171 out.stage="candidate-write" 4172 while sent<n { 4173 let w: i64=sys_write(fd,buffer+sent,n-sent) 4174 if w==FIO_EINTR { continue } 4175 if w<0 { out.code=w;break } 4176 if w==0 { out.code=FIO_EIO;break } 4177 sent=sent+w;out.copied=out.copied+w 4178 } 4179 } 4180 fio_region_close(input);out.source_close=input.close_code 4181 if out.code==0 && input.code!=0 { out.code=input.code;out.stage="source-close" } 4182 sys_munmap(input as *u8,__size_of(NxFileReadRegion)) 4183 if out.code==0 { 4184 out.stage="candidate-mode" 4185 out.code=nx_chmod(candidate,mode) 4186 } 4187 if out.code==0 { 4188 out.stage="candidate-fsync";out.code=sys_fsync(fd) 4189 while out.code==FIO_EINTR { out.code=sys_fsync(fd) } 4190 } 4191 if fd>=0 { 4192 out.destination_close=sys_close(fd) 4193 if out.code==0 && out.destination_close!=0 { out.code=out.destination_close;out.stage="candidate-close" } 4194 } 4195 if out.code==0 { 4196 let sync: *NxFileWriteResult=sys_mmap(__size_of(NxFileWriteResult)) as *NxFileWriteResult 4197 out.code=fio_sync_parent(candidate,sync) 4198 if out.code!=0 { out.stage=sync.stage } 4199 sys_munmap(sync as *u8,__size_of(NxFileWriteResult)) 4200 } 4201 if out.code==0 { out.stage="prepared";out.durable=1 } 4202 return out.code 4203} 4204 4205 4206struct NxFilePublishResult { 4207 stage: *u8, 4208 code: i64, 4209 visible: i64, 4210 durable: i64, 4211} 4212// Caller owns the prepared candidate and target's mutation lock. Rename failure 4213// leaves live intact; sync failure AFTER rename must retain visible=1. 4214func fio_publish_candidate(candidate: *u8,live: *u8,out: *NxFilePublishResult) -> i64 { 4215 out.stage="publish-input";out.code=FIO_EINVAL;out.visible=0;out.durable=0 4216 if (candidate as i64)==0 || (live as i64)==0 { return out.code } 4217 if candidate[0]==(0 as u8) || live[0]==(0 as u8) { return out.code } 4218 out.stage="publish-rename";out.code=sys_renameat(candidate,live) 4219 if out.code!=0 { return out.code } 4220 out.visible=1 4221 let sync: *NxFileWriteResult=sys_mmap(__size_of(NxFileWriteResult)) as *NxFileWriteResult 4222 out.stage="live-directory-sync";out.code=fio_sync_parent(live,sync) 4223 // Both directory entries change if preparation used another directory. 4224 if out.code==0 { out.stage="candidate-directory-sync";out.code=fio_sync_parent(candidate,sync) } 4225 sys_munmap(sync as *u8,__size_of(NxFileWriteResult)) 4226 if out.code==0 { out.stage="published";out.durable=1 } 4227 return out.code 4228} 4229 4230 4231// Stable lock inode: never unlink the lockfile. All cooperating callers must use 4232// the same canonical live pathname in an estate-owned directory. This excludes 4233// arbitrary writers and pathname aliases from the guarantee. 4234struct NxFileTargetLock { 4235 fd: i64, 4236 stage: *u8, 4237 code: i64, 4238 unlock_code: i64, 4239 close_code: i64, 4240} 4241func fio_target_lock_init(lock: *NxFileTargetLock) -> i64 { 4242 lock.fd=0-1;lock.stage="not-started";lock.code=0;lock.unlock_code=0;lock.close_code=0 4243 return 0 4244} 4245func fio_target_lock_release(lock: *NxFileTargetLock) -> i64 { 4246 if lock.fd>=0 { 4247 let fd: i64=lock.fd;lock.fd=0-1 4248 lock.unlock_code=sys_flock(fd,SYS_LOCK_UN) 4249 lock.close_code=sys_close(fd) 4250 if lock.code==0 && lock.unlock_code!=0 { lock.code=lock.unlock_code;lock.stage="target-unlock" } 4251 if lock.code==0 && lock.close_code!=0 { lock.code=lock.close_code;lock.stage="target-lock-close" } 4252 if lock.code==0 { lock.stage="released" } 4253 } 4254 return lock.code 4255} 4256func fio_target_lock_acquire(live: *u8,lock: *NxFileTargetLock) -> i64 { 4257 if lock.fd>=0 { return FIO_EEXIST } 4258 fio_target_lock_init(lock) 4259 lock.stage="target-lock-input";lock.code=FIO_EINVAL 4260 if (live as i64)==0 { return lock.code } 4261 var n: i64=0;while live[n]!=(0 as u8) { n=n+1 } 4262 if n==0 { return lock.code } 4263 let suffix: *u8=".install.lock" 4264 var extra: i64=0;while suffix[extra]!=(0 as u8) { extra=extra+1 } 4265 let bytes: i64=n+extra+1 4266 if bytes<=n { return lock.code } 4267 let path: *u8=sys_mmap(bytes) 4268 if (path as i64)<0 { lock.stage="target-lock-allocation";lock.code=path as i64;return lock.code } 4269 var i: i64=0;while i<n { path[i]=live[i];i=i+1 } 4270 i=0;while i<extra { path[n+i]=suffix[i];i=i+1 };path[n+extra]=0 as u8 4271 lock.stage="target-lock-open";lock.fd=sys_openat_lock(path) 4272 sys_munmap(path,bytes) 4273 if lock.fd<0 { lock.code=lock.fd;return lock.code } 4274 lock.stage="target-lock-acquire";lock.code=sys_flock(lock.fd,SYS_LOCK_EX | SYS_LOCK_NB) 4275 if lock.code!=0 { 4276 let fd: i64=lock.fd;lock.fd=0-1;lock.close_code=sys_close(fd) 4277 return lock.code 4278 } 4279 lock.stage="held";return 0 4280} 4281struct NxFileReplaceResult { 4282 stage: *u8, 4283 code: i64, 4284 candidate: NxFilePrepareResult, 4285 backup: NxFilePrepareResult, 4286 publication: NxFilePublishResult, 4287 lock: NxFileTargetLock, 4288} 4289// Internal locked body. Source remains an immutable artifact; history paths 4290// are exclusive. The public replacement entry points acquire the target lock. 4291func fio_replace_owned(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 { 4292 out.stage="prepare-candidate" 4293 out.backup.created=0;out.backup.durable=0;out.backup.copied=0;out.backup.code=0 4294 out.backup.stage="not-started";out.backup.source_close=0;out.backup.destination_close=0 4295 out.publication.stage="not-started";out.publication.code=0;out.publication.visible=0;out.publication.durable=0 4296 out.code=fio_prepare_copy(source,candidate,mode,buffer,capacity,&out.candidate) 4297 if out.code!=0 { return out.code } 4298 if (expected_candidate as i64)!=0 { 4299 out.stage="candidate-identity" 4300 out.code=fio_verify_sha256(candidate,expected_candidate,buffer,capacity) 4301 if out.code!=0 { return out.code } 4302 } 4303 out.stage="prepare-backup" 4304 out.code=fio_prepare_copy(live,backup,mode,buffer,capacity,&out.backup) 4305 if out.code!=0 { return out.code } 4306 if (expected_live as i64)!=0 { 4307 out.stage="live-identity" 4308 out.code=fio_verify_sha256(backup,expected_live,buffer,capacity) 4309 if out.code!=0 { return out.code } 4310 } 4311 out.stage="publish" 4312 out.code=fio_publish_candidate(candidate,live,&out.publication) 4313 if out.code==0 { out.stage="complete" } 4314 return out.code 4315} 4316 4317func fio_replace_init(out: *NxFileReplaceResult) -> i64 { 4318 let raw: *u8=out as *u8;var i: i64=0 4319 while i<__size_of(NxFileReplaceResult) { raw[i]=0 as u8;i=i+1 } 4320 out.stage="not-started";out.candidate.stage="not-started" 4321 out.backup.stage="not-started";out.publication.stage="not-started" 4322 fio_target_lock_init(&out.lock);return 0 4323} 4324func fio_replace_core(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 { 4325 fio_replace_init(out) 4326 out.code=fio_target_lock_acquire(live,&out.lock) 4327 if out.code!=0 { out.stage=out.lock.stage;return out.code } 4328 fio_replace_owned(source,live,candidate,backup,mode,buffer,capacity,expected_candidate,expected_live,out) 4329 let released: i64=fio_target_lock_release(&out.lock) 4330 if out.code==0 && released!=0 { out.code=released;out.stage=out.lock.stage } 4331 return out.code 4332} 4333func fio_replace_with_backup(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,out: *NxFileReplaceResult) -> i64 { 4334 return fio_replace_core(source,live,candidate,backup,mode,buffer,capacity,0 as *u8,0 as *u8,out) 4335} 4336 4337func fio_replace_verified(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 { 4338 if (expected_candidate as i64)==0 || (expected_live as i64)==0 { 4339 fio_replace_init(out) 4340 out.stage="identity-input";out.code=FIO_EINVAL 4341 out.candidate.stage="not-started";out.backup.stage="not-started";out.publication.stage="not-started" 4342 return out.code 4343 } 4344 return fio_replace_core(source,live,candidate,backup,mode,buffer,capacity,expected_candidate,expected_live,out) 4345} 4346 4347 4348// Digest comparison is over the prepared bytes, before any live replacement. 4349// Expected points to a SHA-256 digest (32 bytes), not a filename or size. 4350func fio_verify_sha256(path: *u8,expected: *u8,buffer: *u8,capacity: i64) -> i64 { 4351 if (expected as i64)==0 || (buffer as i64)==0 || capacity<=0 { return FIO_EINVAL } 4352 let input: *NxFileReadRegion=sys_mmap(__size_of(NxFileReadRegion)) as *NxFileReadRegion 4353 fio_region_init(input) 4354 var rc: i64=fio_region_open(path,input) 4355 let ctx: *Sha256=sys_mmap(__size_of(Sha256)) as *Sha256 4356 sha256_init(ctx) 4357 while rc==0 && input.read_bytes<input.length { 4358 let n: i64=fio_region_next(input,buffer,capacity) 4359 if n<0 { rc=n;break } 4360 sha256_update(ctx,buffer,n) 4361 } 4362 fio_region_close(input) 4363 if rc==0 { rc=input.code } 4364 if rc==0 { 4365 let actual: *u8=sys_mmap(32) 4366 sha256_final(ctx,actual) 4367 var i: i64=0;var differs: i64=0 4368 while i<32 { differs=differs | ((actual[i] as i64) ^ (expected[i] as i64));i=i+1 } 4369 if differs!=0 { rc=FIO_EBADMSG } 4370 sys_munmap(actual,32) 4371 } 4372 sha256_destroy(ctx);sys_munmap(ctx as *u8,__size_of(Sha256)) 4373 sys_munmap(input as *u8,__size_of(NxFileReadRegion)) 4374 return rc 4375} 4376 4377// nx_itoa_lib.nx -- THE shared integer->decimal emitter. ONE copy, so the corpus stops retyping it. 4378// 4379// LIFTED, NEVER COPIED (2026-07-31, debt 1785557603). ccz_cat_num was already correct, already 4380// MSB-first, already zero-allocation, and already had 10+ callers -- it was simply IMPRISONED inside 4381// nx_crashresume_census_core.nx, a crash-resume census organ. Seven files imported an entire census 4382// just to print an integer. That import cost, NOT ignorance of the primitive, is why ~87 sites 4383// hand-rolled their own. LAW: WHEN A CORRECT PRIMITIVE IS RETYPED, MEASURE ITS IMPORT COST BEFORE 4384// BLAMING DISCOVERABILITY -- people do not retype what is CHEAP to reach. 4385// 4386// THE LEAK WAS NEVER IN THE PRIMITIVE, IT WAS IN THE MISSING WRAPPER. ccz_cat_num allocates nothing. 4387// What every clone hand-rolled was the fd shim around it, e.g. nx_lock_reap_gate.g_putn: 4388// let b: *u8 = sys_mmap(32); let e: i64 = ccz_cat_num(b, 0, v); sys_write(1, b, e); return 0 4389// -- one mmap per call, never freed. nxi_fd below is that shim, written ONCE and always freeing. 4390// 4391// The census now imports THIS file; NishiLang import is transitive (verified: nx_lock_reap_gate 4392// imports only nx_syscalls + nx_lock_reap_core, and resolves ccz_cat_num through the core), so all 4393// existing callers keep resolving with no edit. 4394// 4395// LAYERING: lives in runtime/ so BOTH runtime/ and _hdl_build/ can import it. 4396// license_tier: ORIGINAL No hw writes (Rule 26). 4397 4398// MSB-FIRST (2026-07-31, debt 1785516350): the previous body built digits LEAST-significant first, 4399// which comes out BACKWARDS and therefore needed a sys_mmap(32) scratch buffer to reverse through -- 4400// and never freed it, leaking a page per call across 12+ importers. Emitting MOST-significant first 4401// needs no buffer at all, so this now ALLOCATES NOTHING. Output bytes and the NUL-terminate contract 4402// are unchanged; this is a rewrite of the algorithm, not a sprinkled munmap (rule 3). 4403const CCZ_ASCII_0: i64 = 48 4404const CCZ_MINUS: i64 = 45 4405const CCZ_DEC: i64 = 10 4406func ccz_cat_num(buf: *u8, off: i64, v: i64) -> i64 { 4407 var o: i64 = off 4408 var m: i64 = v 4409 if m == 0 { buf[o] = CCZ_ASCII_0 as u8; o = o + 1; buf[o] = 0 as u8; return o } 4410 if m < 0 { buf[o] = CCZ_MINUS as u8; o = o + 1; m = 0 - m } 4411 // i64 MIN negates to itself and stays negative; clamp rather than loop forever on the digit walk. 4412 if m < 0 { m = 0 } 4413 var pw: i64 = 1 4414 while m / pw >= CCZ_DEC { pw = pw * CCZ_DEC } 4415 while pw > 0 { 4416 buf[o] = (CCZ_ASCII_0 + ((m / pw) % CCZ_DEC)) as u8 4417 o = o + 1 4418 pw = pw / CCZ_DEC 4419 } 4420 buf[o] = 0 as u8 4421 return o 4422} 4423 4424// max i64 is 19 digits + sign + the NUL ccz_cat_num writes; 24 leaves slack, well under one page. 4425const NXI_BUF: i64 = 24 4426const NXI_STDOUT: i64 = 1 4427const NXI_STDERR: i64 = 2 4428 4429// Write v as decimal to fd. ONE buffer, ALWAYS freed -- the balanced shape nx_mmapbal certifies. 4430// This is the drop-in for every hand-rolled putn/gn/wn/pn clone. Returns bytes written. 4431func nxi_fd(fd: i64, v: i64) -> i64 { 4432 let b: *u8 = sys_mmap(NXI_BUF) 4433 let n: i64 = ccz_cat_num(b, 0, v) 4434 sys_write(fd, b, n) 4435 sys_munmap(b, NXI_BUF) 4436 return n 4437} 4438 4439func nxi_out(v: i64) -> i64 { return nxi_fd(NXI_STDOUT, v) } 4440func nxi_err(v: i64) -> i64 { return nxi_fd(NXI_STDERR, v) } 4441 4442// NUL-FREE buffer form (2026-07-31). ccz_cat_num NUL-terminates -- it writes dst[ret]=0 -- which is 4443// right for its own callers but WRONG as a drop-in for the large clone family whose contract is 4444// "append digits, touch nothing else, return the new offset". Pointing those at ccz_cat_num would 4445// write one byte past the returned offset, and a clone that patches a number into the MIDDLE of an 4446// already-built buffer would have the next byte clobbered. nx_office_serve.of_catn alone has 60 4447// call sites, none of them audited for that. 4448// So the lib carries BOTH contracts explicitly rather than making every migrator guess: 4449// ccz_cat_num -> digits + NUL, returns the offset BEFORE the NUL 4450// nxi_buf -> digits only, returns the offset AFTER them, ZERO bytes touched beyond 4451// Both are MSB-first and allocate NOTHING. Constants are the CCZ_ ones lifted with ccz_cat_num. 4452func nxi_buf(dst: *u8, off: i64, v: i64) -> i64 { 4453 var p: i64 = off 4454 var m: i64 = v 4455 if m < 0 { 4456 dst[p] = CCZ_MINUS as u8 4457 p = p + 1 4458 m = 0 - m 4459 } 4460 // i64 MIN negates to ITSELF and stays negative. Clamp to 0 rather than looping forever or 4461 // emitting garbage -- a documented bound, never a silent wrong number. 4462 if m < 0 { m = 0 } 4463 var pw: i64 = 1 4464 while m / pw >= CCZ_DEC { pw = pw * CCZ_DEC } 4465 while pw > 0 { 4466 dst[p] = (CCZ_ASCII_0 + ((m / pw) % CCZ_DEC)) as u8 4467 p = p + 1 4468 pw = pw / CCZ_DEC 4469 } 4470 return p 4471} 4472 4473// nx_vsz_watchdog_core.nx -- importable CORE of the VSZ watchdog (the permanent fix for outage MODE 2: 4474// mmap-per-request daemons never munmap -> VSZ balloons (mgmt hit ~160GB) -> fork() fails -> child-exec 4475// SILENTLY EMPTY while /api/health stays 200; see reference-mgmt-api-outage-tmp-log-rootcause-2026-07-12). 4476// The watchdog DECIDES DEATH ONLY: it kills a conf-listed daemon whose VSZ crossed its threshold; RESPAWN 4477// stays 100% the nx_hostctl guard's job (single responsibility, no dueling supervisors). FAIL-SAFE BY 4478// CONSTRUCTION: no conf file -> INERT; unreadable /proc -> skip; cooldown suppresses kill-storms; pid<=300 4479// and self are never killed. Pure decision funcs here (gate-locked); the /proc walk + kill live in the CLI. 4480// license_tier: ORIGINAL 4481 4482 4483const VW_PROC_PATH_CAP: i64 = 256 // /proc/<pid>/status path buffer 4484const VW_STATUS_BUF: i64 = 8192 // /proc status read buffer 4485const VW_PTR_CELL: i64 = 16 // 2-i64 scratch cell (vw_num_at end-pointer out-param) 4486 4487func vw_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 4488 4489// substring containment (hostctl's hc_contains idiom; needle has no NUL so cmdline NUL separators are safe). 4490func vw_contains(hay: *u8, hn: i64, needle: *u8, nn: i64) -> i64 { 4491 if nn == 0 { return 0 } 4492 var i: i64 = 0 4493 while i + nn <= hn { 4494 var k: i64 = 0 4495 var ok: i64 = 1 4496 while k < nn { if hay[i+k] != needle[k] { ok = 0; k = nn } k = k + 1 } 4497 if ok == 1 { return 1 } 4498 i = i + 1 4499 } 4500 return 0 4501} 4502 4503// parse leading unsigned decimal from s[off..n): value, or -1 if no digit at off. end offset in endp[0]. 4504func vw_num_at(s: *u8, n: i64, off: i64, endp: *i64) -> i64 { 4505 var v: i64 = 0 4506 var any: i64 = 0 4507 var i: i64 = off 4508 var go: i64 = 1 4509 while go == 1 { 4510 go = 0 4511 if i < n { let c: i64 = s[i] as i64; if c >= 48 { if c <= 57 { v = v*10 + (c-48); any = 1; i = i + 1; go = 1 } } } 4512 } 4513 endp[0] = i 4514 if any == 0 { return 0 - 1 } 4515 return v 4516} 4517 4518// parse one conf line buf[ls..le): "<needle> <max_gb>". Writes NUL after the needle IN PLACE, returns gb 4519// (>=1) with needle start in outp[0], or -1 for comment/blank/malformed (row dropped, fail-safe). 4520func vw_parse_row(buf: *u8, ls: i64, le: i64, outp: *i64) -> i64 { 4521 var i: i64 = ls 4522 var go: i64 = 1 4523 while go == 1 { go = 0; if i < le { let c: i64 = buf[i] as i64; if c == 32 { i = i + 1; go = 1 } else { if c == 9 { i = i + 1; go = 1 } } } } 4524 if i >= le { return 0 - 1 } 4525 if buf[i] == (35 as u8) { return 0 - 1 } // '#' comment 4526 let nstart: i64 = i 4527 go = 1 4528 while go == 1 { go = 0; if i < le { let c: i64 = buf[i] as i64; if c != 32 { if c != 9 { i = i + 1; go = 1 } } } } 4529 if i >= le { return 0 - 1 } // no separator -> malformed 4530 let nend: i64 = i 4531 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64 4532 var j: i64 = i 4533 go = 1 4534 while go == 1 { go = 0; if j < le { let c: i64 = buf[j] as i64; if c == 32 { j = j + 1; go = 1 } else { if c == 9 { j = j + 1; go = 1 } } } } 4535 let gb: i64 = vw_num_at(buf, le, j, ep) 4536 sys_munmap(ep as *u8, VW_PTR_CELL) // leak-free: ep (the end-ptr out-param) was leaked per row (the ep-out-param class my leak-checker flagged) 4537 if gb < 1 { return 0 - 1 } // gb<1 -> inert row (never a 0-threshold kill-everything) 4538 buf[nend] = 0 as u8 // NUL-terminate the needle in place 4539 outp[0] = nstart 4540 return gb 4541} 4542 4543// parse the kB value of an arbitrary "<Label>:" row out of a /proc status text. -1 absent/unreadable. 4544// Generalized so VmSize (address space) and VmRSS (resident -- the heap-leak meter VSZ can hide) share ONE 4545// parser (DRY; NEVER kill on parse failure). 4546func vw_status_kb(buf: *u8, n: i64, pat: *u8) -> i64 { 4547 let pl: i64 = vw_slen(pat) 4548 var i: i64 = 0 4549 while i + pl <= n { 4550 var k: i64 = 0 4551 var ok: i64 = 1 4552 while k < pl { if buf[i+k] != pat[k] { ok = 0; k = pl } k = k + 1 } 4553 if ok == 1 { 4554 var j: i64 = i + pl 4555 var go: i64 = 1 4556 while go == 1 { go = 0; if j < n { let c: i64 = buf[j] as i64; if c == 32 { j = j + 1; go = 1 } else { if c == 9 { j = j + 1; go = 1 } } } } 4557 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64 4558 let r: i64 = vw_num_at(buf, n, j, ep) 4559 sys_munmap(ep as *u8, VW_PTR_CELL) // leak-free: ep was mmap'd-and-leaked per call (the ep-out-param leak class) 4560 return r 4561 } 4562 i = i + 1 4563 } 4564 return 0 - 1 4565} 4566 4567// THE kill decision. 1 only when: threshold sane (gb>=1) AND vsz known (kb>0) AND over threshold AND the 4568// per-row cooldown expired. Everything else -> 0 (fail-safe). 4569func vw_should_kill(vsz_kb: i64, max_gb: i64, last_kill_s: i64, now_s: i64, cooldown_s: i64) -> i64 { 4570 if max_gb < 1 { return 0 } 4571 if vsz_kb <= 0 { return 0 } 4572 if vsz_kb <= max_gb * 1048576 { return 0 } 4573 if now_s - last_kill_s < cooldown_s { return 0 } 4574 return 1 4575} 4576 4577// bounded whole-file read. -1 absent. 4578func vw_read(path: *u8, buf: *u8, cap: i64) -> i64 { 4579 let fd: i64 = sys_openat_rd(path) 4580 if fd < 0 { return 0 - 1 } 4581 var tot: i64 = 0 4582 var n: i64 = sys_read(fd, buf, cap) 4583 while n > 0 { tot = tot + n; if tot >= cap { n = 0 } else { n = sys_read(fd, (buf as i64 + tot) as *u8, cap - tot) } } 4584 sys_close(fd) 4585 return tot 4586} 4587 4588// monotonic seconds (persists across one-shot runs within a boot -- exactly the cooldown scope we want). 4589func vw_now_s() -> i64 { let ts: *i64 = sys_mmap(16) as *i64; sys_clock_gettime_mono(ts); return ts[0] } 4590 4591// self pid via /proc/self/stat leading digits (no getpid syscall-number risk). 4592func vw_selfpid() -> i64 { 4593 let b: *u8 = sys_mmap(VW_PROC_PATH_CAP) 4594 let n: i64 = vw_read("/proc/self/stat" as *u8, b, VW_PROC_PATH_CAP - 1) 4595 if n <= 0 { sys_munmap(b, VW_PROC_PATH_CAP); return 0 - 1 } 4596 let ep: *i64 = sys_mmap(VW_PTR_CELL) as *i64 4597 let r: i64 = vw_num_at(b, n, 0, ep) 4598 sys_munmap(b, VW_PROC_PATH_CAP); sys_munmap(ep, VW_PTR_CELL) // leak-free (b + ep were leaked per call) 4599 return r 4600} 4601 4602// back-compat: the VmSize row (a sibling gate + vw_status_kb_of call this by name) 4603func vw_vmsize_parse(buf: *u8, n: i64) -> i64 { return vw_status_kb(buf, n, "VmSize:" as *u8) } 4604// kB of an arbitrary "<label>:" status row for a /proc entry named by DIRECTORY STRING (pid or "self"). 4605// -1 unreadable. LEAK-FREE (munmaps path+b on every return). Generalized so VmSize (address space) and 4606// VmRSS (resident heap -- the leak class an arena hides from VSZ) share ONE reader. 4607func vw_status_kb_of(dirname: *u8, label: *u8) -> i64 { 4608 let path: *u8 = sys_mmap(VW_PROC_PATH_CAP) 4609 var o: i64 = 0 4610 let pre: *u8 = "/proc/" as *u8 4611 var a: i64 = 0 4612 while pre[a] != (0 as u8) { path[o] = pre[a]; o = o + 1; a = a + 1 } 4613 a = 0 4614 while dirname[a] != (0 as u8) { path[o] = dirname[a]; o = o + 1; a = a + 1 } 4615 let suf: *u8 = "/status" as *u8 4616 a = 0 4617 while suf[a] != (0 as u8) { path[o] = suf[a]; o = o + 1; a = a + 1 } 4618 path[o] = 0 as u8 4619 let b: *u8 = sys_mmap(VW_STATUS_BUF) 4620 let n: i64 = vw_read(path, b, VW_STATUS_BUF - 1) 4621 if n <= 0 { sys_munmap(path, VW_PROC_PATH_CAP); sys_munmap(b, VW_STATUS_BUF); return 0 - 1 } 4622 let r: i64 = vw_status_kb(b, n, label) 4623 sys_munmap(path, VW_PROC_PATH_CAP) 4624 sys_munmap(b, VW_STATUS_BUF) 4625 return r 4626} 4627// VmSize kB (back-compat; leak_check's memory meter). -1 unreadable. 4628func vw_vmsize_kb_of(dirname: *u8) -> i64 { return vw_status_kb_of(dirname, "VmSize:" as *u8) } 4629// VmRSS kB (resident set -- the heap-leak meter VmSize can hide in an arena). 4630func vw_rss_kb_of(dirname: *u8) -> i64 { return vw_status_kb_of(dirname, "VmRSS:" as *u8) } 4631 4632// nx_os_fs.nx -- OS FILESYSTEM-NAMESPACE SEAM (the write-safety half; sibling of nx_os_proc.nx). 4633// Answers ONE question for the IO layer: is this path in the OS's device/kernel/firmware namespace, 4634// where a file write could touch hardware or kernel state? Rule 26 (never-brick) demands the answer 4635// be BY CONSTRUCTION -- compiled in, not config-disableable -- so the deny lives here, in code, and 4636// callers cannot toggle it off with a conf line. 4637// 4638// LINUX BACKEND (current): the kernel exposes devices/firmware knobs as FILES under /dev, /sys, /proc 4639// (e.g. /sys/firmware/efi/efivars -- an errant write there can brick a board; /dev/sda -- raw disk). 4640// A path is write-forbidden iff it IS or is UNDER one of those roots. 4641// 4642// NISHIOS-NATIVE (target): NishiOS has no ambient device files -- device access is capability-routed 4643// through typed channels, so the ambient-namespace hazard class does not exist; the native backend 4644// returns forbid only for its reserved kernel-object namespace. This file is the SOURCE-SWAP seam 4645// (same contract, swapped backend), exactly like nx_os_proc.nx. license_tier: ORIGINAL 4646 4647 4648const OSF_SLASH: i64 = 47 // '/' -- path separator (namespace-boundary test) 4649 4650// is path EXACTLY root or UNDER root/ ? (blocks "/dev" and "/dev/null", not "/devdata") 4651func osf_under(path: *u8, root: *u8) -> i64 { 4652 var i: i64 = 0 4653 while root[i] != (0 as u8) { 4654 if path[i] != root[i] { return 0 } 4655 i = i + 1 4656 } 4657 if path[i] == (0 as u8) { return 1 } // exactly the root 4658 if path[i] == (OSF_SLASH as u8) { return 1 } // inside the root 4659 return 0 4660} 4661// WRITE-FORBIDDEN check: 1 = the OS device/kernel/firmware namespace, never writable through the IO layer. 4662func osf_write_forbidden(path: *u8) -> i64 { 4663 if osf_under(path, "/dev" as *u8) == 1 { return 1 } 4664 if osf_under(path, "/sys" as *u8) == 1 { return 1 } 4665 if osf_under(path, "/proc" as *u8) == 1 { return 1 } 4666 return 0 4667} 4668 4669// nx_os_proc.nx -- OS PROCESS-INTROSPECTION abstraction (the PORTABILITY SEAM). THE ONE place OS-specific 4670// process access lives, so every tool above it (nx_heal, ...) stays OS-AGNOSTIC + portable. Interop by 4671// construction: ONE source compiles to BOTH backends via the @ifdef target guard -- 4672// LINUX backend = procfs (/proc/<pid>/{stat,cmdline}, getdents on /proc) [current NAS deploy] 4673// NISHIOS backend = native process table (@ifdef TARGET_NISHI seam) -- superior: a direct kernel 4674// query, NO text-parsing of /proc, NO USER_HZ guesswork. FAILS LOUD until wired, 4675// so a NishiOS build never silently inherits Linux assumptions. 4676// LAW (portability): NEVER scatter raw /proc, /sys, /dev, or hardcoded syscall numbers through the LOGIC 4677// layer -- put OS-specifics behind an nx_os_* seam like this one. The sovereign core (seg_store, tool 4678// logic) already only touches nx_syscalls (ABI-abstracted); this extends the same discipline to OS features. 4679// license_tier: ORIGINAL 4680 4681 4682const OSP_HZ_LINUX: i64 = 100 // Linux USER_HZ: /proc/<pid>/stat starttime ticks/sec 4683const OSP_PATH_CAP: i64 = 256 4684const OSP_RD_CAP: i64 = 4096 4685const OSP_STAT_CAP: i64 = 262144 // /proc/stat whole-file read cap (btime scan) 4686const OSP_DENT_BUF: i64 = 65536 // getdents64 batch buffer (proven sizing) 4687const OSP_F_PPID: i64 = 2 // /proc/<pid>/stat field after ')': state=1 ppid=2 ... utime=12 stime=13 ... starttime=20 4688const OSP_F_START: i64 = 20 4689const OSP_F_UTIME: i64 = 12 // user-mode CPU ticks (cumulative) 4690const OSP_F_STIME: i64 = 13 // kernel-mode CPU ticks (cumulative) 4691const OSP_ASCII_0: i64 = 48 4692const OSP_ASCII_9: i64 = 57 4693const OSP_SP: i64 = 32 4694const OSP_NL: i64 = 10 4695const OSP_RP: i64 = 41 // ')' 4696const OSP_SLASH: i64 = 47 4697const OSP_NUL: i64 = 0 4698const OSP_SENTINEL: i64 = 0 - 1 // "not available on this OS backend" 4699 4700func osp_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (OSP_NUL as u8) { n = n + 1 } return n } 4701// pid integer -> decimal string in out; return len 4702func osp_itoa(v: i64, out: *u8) -> i64 { 4703 if v == 0 { out[0] = OSP_ASCII_0 as u8; out[1] = 0 as u8; return 1 } 4704 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0 4705 while m > 0 { t[k] = (OSP_ASCII_0 + (m % 10)) as u8; m = m / 10; k = k + 1 } 4706 var i: i64 = 0 4707 while i < k { out[i] = t[k-1-i]; i = i + 1 } 4708 out[k] = 0 as u8 4709 return k 4710} 4711// parse leading integer at buf[off..]; endp[0] = position of the FIRST non-digit (NOT n) so a caller 4712// walking fields can resume there. BUG-FIX 2026-07-16: the old `i = n` break jumped to end-of-buffer, 4713// so osp_stat_field skipped every field after the first non-numeric one (the state char) and never 4714// reached starttime (field 20) -- diagnose then dropped every real process. 4715func osp_num(buf: *u8, n: i64, off: i64, endp: *i64) -> i64 { 4716 var v: i64 = 0; var i: i64 = off; var any: i64 = 0; var go: i64 = 1 4717 while go == 1 { 4718 go = 0 4719 if i < n { let c: i64 = buf[i] as i64; if c >= OSP_ASCII_0 { if c <= OSP_ASCII_9 { v = v*(10 as i64)+(c-OSP_ASCII_0); any = 1; i = i + 1; go = 1 } } } 4720 } 4721 endp[0] = i 4722 if any == 0 { return OSP_SENTINEL } 4723 return v 4724} 4725// basename (after last '/') of NUL-terminated s 4726func osp_basename(s: *u8) -> *u8 { 4727 var i: i64 = 0; var last: i64 = 0 4728 while s[i] != (OSP_NUL as u8) { if s[i] == (OSP_SLASH as u8) { last = i + 1 } i = i + 1 } 4729 return (s as i64 + last) as *u8 4730} 4731 4732// ============================ LINUX BACKEND (procfs) ============================ 4733// NOTE: the OS-target guard is currently a SOURCE-SWAP seam, not a compile-time @ifdef -- nx_cc's 4734// preprocessor is arch-guard-only today (TARGET_X86_64); a custom TARGET_NISHI compiled BOTH branches 4735// and the stub won (proven 2026-07-16). When nx_cc gains OS-target guards, wrap this in @ifdef 4736// TARGET_LINUX and the NishiOS backend (spec at bottom) in @ifdef TARGET_NISHI. The portability WIN 4737// stands regardless: every raw /proc lives HERE and nowhere else, so swapping the backend is a 4738// single-file change with the whole LOGIC layer (nx_heal) untouched. 4739// bounded read of a whole (small) proc/file into buf; returns len (0 on empty/absent) 4740func osp_bread(path: *u8, buf: *u8, cap: i64) -> i64 { 4741 let fd: i64 = sys_openat_rd(path) 4742 if fd < 0 { return 0 } 4743 var got: i64 = 0; var go: i64 = 1 4744 while go == 1 { let r: i64 = sys_read(fd, (buf as i64 + got) as *u8, cap - got); if r > 0 { got = got + r; if got >= cap { go = 0 } } else { go = 0 } } 4745 sys_close(fd) 4746 return got 4747} 4748// build "/proc/<pid><leaf>" (leaf NUL-terminated, e.g. "/stat") into out 4749func osp_ppath(out: *u8, pid: i64, leaf: *u8) -> i64 { 4750 var o: i64 = 0 4751 let pre: *u8 = "/proc/" as *u8 4752 var i: i64 = 0 4753 while pre[i] != (OSP_NUL as u8) { out[o] = pre[i]; o = o + 1; i = i + 1 } 4754 o = o + osp_itoa(pid, (out as i64 + o) as *u8) 4755 i = 0 4756 while leaf[i] != (OSP_NUL as u8) { out[o] = leaf[i]; o = o + 1; i = i + 1 } 4757 out[o] = 0 as u8 4758 return o 4759} 4760// enumerate live pids into pids[0..cap); returns count 4761func osp_list_pids(pids: *i64, cap: i64) -> i64 { 4762 let fd: i64 = sys_openat_rd("/proc" as *u8) 4763 if fd < 0 { return 0 } 4764 let dbuf: *u8 = sys_mmap(OSP_DENT_BUF) 4765 let ep: *i64 = sys_mmap(16) as *i64 4766 var cnt: i64 = 0; var run: i64 = 1 4767 while run == 1 { 4768 let n: i64 = sys_getdents64(fd, dbuf, OSP_DENT_BUF) 4769 if n <= 0 { run = 0 } else { 4770 var off: i64 = 0 4771 while off < n { 4772 let rec: *u8 = ((dbuf as i64 + off) as *u8) 4773 let reclen: i64 = dirent_reclen(rec) 4774 if reclen <= 0 { off = n } else { 4775 let name: *u8 = dirent_name(rec) 4776 if name[0] >= (OSP_ASCII_0 as u8) { if name[0] <= (OSP_ASCII_9 as u8) { 4777 if cnt < cap { let pid: i64 = osp_num(name, osp_slen(name), 0, ep); if pid > 0 { pids[cnt] = pid; cnt = cnt + 1 } } 4778 } } 4779 off = off + reclen 4780 } 4781 } 4782 } 4783 } 4784 sys_close(fd) 4785 return cnt 4786} 4787// count OPEN FILE DESCRIPTORS of pid = numeric entries in /proc/<pid>/fd. -1 if unreadable (gone/no perm). 4788// The fd METER for anomaly detection: a socket/file-descriptor leak trends up HERE while VmSize can stay 4789// flat (the fd table is not the address space) -- a leak shows in whatever resource disappears. Leak-free. 4790func osp_fd_count_from_fd_result(fd: i64, cause: *i64) -> i64 { 4791 if (cause as i64) != 0 { cause[0]=0 } 4792 let dbuf: *u8=sys_mmap(OSP_DENT_BUF) 4793 var count: i64=0 4794 var running: i64=1 4795 var failure: i64=0 4796 while running == 1 { 4797 let n: i64=sys_getdents64(fd,dbuf,OSP_DENT_BUF) 4798 if n < 0 { failure=n; running=0 } else { 4799 if n == 0 { running=0 } else { 4800 var off: i64=0 4801 while off < n { 4802 let rec: *u8=((dbuf as i64)+off) as *u8 4803 let reclen: i64=dirent_reclen(rec) 4804 if reclen <= 0 { failure=OSP_SENTINEL; running=0; break } 4805 let name: *u8=dirent_name(rec) 4806 if name[0] >= OSP_ASCII_0 as u8 && name[0] <= OSP_ASCII_9 as u8 { count=count+1 } 4807 off=off+reclen 4808 } 4809 } 4810 } 4811 } 4812 sys_munmap(dbuf,OSP_DENT_BUF) 4813 if failure < 0 { if (cause as i64) != 0 { cause[0]=failure }; return OSP_SENTINEL } 4814 return count 4815} 4816func osp_fd_count_from_fd(fd: i64) -> i64 { 4817 return osp_fd_count_from_fd_result(fd,0 as *i64) 4818} 4819func osp_fd_count(pid: i64) -> i64 { 4820 let path: *u8=sys_mmap(OSP_PATH_CAP) 4821 osp_ppath(path,pid,"/fd") 4822 let fd: i64=sys_openat_rd(path) 4823 sys_munmap(path,OSP_PATH_CAP) 4824 if fd < 0 { return OSP_SENTINEL } 4825 let count: i64=osp_fd_count_from_fd(fd) 4826 sys_close(fd) 4827 return count 4828} 4829 4830// field after the last ')' in /proc/<pid>/stat: OSP_F_PPID or OSP_F_START; SENTINEL on fail 4831func osp_stat_field(pid: i64, fidx: i64) -> i64 { 4832 let p: *u8 = sys_mmap(OSP_PATH_CAP) 4833 osp_ppath(p, pid, "/stat" as *u8) 4834 let b: *u8 = sys_mmap(OSP_RD_CAP) 4835 let n: i64 = osp_bread(p, b, OSP_RD_CAP - 1) 4836 if n <= 0 { return OSP_SENTINEL } 4837 var rp: i64 = 0 - 1; var i: i64 = 0 4838 while i < n { if b[i] == (OSP_RP as u8) { rp = i } i = i + 1 } 4839 if rp < 0 { return OSP_SENTINEL } 4840 var f: i64 = 0; i = rp + 1 4841 let ep: *i64 = sys_mmap(16) as *i64 4842 while i < n { 4843 if b[i] == (OSP_SP as u8) { i = i + 1 } else { 4844 f = f + 1 4845 let v: i64 = osp_num(b, n, i, ep) 4846 if f == fidx { return v } 4847 i = ep[0] 4848 var go: i64 = 1 4849 while go == 1 { go = 0; if i < n { if b[i] != (OSP_SP as u8) { i = i + 1; go = 1 } } } 4850 } 4851 } 4852 return OSP_SENTINEL 4853} 4854func osp_ppid(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_PPID) } 4855func osp_starttime_ticks(pid: i64) -> i64 { return osp_stat_field(pid, OSP_F_START) } 4856// cumulative CPU ticks consumed by pid = utime + stime. The CPU METER for anomaly detection: sampled over 4857// time, its RATE (Theil-Sen slope of the per-interval deltas) = the burn = "power disappearing" (a busy-loop 4858// pegs a core). SENTINEL if unreadable. HZ ticks/sec via osp_hz(). 4859func osp_cpu_ticks(pid: i64) -> i64 { 4860 let u: i64 = osp_stat_field(pid, OSP_F_UTIME) 4861 let s: i64 = osp_stat_field(pid, OSP_F_STIME) 4862 if u == OSP_SENTINEL { return OSP_SENTINEL } 4863 if s == OSP_SENTINEL { return OSP_SENTINEL } 4864 return u + s 4865} 4866// argv0 basename of /proc/<pid>/cmdline into out; return len (0 if none) 4867func osp_cmd_argv0(pid: i64, out: *u8, cap: i64) -> i64 { 4868 let p: *u8 = sys_mmap(OSP_PATH_CAP) 4869 osp_ppath(p, pid, "/cmdline" as *u8) 4870 let cl: *u8 = sys_mmap(cap + 1) 4871 let n: i64 = osp_bread(p, cl, cap) 4872 if n <= 0 { out[0] = 0 as u8; return 0 } 4873 cl[n] = 0 as u8 // argv0 = bytes up to the first NUL (already there) 4874 let bn: *u8 = osp_basename(cl) 4875 var o: i64 = 0 4876 while bn[o] != (OSP_NUL as u8) { out[o] = bn[o]; o = o + 1 } 4877 out[o] = 0 as u8 4878 return o 4879} 4880func osp_hz() -> i64 { return OSP_HZ_LINUX } 4881// where the HOST SUPERVISOR writes its log -- a deployment/OS question, so it lives in the seam. 4882// Linux/NAS deploy: /tmp/supervisor.log (nx_hostctl supervise). NishiOS: its native supervisor journal. 4883func osp_supervisor_log() -> *u8 { return "/tmp/supervisor.log" as *u8 } 4884func osp_uptime_s() -> i64 { let ts: *i64 = sys_mmap(16) as *i64; sys_clock_gettime_mono(ts); return ts[0] } 4885func osp_selfpid() -> i64 { 4886 let b: *u8 = sys_mmap(OSP_RD_CAP) 4887 let n: i64 = osp_bread("/proc/self/stat" as *u8, b, OSP_RD_CAP - 1) 4888 if n <= 0 { return OSP_SENTINEL } 4889 let ep: *i64 = sys_mmap(16) as *i64 4890 return osp_num(b, n, 0, ep) 4891} 4892// wallclock epoch = /proc/stat btime + monotonic-since-boot 4893func osp_boot_epoch() -> i64 { 4894 let b: *u8 = sys_mmap(OSP_STAT_CAP) 4895 let n: i64 = osp_bread("/proc/stat" as *u8, b, OSP_STAT_CAP - 1) 4896 if n <= 0 { return 0 } 4897 let needle: *u8 = "btime " as *u8 4898 var i: i64 = 0 4899 let ep: *i64 = sys_mmap(16) as *i64 4900 while i < n { 4901 var m: i64 = 1; var k: i64 = 0 4902 while needle[k] != (OSP_NUL as u8) { if i+k >= n { m = 0 } else { if b[i+k] != needle[k] { m = 0 } } k = k + 1 } 4903 if m == 1 { return osp_num(b, n, i + k, ep) } 4904 i = i + 1 4905 } 4906 return 0 4907} 4908 4909// ======================= NISHIOS BACKEND SPEC (the swap-in seam) ======================= 4910// When NishiOS's native process API lands, REPLACE the Linux backend above (or @ifdef-branch it once 4911// nx_cc has OS-target guards) with these ~8 functions over NishiOS's DIRECT kernel process table -- 4912// superior to procfs: no /proc text-parsing, no USER_HZ, a real syscall query. The interface the LOGIC 4913// layer depends on (and ALL it depends on) is exactly: 4914// osp_list_pids(pids,cap)->count nishi_proc_enumerate (live pids) 4915// osp_ppid(pid)->ppid nishi_proc_parent 4916// osp_starttime_ticks(pid)->ticks nishi_proc_starttime (osp_hz() ticks/sec) 4917// osp_cmd_argv0(pid,out,cap)->len nishi_proc_argv0 (basename of argv0) 4918// osp_hz()->ticks_per_sec native rate (not the Linux-100 assumption) 4919// osp_uptime_s()->secs sys_clock_gettime_mono (already OS-neutral) 4920// osp_selfpid()->pid nishi_getpid 4921// osp_boot_epoch()->epoch nishi_boot_epoch 4922// Interop: NishiOS ships the superior backend; Linux stays supported for the current NAS deploy; the 4923// LOGIC (nx_heal) compiles unchanged on both. THAT is the point of this file. 4924 4925// Direct-child enumeration does not depend on CONFIG_CHECKPOINT_RESTORE's 4926// optional /proc/<pid>/task/<pid>/children file. The caller owns the output 4927// capacity; overflow is an error, never a plausible partial child set. 4928const OSP_DIRENT_NAME_OFFSET: i64 = 19 // Linux linux_dirent64 ABI 4929func osp_stat_parent(buf: *u8, n: i64) -> i64 { 4930 var last: i64=0-1;var i: i64=0 4931 while i < n { if buf[i] == OSP_RP as u8 { last=i };i=i+1 } 4932 if last < 0 { return 0-5 } 4933 i=last+1 4934 while i < n && buf[i] == OSP_SP as u8 { i=i+1 } 4935 while i < n && buf[i] != OSP_SP as u8 { i=i+1 } 4936 while i < n && buf[i] == OSP_SP as u8 { i=i+1 } 4937 let first: i64=i;var parent: i64=0 4938 while i < n && buf[i] >= OSP_ASCII_0 as u8 && buf[i] <= OSP_ASCII_9 as u8 { 4939 parent=parent*10+(buf[i] as i64)-OSP_ASCII_0;i=i+1 4940 } 4941 if i == first || i == n || buf[i] != OSP_SP as u8 { return 0-5 } 4942 return parent 4943} 4944func osp_children(parent: i64, children: *i64, capacity: i64) -> i64 { 4945 if parent <= 0 || capacity < 0 { return 0-22 } 4946 let directory: i64=sys_openat_directory("/proc") 4947 if directory < 0 { return directory } 4948 let batch: *u8=sys_mmap(OSP_DENT_BUF) 4949 let path: *u8=sys_mmap(OSP_PATH_CAP) 4950 let stat: *u8=sys_mmap(OSP_RD_CAP) 4951 var count: i64=0;var failure: i64=0;var running: i64=1 4952 while running == 1 { 4953 let n: i64=sys_getdents64(directory,batch,OSP_DENT_BUF) 4954 if n == (0-4) { continue } 4955 if n <= 0 { failure=n;break } 4956 var off: i64=0 4957 while off < n { 4958 if n-off <= OSP_DIRENT_NAME_OFFSET { failure=0-5;running=0;break } 4959 let rec: *u8=batch+off 4960 let size: i64=dirent_reclen(rec) 4961 if size <= OSP_DIRENT_NAME_OFFSET || size > n-off { failure=0-5;running=0;break } 4962 var i: i64=OSP_DIRENT_NAME_OFFSET;var pid: i64=0 4963 while i < size && rec[i] >= OSP_ASCII_0 as u8 && rec[i] <= OSP_ASCII_9 as u8 { 4964 pid=pid*10+(rec[i] as i64)-OSP_ASCII_0;i=i+1 4965 } 4966 if pid > 0 && i < size && rec[i] == OSP_NUL as u8 { 4967 osp_ppath(path,pid,"/stat") 4968 let fd: i64=sys_openat_rd(path) 4969 // A process may disappear during enumeration; other failures 4970 // make completeness unproven and must propagate. 4971 if fd < 0 && fd != (0-2) { failure=fd;running=0;break } 4972 if fd >= 0 { 4973 var used: i64=0;var readrc: i64=1 4974 while readrc > 0 && used < OSP_RD_CAP { 4975 readrc=sys_read(fd,stat+used,OSP_RD_CAP-used) 4976 if readrc == (0-4) { readrc=1;continue } 4977 if readrc > 0 { used=used+readrc } 4978 } 4979 let closed: i64=sys_close(fd) 4980 if readrc < 0 && readrc != (0-3) { failure=readrc;running=0;break } 4981 if closed < 0 { failure=closed;running=0;break } 4982 if used == OSP_RD_CAP { failure=0-75;running=0;break } 4983 if used > 0 { 4984 let observed: i64=osp_stat_parent(stat,used) 4985 if observed < 0 { failure=observed;running=0;break } 4986 if observed == parent { 4987 if count >= capacity { failure=0-28;running=0;break } 4988 children[count]=pid;count=count+1 4989 } 4990 } 4991 } 4992 } 4993 off=off+size 4994 } 4995 } 4996 let closed: i64=sys_close(directory) 4997 sys_munmap(batch,OSP_DENT_BUF);sys_munmap(path,OSP_PATH_CAP);sys_munmap(stat,OSP_RD_CAP) 4998 if failure < 0 { return failure } 4999 if closed < 0 { return closed } 5000 return count 5001} 5002 5003const FSX_MAGIC_4095: i64 = 4095 5004 5005const FSX_READ_CAP: i64 = 1048576 // max bytes returned by `read` (truncation is MARKED, never silent) 5006const FSX_DENY_CAP: i64 = 8192 // fs_read_deny.conf read cap 5007const FSX_PATH_CAP: i64 = 1024 // lowercased path work buffer 5008const FSX_DENT_BUF: i64 = 65536 // getdents64 batch buffer (matches the proven vsz/heal sizing) 5009const FSX_LS_CAP: i64 = 200 // scale-law: max ls entries EMITTED; true total ALWAYS declared (65KB-dump fix) 5010const FSX_RC_ABSENT: i64 = 3 // exit: path absent/unreadable (mirrors nx_fileop's exists convention) 5011const FSX_RC_DENIED: i64 = 5 // exit: deny-list refused the read 5012const FSX_UPPER_A: i64 = 65 // 'A' (ASCII lowercasing) 5013const FSX_UPPER_Z: i64 = 90 // 'Z' 5014const FSX_CASE_OFF: i64 = 32 // 'a' - 'A' 5015const FSX_ASCII_0: i64 = 48 // '0' (decimal print) 5016 5017func fsx_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } 5018// MIGRATED to the shared emitter (debt 1785563586). The old body mmapped a scratch buffer 5019// per call and never freed it. At PAGE granularity that is 4096B leaked PER CALL -- the 5020// defect that took 28.5GB of a 36GB host in nx_ts_lumadiff (2MB input, ~3.66M calls). 5021// nxi_* is MSB-first, allocates NOTHING, and emits identical bytes including the sign. 5022func fsx_putn(v: i64) -> i64 { nxi_out(v); return 0 } 5023// lowercase copy of s into out (bounded), returns length 5024func fsx_lower(s: *u8, out: *u8, cap: i64) -> i64 { 5025 var i: i64 = 0 5026 while s[i] != (0 as u8) { 5027 if i >= cap - 1 { out[i] = 0 as u8; return i } 5028 var c: i64 = s[i] as i64 5029 if c >= FSX_UPPER_A { if c <= FSX_UPPER_Z { c = c + FSX_CASE_OFF } } 5030 out[i] = c as u8 5031 i = i + 1 5032 } 5033 out[i] = 0 as u8 5034 return i 5035} 5036// exact NUL-terminated string equality 5037func fsx_seq(a: *u8, b: *u8) -> i64 { var i: i64 = 0; while a[i] != (0 as u8) { if a[i] != b[i] { return 0 } i = i + 1 } if b[i] != (0 as u8) { return 0 } return 1 } 5038// is `needle` (NUL-terminated, lowercase) contained in lowercase path lp[0..ln)? 5039// ---------- compare-and-swap decision (seq1422/seq1456) ---------- 5040// 5041// PURE, and in the LIB on purpose: the decision used to live inside the CLI's 5042// main(), where a gate cannot reach it -- which is exactly how it shipped 5043// refusing every correct expectation (seq1422). A rule nothing can drive is a 5044// rule nothing can prove. 5045// 5046// tok is the raw argv token (`expect=<n>` / `expect=any` / a bare number); 5047// cur is the file's real size. Returns 1 = ALLOW, 0 = REFUSE. 5048func fsx_cas_val(tok: *u8) -> *u8 { 5049 var i: i64 = 0 5050 while tok[i] != (0 as u8) { 5051 if tok[i] == (61 as u8) { return ((tok as i64) + i + 1) as *u8 } 5052 i = i + 1 5053 } 5054 return tok 5055} 5056func fsx_cas_ok(cur: i64, tok: *u8) -> i64 { 5057 let v: *u8 = fsx_cas_val(tok) 5058 if fsx_seq(v, "any" as *u8) == 1 { return 1 } 5059 var n: i64 = 0 5060 var i: i64 = 0 5061 var got: i64 = 0 5062 while v[i] != (0 as u8) { 5063 let c: i64 = v[i] as i64 5064 if c >= 48 { if c <= 57 { n = n * 10 + (c - 48); got = 1 } } 5065 i = i + 1 5066 } 5067 if got == 0 { return 0 } 5068 if n == cur { return 1 } 5069 return 0 5070} 5071 5072func fsx_deny_hit(lp: *u8, ln: i64, needle: *u8) -> i64 { 5073 let nl: i64 = vw_slen(needle) 5074 if nl == 0 { return 0 } 5075 return vw_contains(lp, ln, needle, nl) 5076} 5077// data-driven deny extras: one lowercase needle per line in `conf`; 1 = some line matches the path. 5078// Factored out so the read deny (fs_read_deny.conf) and write deny (fs_write_deny.conf) share ONE scanner. 5079func fsx_conf_deny(lp: *u8, ln: i64, conf: *u8) -> i64 { 5080 let cb: *u8 = sys_mmap(FSX_DENY_CAP) 5081 let cn: i64 = vw_read(conf, cb, FSX_DENY_CAP - 1) 5082 if cn > 0 { 5083 var ls: i64 = 0 5084 var i: i64 = 0 5085 while i <= cn { 5086 var eol: i64 = 0 5087 if i == cn { eol = 1 } else { if cb[i] == (10 as u8) { eol = 1 } } 5088 if eol == 1 { 5089 if i > ls { 5090 cb[i] = 0 as u8 // terminate the line in place 5091 if fsx_deny_hit(lp, ln, (cb as i64 + ls) as *u8) == 1 { return 1 } 5092 } 5093 ls = i + 1 5094 } 5095 i = i + 1 5096 } 5097 } 5098 return 0 5099} 5100const FSX_SNIFF_CAP: i64 = 4096 5101 5102func fsx_isalnum(c: i64) -> i64 { 5103 if c >= 48 { if c <= 57 { return 1 } } 5104 if c >= 97 { if c <= 122 { return 1 } } 5105 if c >= 65 { if c <= 90 { return 1 } } 5106 return 0 5107} 5108 5109func fsx_ends_with(lp: *u8, ln: i64, suf: *u8) -> i64 { 5110 let sl: i64 = vw_slen(suf) 5111 if sl == 0 { return 0 } 5112 if sl > ln { return 0 } 5113 var i: i64 = 0 5114 while i < sl { 5115 if lp[ln - sl + i] != suf[i] { return 0 } 5116 i = i + 1 5117 } 5118 return 1 5119} 5120 5121func fsx_basename_is(lp: *u8, ln: i64, name: *u8) -> i64 { 5122 let nl: i64 = vw_slen(name) 5123 if nl == 0 { return 0 } 5124 if nl > ln { return 0 } 5125 if fsx_ends_with(lp, ln, name) == 0 { return 0 } 5126 if nl == ln { return 1 } 5127 let c: i64 = lp[ln - nl - 1] as i64 5128 if c == 47 { return 1 } 5129 if c == 92 { return 1 } 5130 return 0 5131} 5132 5133// Whole-word containment: bounded by non-alphanumeric on BOTH sides, so `api_secret.txt` is denied and 5134// `secretary_notes.md` is not. 5135func fsx_word_has(lp: *u8, ln: i64, w: *u8) -> i64 { 5136 let wl: i64 = vw_slen(w) 5137 if wl == 0 { return 0 } 5138 if wl > ln { return 0 } 5139 var i: i64 = 0 5140 while i + wl <= ln { 5141 var eq: i64 = 1 5142 var k: i64 = 0 5143 while k < wl { if lp[i + k] != w[k] { eq = 0; k = wl } else { k = k + 1 } } 5144 if eq == 1 { 5145 var lb: i64 = 1 5146 if i > 0 { if fsx_isalnum(lp[i - 1] as i64) == 1 { lb = 0 } } 5147 var rb: i64 = 1 5148 if i + wl < ln { if fsx_isalnum(lp[i + wl] as i64) == 1 { rb = 0 } } 5149 if lb == 1 { if rb == 1 { return 1 } } 5150 } 5151 i = i + 1 5152 } 5153 return 0 5154} 5155 5156// CONTENT LEG: sniff the leading bytes for what a secret actually IS. This is the half a name-only list 5157// can never do -- it denies a private key no matter what it is called, including `notes.txt`. 5158// A CERTIFICATE is deliberately NOT denied: certs are public by definition, and denying them is the same 5159// category error as denying the tokenizer. 5160func fsx_content_secret(path: *u8) -> i64 { 5161 let fd: i64 = sys_openat_rd(path) 5162 if fd < 0 { return 0 } 5163 let b: *u8 = sys_mmap(FSX_SNIFF_CAP) 5164 let n: i64 = sys_read(fd, b, FSX_SNIFF_CAP - 1) 5165 sys_close(fd) 5166 if n <= 0 { return 0 } 5167 if vw_contains(b, n, "PRIVATE KEY-----" as *u8, 16) == 1 { return 1 } 5168 if vw_contains(b, n, "OPENSSH PRIVATE KEY" as *u8, 19) == 1 { return 1 } 5169 if vw_contains(b, n, "PGP PRIVATE KEY BLOCK" as *u8, 21) == 1 { return 1 } 5170 if vw_contains(b, n, "PuTTY-User-Key-File" as *u8, 19) == 1 { return 1 } 5171 return 0 5172} 5173 5174// DENY check: 1 = refuse this path. SOTA-2026 REWRITE (2026-07-31). 5175// 5176// THE OLD RULE WAS WRONG IN BOTH DIRECTIONS, measured on real paths: 5177// OVER-BLOCKED substring "token" denied runtime/nx_tokenizer.nx -- the compiler's own tokenizer, which 5178// contains no secret -- and blocked BOTH nx_fs read AND nx_fs_write on it, while 5179// nx_shelltool grep returned the same bytes freely. It cost real work and bought nothing. 5180// substring "key" likewise denies monkey / keyword / keyboard. 5181// UNDER-BLOCKED `id_rsa`, the canonical SSH private key filename, contains NONE of 5182// secret/key/token/passw/.pem and sailed straight through. 5183// A denylist that blocks source and passes private keys is not a security control -- it is a rename away 5184// from useless in one direction and a permanent nuisance in the other. 5185// 5186// REPLACEMENT -- two INDEPENDENT legs, either one denies: 5187// (1) PATH leg: real secret-bearing EXTENSIONS and exact BASENAMES, matched at a true suffix/segment 5188// boundary, plus whole-word `secret`/`password`. No substring-anywhere matching survives. 5189// (2) CONTENT leg: PEM/OpenSSH/PGP/PuTTY private-key armour, which catches a secret regardless of name. 5190// Net effect: strictly MORE secrets denied (id_rsa, a renamed key, a key with no extension) and strictly 5191// FEWER ordinary sources blocked. 5192func fsx_denied(path: *u8) -> i64 { 5193 let lp: *u8 = sys_mmap(FSX_PATH_CAP) 5194 let ln: i64 = fsx_lower(path, lp, FSX_PATH_CAP) 5195 5196 if fsx_ends_with(lp, ln, ".pem" as *u8) == 1 { return 1 } 5197 if fsx_ends_with(lp, ln, ".key" as *u8) == 1 { return 1 } 5198 if fsx_ends_with(lp, ln, ".cap" as *u8) == 1 { return 1 } 5199 if fsx_ends_with(lp, ln, ".p12" as *u8) == 1 { return 1 } 5200 if fsx_ends_with(lp, ln, ".pfx" as *u8) == 1 { return 1 } 5201 if fsx_ends_with(lp, ln, ".jks" as *u8) == 1 { return 1 } 5202 if fsx_ends_with(lp, ln, ".ppk" as *u8) == 1 { return 1 } 5203 if fsx_ends_with(lp, ln, "_rsa" as *u8) == 1 { return 1 } 5204 if fsx_ends_with(lp, ln, "_dsa" as *u8) == 1 { return 1 } 5205 if fsx_ends_with(lp, ln, "_ecdsa" as *u8) == 1 { return 1 } 5206 if fsx_ends_with(lp, ln, "_ed25519" as *u8) == 1 { return 1 } 5207 5208 if fsx_basename_is(lp, ln, ".env" as *u8) == 1 { return 1 } 5209 if fsx_basename_is(lp, ln, "credentials" as *u8) == 1 { return 1 } 5210 if fsx_basename_is(lp, ln, "shadow" as *u8) == 1 { return 1 } 5211 if fsx_basename_is(lp, ln, "opaque_keys.bin" as *u8) == 1 { return 1 } 5212 5213 // CALIBRATED BY WORD FREQUENCY, not by one uniform rule -- the gate proved a uniform rule wrong in 5214 // BOTH directions within minutes. `secret` and `passw` are high-signal and essentially absent from 5215 // ordinary source, so SUBSTRING matching is correct for them and catches mysecret_key.bin. `key` and 5216 // `token` are common English fragments (tokenizer, monkey, keyword, keyboard) and must NEVER be 5217 // substring-matched -- that is what denied the compiler's own tokenizer. They are covered instead by 5218 // the extension/suffix rules above and by the content leg below. 5219 if fsx_deny_hit(lp, ln, "secret" as *u8) == 1 { return 1 } 5220 if fsx_deny_hit(lp, ln, "passw" as *u8) == 1 { return 1 } 5221 if fsx_deny_hit(lp, ln, "credential" as *u8) == 1 { return 1 } 5222 5223 if fsx_content_secret(path) == 1 { return 1 } 5224 5225 return fsx_conf_deny(lp, ln, "fs_read_deny.conf" as *u8) 5226} 5227// read: emit up to `cap` bytes of path to stdout. Returns bytes emitted; -1 absent; -2 DENIED. 5228// deniedp/absent are ALSO visible in the CLI exit code. Truncation is marked with a trailing banner. 5229// Failure reporter that KEEPS THE ERRNO. sys_openat_rd returns -errno, and the old message printed 5230// "ABSENT" for every negative -- so EACCES (-13, EXISTS but unopenable) read as "missing", which are 5231// OPPOSITE remedies. Cost a real hour on 2026-08-01: knowledge/foundation existed with mode 0100 and 5232// every instrument in the stack called it absent (the mkdirp read-back that printed the errno cracked 5233// the case in one call). rc>=0 means a probe re-open SUCCEEDED: the earlier read failed for a 5234// non-open reason (an empty file), so say THAT. Always returns -1 (callers' contract unchanged; 5235// the -2 DENIED sentinel stays distinct). 5236func fsx_fail(path: *u8, rc: i64) -> i64 { 5237 if rc >= 0 { sys_close(rc); fsx_puts("NX-FS EMPTY: 0 bytes: " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } 5238 if rc == 0 - 13 { 5239 fsx_puts("NX-FS PERMISSION (EACCES): exists but this process may not open it: " as *u8) 5240 fsx_puts(path); fsx_puts("\n" as *u8) 5241 return 0 - 1 5242 } 5243 if rc == 0 - 2 { fsx_puts("NX-FS ABSENT: " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } 5244 fsx_puts("NX-FS ERROR rc=" as *u8); fsx_putn(rc) 5245 fsx_puts(": " as *u8); fsx_puts(path); fsx_puts("\n" as *u8) 5246 return 0 - 1 5247} 5248 5249const FSX_SEEK_END: i64 = 2 // lseek whence: EOF offset = size, WITHOUT reading a single byte 5250 5251// TRUE SIZE -- the one thing no other read verb in this lib can give you (2026-08-07, debt 1786054029). 5252// read/lines/outline all report BYTES THEY READ against FSX_READ_CAP/FSX_LINES_SCAN, and they DO honestly 5253// declare the cap -- but an honest floor is still not a measurement: "bytes=1048576 (covers first 1048576 5254// bytes only)" is the IDENTICAL answer for a 1.05MB file and a 30MB one. 5255// MEASURED COST OF NOT HAVING IT: bounding ONE 1.38MB journal took TWELVE probe reads at hand-chosen 5256// offsets, because the only way to learn a big file size was to binary-search EOF by hand. 5257// lseek(SEEK_END) reads ZERO bytes, so the answer is exact at ANY size for one syscall. 5258// Deny-list still applies: consistency with every other verb beats a special case for a metadata read. 5259// CONTRACT DIFFERS FROM fsx_read ON PURPOSE: an EMPTY file returns 0, never -1. Size is the one caller for 5260// which "absent" and "zero bytes" are DIFFERENT FACTS, so fsx_fail -- which folds both to -1 -- is not used 5261// here. (Same distinction lt_read_tail needed: -1 ABSENT vs 0 EMPTY. A reader that conflates them cannot 5262// tell a lane that never wrote from a lane whose file vanished.) 5263// A DECLARED FLOOR IS HONEST BUT IT IS NOT A MEASUREMENT -- IF THE NUMBER IS CHEAP, EMIT THE NUMBER. 5264func fsx_size(path: *u8) -> i64 { 5265 if fsx_denied(path) == 1 { 5266 fsx_puts("NX-FS-SIZE DENIED: path matches the secret deny-list. WHY: this tool never returns key material.\n" as *u8) 5267 return 0 - (2 as i64) 5268 } 5269 let fd: i64 = sys_openat_rd(path) 5270 if fd < 0 { 5271 fsx_puts("NX-FS-SIZE ABSENT: cannot open " as *u8); fsx_puts(path) 5272 fsx_puts(" . FIX: confirm the path with `nx_fs ls <dir>`.\n" as *u8) 5273 return 0 - 1 5274 } 5275 let sz: i64 = sys_lseek(fd, 0, FSX_SEEK_END) 5276 sys_close(fd) 5277 if sz < 0 { 5278 fsx_puts("NX-FS-SIZE UNSEEKABLE: " as *u8); fsx_puts(path) 5279 fsx_puts(" (a pipe/char device has no size; this is NOT a zero-byte file)\n" as *u8) 5280 return 0 - 1 5281 } 5282 fsx_puts("NX-FS-SIZE " as *u8); fsx_puts(path) 5283 fsx_puts(" bytes=" as *u8); fsx_putn(sz) 5284 fsx_puts(" exact=1 read_bytes=0\n" as *u8) 5285 return sz 5286} 5287 5288func fsx_read(path: *u8, cap: i64) -> i64 { 5289 if fsx_denied(path) == 1 { 5290 fsx_puts("NX-FS DENIED: path matches the secret deny-list (defaults + fs_read_deny.conf)\n" as *u8) 5291 return 0 - (2 as i64) // DENIED sentinel (distinct from -1 absent) 5292 } 5293 var want: i64 = cap 5294 if want <= 0 { want = FSX_READ_CAP } 5295 if want > FSX_READ_CAP { want = FSX_READ_CAP } 5296 let buf: *u8 = sys_mmap(want + 1) 5297 let n: i64 = vw_read(path, buf, want) 5298 // vw_read flattens the errno (-1 for every failure); re-probe the open ONLY on the failure path 5299 // so the message can distinguish absent / permission / empty. Zero cost on success. 5300 if n <= 0 { return fsx_fail(path, sys_openat_rd(path)) } 5301 sys_write(1, buf, n) 5302 if n == want { 5303 fsx_puts("\n[NX-FS TRUNCATED at " as *u8); fsx_putn(n); fsx_puts(" bytes]\n" as *u8) 5304 } 5305 return n 5306} 5307// WINDOWED read (eats debt seq222: the tools-call transport caps ~64KB, so files past the cap were 5308// unreadable over MCP): emit up to `cap` bytes starting at byte `off`. Same deny-list as fsx_read. 5309// A separate function (NOT an fsx_read arity change) so every existing caller keeps its exact contract. 5310func fsx_read_at(path: *u8, cap: i64, off: i64) -> i64 { 5311 if fsx_denied(path) == 1 { 5312 fsx_puts("NX-FS DENIED: path matches the secret deny-list (defaults + fs_read_deny.conf)\n" as *u8) 5313 return 0 - (2 as i64) 5314 } 5315 var want: i64 = cap 5316 if want <= 0 { want = FSX_READ_CAP } 5317 if want > FSX_READ_CAP { want = FSX_READ_CAP } 5318 let fd: i64 = sys_openat_rd(path) 5319 if fd < 0 { return fsx_fail(path, fd) } 5320 if off > 0 { if sys_lseek(fd, off, 0) < 0 { sys_close(fd); fsx_puts("NX-FS ABSENT: seek failed " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } } 5321 let buf: *u8 = sys_mmap(want + 1) 5322 var got: i64 = 0 5323 var sc: i64 = 1 5324 while sc == 1 { 5325 let r: i64 = sys_read(fd, ((buf as i64 + got) as *u8), want - got) 5326 if r <= 0 { sc = 0 } else { got = got + r; if got >= want { sc = 0 } } 5327 } 5328 sys_close(fd) 5329 if got <= 0 { fsx_puts("NX-FS EOF: no bytes at offset " as *u8); fsx_putn(off); fsx_puts(" in " as *u8); fsx_puts(path); fsx_puts("\n" as *u8); return 0 - 1 } 5330 sys_write(1, buf, got) 5331 if got == want { 5332 fsx_puts("\n[NX-FS WINDOW off=" as *u8); fsx_putn(off); fsx_puts(" n=" as *u8); fsx_putn(got); fsx_puts(" -- more remains]\n" as *u8) 5333 } 5334 return got 5335} 5336const FSX_LINES_SCAN: i64 = 1048576 // line-addressing scan window (matches the proven read cap) 5337const FSX_LINES_MAXOUT: i64 = 262144 // max bytes emitted by one `lines` call (transport-friendly) 5338const FSX_LINES_DEFN: i64 = 40 // default line count when the caller omits it 5339const FSX_LINES_MAXN: i64 = 400 // max lines per call 5340 5341// LINE-ADDRESSED read -- THE MISSING PRIMITIVE (measured 2026-07-20): `grep` reports file:LINE but `read` 5342// takes BYTES, so the two did NOT compose -- locating one function in a remote file meant hand 5343// binary-searching byte offsets (cost one subagent 70K tokens + 22 calls for a single extraction). 5344// Emits lines [start, start+count) 1-based, then a DECLARED envelope banner (scale-law: a caller can 5345// NEVER be silently windowed -- scanned bytes, scan cap, over-window and clip flags are all stated). 5346// Same deny-list as fsx_read. Returns bytes emitted; -1 absent; -2 DENIED. 5347func fsx_read_lines(path: *u8, start: i64, count: i64) -> i64 { 5348 if fsx_denied(path) == 1 { 5349 fsx_puts("NX-FS DENIED: path matches the secret deny-list (defaults + fs_read_deny.conf)\n" as *u8) 5350 return 0 - (2 as i64) 5351 } 5352 var s: i64 = start 5353 if s < 1 { s = 1 } 5354 var c: i64 = count 5355 if c <= 0 { c = FSX_LINES_DEFN } 5356 if c > FSX_LINES_MAXN { c = FSX_LINES_MAXN } 5357 let buf: *u8 = sys_mmap(FSX_LINES_SCAN + 1) 5358 let n: i64 = vw_read(path, buf, FSX_LINES_SCAN) 5359 if n <= 0 { return fsx_fail(path, sys_openat_rd(path)) } 5360 // walk to the first byte of line `s`; cur > s afterwards means we ran off the end (fail-loud, not empty) 5361 var i: i64 = 0 5362 var cur: i64 = 1 5363 while cur < s { 5364 if i >= n { cur = s + 1 } else { 5365 if buf[i] == (10 as u8) {