code wiki / (root) / nx_syscalls.nx

nx_syscalls.nx source

↩ module page · 1971 lines · 106338 B

1// syscalls.nx -- thin __syscall wrappers used across modules. 2// 3// Sovereign path: no libc. Every memory allocation, file op, and 4// clock read in the rest of the runtime routes through one of these 5// helpers. Numbers match Linux RV64; NishiOS uses the same set. 6// 7// Extracted from runtime.nx and ir.nx's copy-pasted helpers so the 8// module-import build doesn't produce duplicate symbols. 9 10// Tier aliases (nx_size / nx_idx / nx_fd / ...) ride along with the 11// syscall shelf: 141 runtime files use `as nx_size` etc. and only 12// compiled historically because the old parser silently void-cast 13// unknown type names (T#nx-int-alias-size-0 closed that hole LOUDLY, 14// which exposed the missing import). nx_tier.nx is pure type 15// aliases (0 funcs); prepass_register_aliases skips duplicates, so 16// modules that also import it directly stay fine. 17import "nx_tier.nx" 18const SYS_MAGIC_1024: i64 = 1024 19const SYS_MAGIC_1000000: i64 = 1000000 20const SYS_MAGIC_4294967296: i64 = 4294967296 21// first read window for a size-UNKNOWABLE file (lseek END <= 0); doubles while it fills -- see sys_read_file 22const SYS_READ_GROW_INIT: i64 = 65536 23const SYS_MAGIC_100000: i64 = 100000 24 25// ---- syscall numbers (per-target) ---- 26// 27// Cross-target via the macro processor (cardinal landed 2026-05-20: 28// feedback-hardware-agnostic-is-robustness -- the substrate must 29// compile + run on every silicon we point it at). Default path 30// (TARGET_X86_64 not defined) carries Linux RV64 numbers used by 31// qemu-RV64 + NishiOS. When nxc2 is invoked with --target x86_64 32// main.c pre-defines @macro TARGET_X86_64 1 so this file resolves 33// to x86_64 Linux ABI numbers. 34// 35// nx_syscalls_x86_64.nx remains the dedicated x86_64-only mirror 36// for files that want explicit single-target imports (e.g., bench 37// smokes built only for x86_64). This block makes nx_syscalls.nx 38// itself dual-target so substrate primitives compile portably. 39 40@ifdef TARGET_X86_64 41const SYS_READ: i64 = 0 42const SYS_WRITE: i64 = 1 43const SYS_CLOSE: i64 = 3 44const SYS_LSEEK: i64 = 8 45const SYS_OPENAT: i64 = 257 46const SYS_EXIT: i64 = 60 47const SYS_MMAP: i64 = 9 48const SYS_CLOCK_GETTIME: i64 = 228 49const SYS_IOCTL: i64 = 16 50const SYS_CLOCK_NANOSLEEP: i64 = 230 51// Namespace/container family, x86 branch (debt 1785528831). Moved here from 52// nx_syscalls_x86_64.nx so ONE module owns the wrapper set -- a TU reaching both 53// modules used to hold every wrapper TWICE, resolved silently by definition ORDER. 54const SYS_CHROOT: i64 = 161 55const SYS_MOUNT: i64 = 165 56const SYS_UNSHARE: i64 = 272 57const SYS_GETUID: i64 = 102 58const SYS_GETGID: i64 = 104 59const SYS_POLL: i64 = 7 60@endif 61 62@ifndef TARGET_X86_64 63const SYS_READ: i64 = 63 64const SYS_WRITE: i64 = 64 65const SYS_CLOSE: i64 = 57 66const SYS_LSEEK: i64 = 62 67const SYS_OPENAT: i64 = 56 68const SYS_EXIT: i64 = 93 69const SYS_MMAP: i64 = 222 70const SYS_CLOCK_GETTIME: i64 = 113 71const SYS_IOCTL: i64 = 29 72const SYS_CLOCK_NANOSLEEP: i64 = 115 73// Namespace/container family, RV64 branch (debt 1785528831). This is the branch actually 74// KEPT (TARGET_X86_64 is hard-pinned undefined), so these are the numbers the x86 backend 75// translates at emit: 51->161 chroot, 40->165 mount, 97->272 unshare, 174->102 getuid, 76// 176->104 getgid. The 40 and 51 rows were added to x86ctx_rv64_to_x86_64_syscall and 77// shipped FIRST -- without them both would pass through to the WRONG x86 syscall 78// (sendfile / getsockname), silently, because that translator's default is `return num`. 79const SYS_CHROOT: i64 = 51 80const SYS_MOUNT: i64 = 40 81const SYS_UNSHARE: i64 = 97 82const SYS_GETUID: i64 = 174 83const SYS_GETGID: i64 = 176 84const SYS_POLL: i64 = 73 85@endif 86 87func sys_ioctl(fd: i64, request: i64, arg: i64) -> i64 { 88 return __syscall(SYS_IOCTL, fd, request, arg, 0, 0, 0) 89} 90 91// poll(2): wait for events on fds. fds points to an array of `nfds` 92// struct pollfd { i32 fd; i16 events; i16 revents } (8 bytes each). 93// timeout_ms < 0 = block forever, 0 = return immediately. Returns the 94// count of ready fds (>0), 0 on timeout, or -errno. Used by the 95// substrate's own network diagnostics (bounded non-blocking connect) 96// instead of reaching for external tools. (rv64 const = ppoll; this 97// wrapper only runs on the x86_64 target.) 98func sys_poll(fds: *u8, nfds: i64, timeout_ms: i64) -> i64 { 99 return __syscall(SYS_POLL, fds, nfds, timeout_ms, 0, 0, 0) 100} 101 102// ---- core wrappers ---- 103 104func sys_write(fd: i64, buf: *u8, count: i64) -> i64 { 105 return __syscall(SYS_WRITE, fd, buf, count, 0, 0, 0) 106} 107 108func sys_read(fd: i64, buf: *u8, count: i64) -> i64 { 109 return __syscall(SYS_READ, fd, buf, count, 0, 0, 0) 110} 111 112func sys_close(fd: i64) -> i64 { 113 return __syscall(SYS_CLOSE, fd, 0, 0, 0, 0, 0) 114} 115 116// chdir. The compiler only rv64->x86 translates CONSTANT syscall numbers (x86ctx_emit_syscall: 117// VK_CONST_INT); chdir is absent from that table, so a constant 49 falls through to x86_64 bind and a 118// constant 80 is mapped to fstat -- BOTH gave EBADF (PROBE-PROVEN by test_chdir). The documented escape 119// (nx_x86_64_ctx.nx:1004 "Runtime-computed syscall number -- load as-is") is to make op0 RUNTIME: a memory 120// load can't be folded to VK_CONST_INT, so the raw x86_64 number 80 passes through untranslated = real 121// chdir. Used by the supervisor to set a spawned daemon's CWD before execve. 0 on success, -errno on fail. 122func sys_chdir(path: *u8) -> i64 { 123 let nbox: *i64 = sys_mmap(16) as *i64 124 nbox[0] = 80 // x86_64 chdir, forced runtime so the rv64->x86 xlate is skipped 125 return __syscall(nbox[0], path as i64, 0, 0, 0, 0, 0) 126} 127 128// getcwd -- SAME runtime-number escape as sys_chdir directly above, for the same documented reason: the 129// rv64->x86 translator only rewrites CONSTANT syscall numbers, and getcwd is absent from that table, so a 130// constant would be mangled exactly as chdir's was. A memory load cannot be folded to VK_CONST_INT, so the 131// raw x86_64 number passes through untranslated. 132// WHY THIS EXISTS (2026-08-14): the shim had sys_chdir but NOTHING to ask where we are. Every organ that 133// resolves a path against the CWD could therefore only print a RELATIVE path -- a claim whose truth depends 134// on invisible state. Three separate working-directory faults in one session stayed invisible until they 135// bit, and in each the reader could not tell "the file is missing" from "I am standing somewhere else". 136// ★★★AN ORGAN THAT CANNOT REPORT WHERE IT IS CANNOT WRITE AN HONEST PATH. 137// Returns the byte length written INCLUDING the terminator, or -errno (notably -ERANGE if cap is short). 138// SYS_PATH_MAX is exported so a caller never hand-writes the size: the FIRST consumer of sys_getcwd (this 139// author, minutes after adding it) wrote `sys_mmap(4096)` and `sys_getcwd(buf, 4096)` on consecutive 140// lines -- a bare literal AND a duplicate-authored pair, the exact shape being removed elsewhere the same 141// day. ★★A NEW PRIMITIVE THAT DOES NOT EXPORT ITS OWN SIZE INVITES EVERY CALLER TO INVENT ONE. 142const SYS_PATH_MAX: i64 = 4096 // Linux PATH_MAX; getcwd returns -ERANGE below it 143// The DIRECTORY sibling of MODE_0644, added on the same evidence: `0x1ed` appears at 569 sites in 144// buildroot/runtime (nx_shelltool, corpus_complete=1), i.e. the estate scatters TWO file-mode constants, 145// not one. Named here so the pair lives together and a reader meets both at the same place. 146const MODE_0755: i64 = 0x1ed // rwxr-xr-x : default mode for a created directory 147func sys_getcwd(buf: *u8, cap: i64) -> i64 { 148 let nbox: *i64 = sys_mmap(16) as *i64 149 nbox[0] = 79 // x86_64 getcwd, forced runtime so the rv64->x86 xlate is skipped 150 return __syscall(nbox[0], buf as i64, cap, 0, 0, 0, 0) 151} 152 153// ⚠AT_FDCWD MOVED UP 2026-07-20 -- IT WAS A LIVE MISCOMPILE. This const was declared ~60 lines BELOW 154// (in the openat block) while sys_unlinkat and sys_fchmodat immediately below REFERENCE it. A module 155// const referenced ABOVE its declaration does not resolve, and nx_cc silently substituted CONSTANT 0 156// -- so both wrappers passed dirfd=0 (stdin) instead of -100. Absolute paths survive that (openat 157// ignores dirfd when the path is absolute), RELATIVE paths do not, which is exactly why unlinkat was 158// long recorded as flaky and "passing only by luck". Surfaced by the new unknown-identifier 159// diagnostic, which turned a silent 0 into a compile error. LAW (already banked, now enforced): 160// module-wide consts/statics go ABOVE every possible reader. 161const AT_FDCWD: i64 = -100 162 163// unlinkat(AT_FDCWD, path, 0) -- delete a file. x86_64 263 is a PROVEN pass-through (not an rv64 key), 164// but this is THE canonical home: 5+ organs hand-rolled `__syscall(263,...)` before this landed (DRY, 165// 2026-07-20). 0 on success, -errno on fail. 166func sys_unlinkat(path: *u8) -> i64 { 167 return __syscall(263, AT_FDCWD, path as i64, 0, 0, 0, 0) 168} 169 170// fchmodat(AT_FDCWD, path, mode) -- chmod by path. ⚠a CONSTANT 268 gets rv64->x86 TRANSLATED to the 171// wrong syscall (silent no-op chmod -- cost a vacuous-permission-test debug cycle, 2026-07-20), so the 172// number is forced RUNTIME via the sys_chdir nbox pattern. 0 on success, -errno on fail. 173func sys_fchmodat(path: *u8, mode: i64) -> i64 { 174 let nbox: *i64 = sys_mmap(16) as *i64 175 nbox[0] = 268 // x86_64 fchmodat, forced runtime so the xlate is skipped 176 return __syscall(nbox[0], AT_FDCWD, path as i64, mode, 0, 0, 0) 177} 178 179// exit_group(2) -- terminate ALL tasks in the thread group. Raw x86_64 231 180// (231 is NOT an rv64 key in the compiler's swap table, so it passes through 181// untranslated -- the munmap-11 precedent). THE explicit program-exit call 182// once a process holds live nx_thread_pool workers: CLONE_VM tasks are 183// separate PIDs, so plain sys_exit (93 -> x86 60, single task) leaves them 184// running, holding stdout open and wedging any pipeline that waits for EOF 185// (found 2026-07-07: the shared-pool matmul dispatcher hung the build lane 186// this way). Return-from-main already exit_groups via the _start trampoline; 187// use THIS for explicit early program exit. Per-THREAD exit stays sys_exit 188// (see nx_thread_exit). 189func sys_exit_group(code: i64) -> i64 { 190 return __syscall(231, code, 0, 0, 0, 0, 0) 191} 192 193// setpriority(PRIO_PROCESS=0, who=0 -> SELF, prio) -- x86_64 syscall 141. 194// Lower priority = larger nice value; 19 is the maximum yield. 195// WHY A WRAPPER AND NOT AN OPERATOR STEP (measured 2026-07-30): a bulk media 196// migration walk saturated the NAS; every forked organ queued behind its I/O so 197// EVERY agent MCP call 503'd for minutes -- the control plane went blind while a 198// background job did exactly what it was told. `renice 19` on the running pid 199// restored interactive service at once. 200// LAW: a long-running BULK job must yield to the interactive control plane BY 201// CONSTRUCTION at its own launch, not when an operator notices. Bind it to the 202// one act every bulk job performs (its startup) and nothing has to remember it. 203// WARN: `ionice` does NOT exist on the Synology busybox, so the I/O-class lever 204// is unavailable; CPU nice sufficed because the walk is SHA-256-bound over 205// cached reads (state R, not D, once niced). 206func sys_setpriority(prio: i64) -> i64 { 207 return __syscall(141, 0, 0, prio, 0, 0, 0) 208} 209 210// ADDITIVE TWIN 2026-08-04 (nx_resgov): re-nice ANOTHER process by pid. The incumbent above pins 211// who=0 = "me", so it cannot deprioritise a runaway -- and a governor that can only slow ITSELF has 212// no graceful rung between "observe" and "kill". PRIO_PROCESS=0, who=pid. Existing callers untouched 213// (rule 19: add the new entry point, never re-shape the one in service). 214func sys_setpriority_of(pid: i64, prio: i64) -> i64 { 215 return __syscall(141, 0, pid, prio, 0, 0, 0) 216} 217 218// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 219// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 220// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 221// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 222// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 223// ===== SMALL-ALLOCATION BUMP ARENA (2026-08-06, debt 1785516350 / 1786055008) ===================== 224// MEASURED FIRST, THEN BUILT. nx_arena_probe: 20,000 x sys_mmap(32) -> VmSize 80,172 kB, 225// VmRSS 80,024 kB. 640 KB of requested data cost 78 MB of RESIDENT memory -- 4096 bytes per 32-byte 226// request, exactly one page and one kernel VMA each. Across the corpus nx_mmapbal deep counts 17,157 227// functions / 43,498 sites that allocate and never return, so this multiplier is the actual shape of 228// the leak: the call sites are not individually wrong so much as individually EXPENSIVE. 229// 230// One VMA per call is also a HARD CORRECTNESS CEILING, not just a memory cost: vm.max_map_count 231// defaults to 65530, after which mmap returns -ENOMEM and callers write through the failed pointer. 232// That is precisely the dmesg-proven nx_hostctl SEGFAULT at 0xfffffffffffffff4 described below. 233// 234// SO: requests <= NXA_SMALL_MAX are bump-allocated out of a 256 KiB chunk (one VMA per ~5,400 small 235// allocations instead of one per allocation). Larger requests take the ORIGINAL path untouched -- 236// they are the ones plausibly relying on page alignment, and they are not where the leak lives. 237// 238// THE ZEROING CONTRACT IS LOAD-BEARING AND IS PRESERVED BY NEVER RECYCLING. Callers rely on mmap 239// returning zeroed memory (nx_mmapbal: "mmap zeroes, so an untouched slot reads empty with no init 240// loop"). Bytes handed out here come from a freshly mmapped chunk and are NEVER handed out twice, so 241// every region is zero-filled exactly as before. LIFO give-back on munmap was deliberately REJECTED: 242// it would recover memory but hand back dirty bytes, silently breaking every caller that trusts the 243// zero -- a correctness regression traded for a memory win, which is the wrong trade. 244// 245// KNOWN TRADE-OFF, stated rather than hidden: small allocations are now ADJACENT within a chunk 246// instead of isolated in their own pages. An overrun that today walks off the end of a page and 247// SIGSEGVs loudly may instead corrupt a neighbouring allocation quietly. NXA_GAP puts slack between 248// allocations and NXA_SMALL_MAX is kept deliberately low to bound the exposure, but the risk is real 249// and is the reason this starts at 256 rather than a page. 250// ---- MEMORY ORDERING, THE ONE DEFINITION ------------------------------------------------------- 251// Moved here from nx_atom.nx on 2026-08-25 and DELETED from its two other copies 252// (nx_atomic_intrinsic_test, nx_simd_i32x8_test). Measured before the move, corpus_complete=1: 253// THREE files each declared NX_MO_SEQ_CST = 5 independently. A constant written in three places is 254// three rulers that agree until one of them does not. 255// 256// They live at THIS layer because the arena allocator below needs an ordering value for its own 257// lock, and this file cannot import nx_atom.nx -- nx_atom imports THIS file, so that direction is a 258// cycle. Everything that had these constants still has them: nx_atom.nx imports this file, and so 259// does every consumer of nx_atom. 260// 261// The __atomic_* forms these feed are COMPILER INTRINSICS, not library calls, so this file can use 262// them with no import at all. Verified in nx_x86_64_ctx rather than assumed: __atomic_cas_i64 emits 263// `lock cmpxchgq`, __atomic_faa_i64 emits `lock xaddq`, __atomic_fence emits `mfence`. On x86-64 the 264// ordering operand is not consulted by the emitter because those instructions are full barriers 265// regardless; it is carried for the RV64A backend, where it selects the aq/rl bits. 266const NX_MO_RELAXED: i64 = 0 267const NX_MO_CONSUME: i64 = 1 268const NX_MO_ACQUIRE: i64 = 2 269const NX_MO_RELEASE: i64 = 3 270const NX_MO_ACQ_REL: i64 = 4 271const NX_MO_SEQ_CST: i64 = 5 272 273const NXA_SMALL_MAX: i64 = 256 274const NXA_CHUNK: i64 = 262144 275const NXA_ALIGN: i64 = 16 276const NXA_GAP: i64 = 16 277const NXA_STATE: i64 = 4096 278// RING CANARY (temporary diagnostic): the single-slot canary checked only the immediately 279// previous allocation and reported ZERO overruns -- but the bisection proved the write is 280// DELAYED, landing after later allocations have been served. Track the last NXA_RING 281// allocations and re-verify every one of them on each call. Lives at i64 slot NXA_RBASE in 282// the state page; the reporter borrows bytes 64/128, so 512 is clear of it. 283const NXA_RING: i64 = 128 284const NXA_RBASE: i64 = 64 285// ---- ARENA MARK/RESET (2026-08-12, additive; the durable fix for bump-without-reset). The arena 286// abandons a full chunk on rollover, so a long-running accept loop accumulates chunks into one giant 287// coalesced VMA (hub_gw MEASURED 3.4GB over 64k requests). A daemon marks the arena AFTER startup and 288// resets at its accept-loop's quiescent point; reset munmaps every chunk allocated since the mark and 289// zeroes the marked chunk's reclaimed tail, so per-request small allocations reuse a bounded slab. 290// State slots (state page is 512 i64): [3]=chunk_count [4]=mark_valid [5]=mark_bump [6]=mark_chunk_end 291// [7]=mark_chunk_count; the chunk-base list lives at slots NXA_CHUNKBASE..+NXA_CHUNKMAX (clear of the 292// ring at 64..320 and the reporter scratch below 64). CONTRACT: the caller guarantees NO arena 293// allocation made after the mark is still referenced at reset (the accept-loop top, where the previous 294// request's frames have all returned -- the same quiescent point ss_cache_reap already uses). LARGE 295// (>NXA_SMALL_MAX) allocations take their own VMA and are NOT tracked here; a per-request large mmap 296// still needs its own munmap. Untracked-overflow (>NXA_CHUNKMAX chunks between resets) degrades to the 297// old leak for the excess, never corrupts. 298// ---- ARENA MUTUAL EXCLUSION (2026-08-25) ------------------------------------------------------- 299// THE DEFECT: the bump-pointer advance below was a plain read-modify-write -- 300// let p: i64 = nxa_st[0] 301// nxa_st[0] = p + need 302// -- so two threads that read nxa_st[0] before either wrote it BOTH RECEIVE THE SAME POINTER and 303// then write over each other. The chunk refill, the ring-canary scan and the nxa_st[2] counter have 304// the same shape. MEASURED while shipping structured concurrency: eight pool workers calling a 305// helper that allocates a 16-byte timespec raced this cursor and produced ARENA-OVERRUN 306// prev_alloc_size=16 followed by SIGSEGV. It generalises to EVERY small allocation from more than 307// one thread, which is why the scoped-spawn child body was written to allocate nothing at all. 308// 309// WHY A LOCK AND NOT A LOCK-FREE BUMP. A fetch-and-add on the cursor fixes only the fast path; two 310// threads can still both observe the chunk exhausted and both refill, and the canary ring and the 311// counter would still race. One lock over the whole mutable region is correct by inspection, which 312// on the allocator that every organ in the estate calls is worth more than a clever fast path. 313// THE COST IS NOT THE DOMINANT COST HERE: this function ALREADY walks all NXA_RING canary slots on 314// every allocation, so one uncontended `lock cmpxchgq` is far below the noise of work already done. 315// 316// SLOT 4 IS FREE BY THE LAYOUT ABOVE: [0] cursor, [1] limit, [2] ring counter, [3] chunk count, and 317// the ring starts at NXA_RBASE=64. It is also clear of the byte-64 and byte-128 scratch that 318// nxa_report_overrun formats digits into (slots 8 and 16), which slot 4 (bytes 32-39) does not touch. 319const NXA_LOCK: i64 = 4 320// A BOUND ON AN UNKNOWABLE WAIT, DERIVED RATHER THAN PICKED, AND ITS EXHAUSTION ANNOUNCES. The 321// longest thing the critical section can do is the NXA_RING canary scan plus one mmap, so a spin far 322// beyond that is not contention -- it is a holder that is never coming back. Eight times the ring 323// gives an order of magnitude of headroom over the longest legitimate hold; on reaching it the 324// allocator SAYS SO on stderr once and keeps waiting, because hanging visibly is recoverable and 325// corrupting silently is not, and dying inside the allocator would take down a process that may be 326// merely slow. 327const NXA_LOCK_WARN: i64 = NXA_RING * 8 328// Slot 5: "the contention hint has already been printed by this process". Also free by the layout 329// above and clear of every scratch region. It is a FLAG, not a counter, and it is set through a CAS 330// so the once-ness is itself race-free rather than depending on the lock it reports about. 331const NXA_LOCK_WARNED: i64 = 5 332 333const NXA_CHUNKBASE: i64 = 320 334const NXA_CHUNKMAX: i64 = 192 335 336// [0] = next free byte, [1] = one past the end of the current chunk. A static POINTER to a real 337// mmapped page rather than scalar statics, matching the idiom the corpus already proves; the state 338// page is taken through __syscall directly so this can never recurse into itself. 339static nxa_st: *i64 340 341// munmap -- free a region from sys_mmap. x86_64 munmap = 11; 11 is NOT an rv64 number in the compiler's 342// swap table, so the literal passes through untranslated = real munmap (unlike chdir, where rv64 80=fstat 343// intercepted it). CRITICAL for long-running loops: the supervisor's per-poll proc_* scans mmap 64KB+ each; 344// unfreed, the leak hits DSM's RLIMIT_AS -> mmap returns -12 -> the code writes through it -> SEGFAULT 345// (dmesg-proven: nx_hostctl segfault at 0xfffffffffffffff4). Free scan buffers to keep the supervisor alive. 346// 347// A small len means the region came from the bump arena above, because sys_mmap routes by the SAME 348// threshold. Unmapping an interior pointer would tear a hole in a chunk still holding other callers' 349// live allocations, so it is a no-op here. Balanced small callers therefore no longer return memory -- 350// but they now cost ~48 bytes instead of 4096, so the arena wins by two orders of magnitude even 351// against code that was already correct. 352// Matching release for sys_mmap_try and other whole kernel mappings. 353// Never pass an arena allocation from sys_mmap: its small pointers may be interior. 354// Preserve the requested mapping length; the kernel applies its page rounding. 355const NXA_MAP_INVALID:i64=0-22 // Linux EINVAL, a protocol value rather than a resource budget. 356func sys_munmap_direct(addr:*u8,len:i64)->i64{ 357 if (addr as i64)<=0||len<=0{return NXA_MAP_INVALID} 358 return __syscall(11,addr as i64,len,0,0,0,0) 359} 360 361func sys_munmap(addr: *u8, len: i64) -> i64 { 362 if len <= NXA_SMALL_MAX { return 0 } 363 return __syscall(11, addr as i64, len, 0, 0, 0, 0) 364} 365 366// Seek within a file. whence: 0=SEEK_SET, 1=SEEK_CUR, 2=SEEK_END. 367// Returns new file offset on success, -errno on failure. 368func sys_lseek(fd: i64, offset: i64, whence: i64) -> i64 { 369 return __syscall(SYS_LSEEK, fd, offset, whence, 0, 0, 0) 370} 371 372// ---- FILESYSTEM SPACE: THE AXIS THE ESTATE DID NOT HAVE (2026-08-28) ----------------------------- 373// WHY THIS IS HERE AND NOT LEFT WHERE IT WAS. On 2026-08-28 a 100%-FULL DISK truncated a sibling seat's 374// MEMORY.md to 0 bytes -- open(path,"w") truncates before it writes, so a full volume does not refuse a 375// write, it DESTROYS the file. Nothing in the estate saw it coming: nx_resmon is "the resource axis 376// nx_health lacks" for MEMORY and SWAP, and a search for the disk primitive returned matches=0 for BOTH 377// sys_statfs and statvfs with corpus_complete=1. nx_res_census records the same absence in its own header. 378// The capability was not missing, it was DARK: nx_system_triage.tr_free_gb has read filesystem space since 379// 2026-06-10, in an _hdl_build organ that is NOT REGISTERED (nx_job_run refuses it as "not an unpinned 380// GREEN tool"), so the one instrument that could have warned was unreachable by any caller. 381// A CAPABILITY THAT EXISTS IN ONE UNREACHABLE ORGAN IS INDISTINGUISHABLE FROM ONE NOBODY BUILT. 382// 383// WHY THE RAW 137 AND NOT A SYS_ CONST. This file's dual-arch blocks are gated on TARGET_X86_64, which is 384// HARD-PINNED UNDEFINED, so the RV64 branch is what compiles and the x86 backend translates each number at 385// emit through x86ctx_rv64_to_x86_64_syscall -- whose default is `return num`. There is NO row for RV64 43 386// (statfs), so a SYS_STATFS=43 const would pass through unmapped to x86_64 43 = ACCEPT: a different 387// syscall, silently, on a path pointer. That is not a hypothesis -- nx_system_triage PROBE-PROVED it on 388// 2026-06-10: "rv64 43 returns -9 through the translation table; 137 raw matches df exactly." So 137 is 389// the MEASURED-CORRECT number for the target we actually emit, and it is named here ONCE instead of 390// sitting as a bare literal at each call site. 391// ⚠NAMED FOLLOW-UP, conflict-checked and deliberately NOT taken here: adding `if num == 43 { return 137 }` 392// to x86ctx_rv64_to_x86_64_syscall would make the arch-correct const work too. Nothing passes 43 as an x86 393// number (43 appears only as a translation TARGET, from RV64 202 accept), so the row is safe -- but it is a 394// COMPILER change that activates only on the next nx_cc self-host rebuild, and the working path needs none. 395// 396// 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. 397// f_bavail (not f_bfree) is the honest number for "will my write succeed": it excludes the root reserve, so 398// it reports FULLER than root would see. Wrong in the safe direction, and said out loud rather than implied. 399// ⚠THE IMPRECISION, MEASURED AND NAMED SO NOBODY LATER "FIXES" IT INTO AGREEING WITH df: this permil is 400// NOT df's Use%. df computes Used/(Used+Available), which EXCLUDES the root-reserved blocks from its 401// denominator; this computes (blocks-bavail)/blocks, which counts the reserve as used. VERIFIED against df 402// on 2026-08-28: avail_bytes came back 958449582080, which is EXACTLY df's Available of 935985920 KiB, while 403// the same volume read 113 permil here and 7% there -- both correct, measuring different things. Both reach 404// their maximum at the SAME event (bavail = 0), so a threshold calibrated against THIS metric alarms at the 405// same moment a writer actually hits the wall; it simply sits higher below that. Calibrate thresholds to 406// this definition, and do not import a df-derived number as if it were the same quantity. 407const SYS_STATFS_X86_MEASURED: i64 = 137 408const STATFS_BUF_BYTES: i64 = 144 409const STATFS_I_BSIZE: i64 = 1 410const STATFS_I_BLOCKS: i64 = 2 411const STATFS_I_BAVAIL: i64 = 4 412const STATFS_PERMIL: i64 = 1000 413const STATFS_ERR: i64 = 0 - 1 414 415// raw statfs into a caller-supplied 144-byte buffer. 0 = ok, non-zero = the kernel's negative errno. 416func sys_statfs(path: *u8, buf: *i64) -> i64 { 417 return __syscall(SYS_STATFS_X86_MEASURED, path, buf, 0, 0, 0, 0) 418} 419 420// bytes available to a non-root writer on the filesystem holding `path`; STATFS_ERR if statfs failed. 421func sys_fs_avail_bytes(path: *u8) -> i64 { 422 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 423 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 424 return buf[STATFS_I_BSIZE] * buf[STATFS_I_BAVAIL] 425} 426 427// USED per-mille of the filesystem holding `path`, counted against what a non-root writer can reach: 428// (blocks - bavail) * 1000 / blocks. STATFS_ERR if statfs failed or the volume reports zero blocks -- 429// an UNMEASURABLE volume must never read as 0 permil used, which is the most flattering possible lie. 430func sys_fs_used_permil(path: *u8) -> i64 { 431 let buf: *i64 = sys_mmap(STATFS_BUF_BYTES) as *i64 432 if sys_statfs(path, buf) != 0 { return STATFS_ERR } 433 let blocks: i64 = buf[STATFS_I_BLOCKS] 434 if blocks <= 0 { return STATFS_ERR } 435 let avail: i64 = buf[STATFS_I_BAVAIL] 436 return ((blocks - avail) * STATFS_PERMIL) / blocks 437} 438 439func sys_exit(code: i64) -> i64 { 440 return __syscall(SYS_EXIT, code, 0, 0, 0, 0, 0) 441} 442 443// mmap anonymous R/W memory; returns raw bytes. Fixed flags: 444// PROT_READ|PROT_WRITE = 3, MAP_PRIVATE|MAP_ANONYMOUS = 0x22, fd=-1. 445// FAIL-CLOSED ON A REFUSED MAPPING (2026-08-07). MEASURED: the corpus has 90,817 sys_mmap call sites 446// and SIX of them check the result -- all six in test probes whose response is sys_exit anyway. So 447// 90,811 sites take whatever this returns and write through it. When the kernel refuses, that value is 448// -errno, and the write lands at 0xfffffffffffffff4 (-12, ENOMEM). That is not a hypothetical: dmesg 449// on this host recorded it hourly in nx_web_shard_compact, and 18 times in nx_web_crawl_step. 450// Returning a poisoned pointer to 90,811 unguarded callers is the defect. Dying here is strictly safer 451// than dying there: the process ends either way, but this way there is no memory corruption first and 452// the failure is NAMED instead of arriving as a bare segfault address an operator has to decode. 453// This is the never-brick shape -- fail-safe BY CONSTRUCTION, not by every caller remembering. 454// KNOWN COST, stated: nx_mmap_probe / test_munmap deliberately provoke a refusal to observe it. They 455// now exit here with code 12 rather than printing their own verdict. Six probes lose a diagnostic; 456// 90,811 sites stop corrupting memory. 457// ===== TEMPORARY DIAGNOSTIC -- ARENA OVERRUN CANARY (2026-08-07) ===================================== 458// ⛔DO NOT BLESS A COMPILER BUILT WITH THIS. The canary writes 0xC7 into the NXA_GAP slack that a 459// caller could otherwise legitimately read as zeros, so it changes observable behaviour for any code 460// that reads past its declared size -- which is precisely the code being hunted. 461// PURPOSE: at NXA_SMALL_MAX=256 the compiler produces 14 SPURIOUS type diagnostics (it reports 462// `arg 2 is an INTEGER but the parameter is a POINTER` against a parameter DECLARED `j: *u8`), i.e. 463// something writes past its allocation and corrupts the parser's type table. At threshold 64 the same 464// requests each get a 4096-byte page whose slack absorbs it. Reading the source found nothing: the 465// two obvious suspects (nx_ir.nx:70 sys_mmap(104), nx_parse.nx:868 sys_mmap(256)) are both correctly 466// sized and bounded. So stop reading and MEASURE: stamp each small allocation's gap, verify the 467// PREVIOUS one on the next call, and print the size of whichever allocation was overrun. 468// Writes to fd 2 without allocating -- it borrows scratch inside the arena state page, because a 469// reporter that called sys_mmap would recurse into the thing it is instrumenting. 470// Dump n bytes at src to fd 2, unprintables as '.', using scratch at state+256 (the ring starts at 471// state+512 and the decimal scratch sits at +64/+128, so this cannot collide with either). n is 472// capped by callers at 48 so the buffer stays clear of the ring. 473func nxa_dump_printable(src: i64, n: i64) -> i64 { 474 let o: *u8 = ((nxa_st as i64) + 256) as *u8 475 var i: i64 = 0 476 while i < n { 477 let sp: *u8 = (src + i) as *u8 478 var c: i64 = sp[0] as i64 479 if c < 32 { c = 46 } 480 if c > 126 { c = 46 } 481 o[i] = c as u8 482 i = i + 1 483 } 484 o[n] = 10 as u8 485 sys_write(2, o, n + 1) 486 return 0 487} 488 489// FINGERPRINT (2026-08-12): the size alone + all-zeros byte dump never named the site. The ring already 490// records each allocation's REQUESTED size in counter order, so the recent size SEQUENCE fingerprints the 491// code path that was running when the overrun landed (a distinctive run of sizes is near-unique to a 492// function). Writes to fd 2 borrowing state-page scratch at bytes 320/340 (clear of the ring at byte 512, 493// the reporter decimals at 64/128, and the byte-dump at 256). No allocation -- must not recurse into sys_mmap. 494func nxa_dump_sizes() -> i64 { 495 sys_write(2, " ring_sizes(old->recent): " as *u8, 27) 496 let scr: *u8 = ((nxa_st as i64) + 320) as *u8 497 let out2: *u8 = ((nxa_st as i64) + 340) as *u8 498 let cnt: i64 = nxa_st[2] 499 var start: i64 = cnt - 32 500 if start < 0 { start = 0 } 501 var idx: i64 = start 502 while idx < cnt { 503 let slot: i64 = idx % NXA_RING 504 let szv: i64 = nxa_st[NXA_RBASE + slot * 2 + 1] 505 var m: i64 = szv 506 var k: i64 = 0 507 if m == 0 { scr[0] = 48 as u8; k = 1 } 508 while m > 0 { scr[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 509 var j: i64 = 0 510 while j < k { out2[j] = scr[k - 1 - j]; j = j + 1 } 511 out2[k] = 44 as u8 512 sys_write(2, out2, k + 1) 513 idx = idx + 1 514 } 515 sys_write(2, "\n" as *u8, 1) 516 return 0 517} 518 519func nxa_report_overrun(sz: i64, gs: i64) -> i64 { 520 let msg: *u8 = "ARENA-OVERRUN prev_alloc_size=" as *u8 521 var n: i64 = 0 522 while msg[n] != (0 as u8) { n = n + 1 } 523 sys_write(2, msg, n) 524 let b: *u8 = ((nxa_st as i64) + 64) as *u8 525 let o: *u8 = ((nxa_st as i64) + 128) as *u8 526 var m: i64 = sz 527 var k: i64 = 0 528 if m == 0 { b[0] = 48 as u8; k = 1 } 529 while m > 0 { b[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 530 var i: i64 = 0 531 while i < k { o[i] = b[k - 1 - i]; i = i + 1 } 532 o[k] = 10 as u8 533 sys_write(2, o, k + 1) 534 // The SIZE alone did not name the site (four 80-byte victims, and the two unbounded 80-byte 535 // buffers in nx_parse.nx were sized from their inputs with no effect). So show the DATA: the 536 // victim's own bytes identify the buffer, and the bytes written past its end identify the WRITER. 537 let algn: i64 = (sz + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN 538 let base: i64 = gs - algn 539 var dn: i64 = sz 540 if dn > 48 { dn = 48 } 541 sys_write(2, " own : " as *u8, 8) 542 nxa_dump_printable(base, dn) 543 sys_write(2, " over: " as *u8, 8) 544 nxa_dump_printable(gs, 16) 545 nxa_dump_sizes() 546 return 0 547} 548 549func nxa_die(msg: *u8) -> i64 { 550 var n: i64 = 0 551 while msg[n] != (0 as u8) { n = n + 1 } 552 sys_write(2, msg, n) 553 sys_exit(12) 554 return 0 555} 556 557// Address of the arena lock word. Valid only once nxa_st exists; every caller below has already 558// ensured that, and the state-page creation itself is discussed at the take site. 559func nxa_lock_addr() -> *i64 { 560 return ((nxa_st as i64) + NXA_LOCK * 8) as *i64 561} 562 563// __atomic_cas_i64 returns 1 when it wrote and 0 when it did not, so the spin condition is == 0. 564// It is a COMPILER INTRINSIC, not a call into nx_atom -- that module imports THIS file, so importing 565// it back would be a cycle. Verified in nx_x86_64_ctx rather than assumed: it lowers to a genuine 566// `lock cmpxchgq` followed by sete, which is a full barrier on x86-64 whatever ordering is passed. 567func nxa_lock_take() -> i64 { 568 var spins: i64 = 0 569 while __atomic_cas_i64(nxa_lock_addr(), 0, 1, NX_MO_ACQUIRE) == 0 { 570 spins = spins + 1 571 // Fires EXACTLY ONCE, on equality rather than on exceeding, so a genuinely long wait reports 572 // itself without turning the allocator into a log generator. 573 if spins == NXA_LOCK_WARN { 574 // ONCE PER PROCESS, not once per acquisition. MEASURED 2026-08-25 and this is a 575 // correction to the first cut of this very function: it fired on equality per CALL, and 576 // eight workers contending LEGITIMATELY produced hundreds of identical lines in a single 577 // gate run. A DIAGNOSTIC THAT FIRES CONSTANTLY IS ONE EVERY READER LEARNS TO IGNORE, and 578 // this one writes to the stderr of every organ in the estate. 579 // The threshold was derived from the longest the critical section can run, which bounds 580 // ONE hold and says nothing about QUEUE DEPTH: with N threads waiting, a legitimate wait 581 // is N holds and can exceed any per-section derivation. So this is a NOISE FLOOR for a 582 // hint, never a correctness bound -- it never fails, never delays, and never repeats. 583 // The flag is set through a CAS so the once-ness cannot itself race. 584 let wflag: *i64 = ((nxa_st as i64) + NXA_LOCK_WARNED * 8) as *i64 585 if __atomic_cas_i64(wflag, 0, 1, NX_MO_ACQ_REL) == 1 { 586 let m: *u8 = "ARENA-LOCK: sustained allocator contention seen (reported once per process; a hint, not an error -- allocation proceeds normally).\n" as *u8 587 var mn: i64 = 0 588 while m[mn] != (0 as u8) { mn = mn + 1 } 589 sys_write(2, m, mn) 590 } 591 } 592 } 593 return 0 594} 595 596func nxa_lock_give() -> i64 { 597 // nx_cc refuses a bare intrinsic statement ("computes a value and never uses it") and an atomic 598 // store has no result worth using, so it is bound and discarded -- the same shape nx_atom uses 599 // for exactly this reason. The contract is unchanged: this returns 0 either way. 600 let discarded: i64 = __atomic_store_i64(nxa_lock_addr(), 0, NX_MO_RELEASE) 601 if discarded != 0 { return 0 } 602 return 0 603} 604 605// Optional mapping for request boundaries that must report allocation refusal. 606// Unlike sys_mmap, this never aborts the process and never consumes arena storage. 607// Release successful mappings with sys_munmap_direct, not the arena-aware sys_munmap. 608// A successful reservation can still fail on later physical-memory pressure; callers 609// must not describe virtual address admission as guaranteed resident RAM. 610func sys_mmap_try(size:i64)->*u8 { 611 if size<=0 { return 0 as *u8 } 612 let mapped:i64=__syscall(SYS_MMAP,0,size,3,0x22,-1,0) 613 if mapped<=0 { return 0 as *u8 } 614 return mapped as *u8 615} 616 617func sys_mmap(size: i64) -> *u8 { 618 // Large requests keep the EXACT original behaviour, byte for byte: page-aligned, own VMA. Any 619 // caller that depends on page alignment is allocating at least a page, so the arena cannot reach 620 // it. Every failure path below also falls back to this same call, so an exhausted arena degrades 621 // to the old allocator rather than returning a bad pointer. 622 if size > NXA_SMALL_MAX { 623 let big: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 624 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) } 625 return big as *u8 626 } 627 if (nxa_st as i64) == 0 { 628 let s: i64 = __syscall(SYS_MMAP, 0, NXA_STATE, 3, 0x22, -1, 0) 629 if s <= 0 { 630 // arena state page refused -- degrade to the plain allocator, and only die if THAT fails too 631 let f1: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 632 if f1 <= 0 { nxa_die("FATAL sys_mmap: kernel refused the arena state page AND the fallback mapping (ENOMEM).\n" as *u8) } 633 return f1 as *u8 634 } 635 nxa_st = s as *i64 636 } 637 // EVERYTHING FROM HERE TO THE RETURN TOUCHES SHARED STATE: the cursor, the limit, the chunk 638 // table, the canary ring and the ring counter. It is ONE critical section because the refill 639 // decision and the bump that depends on it cannot be separated without reintroducing the race. 640 // The state page itself is created ABOVE this point, unlocked: two threads arriving there 641 // together would each map a page and one would win the static, leaking the other's 4 KiB but 642 // corrupting nothing, and in practice the arena is warm long before any thread is spawned 643 // because spawning one allocates. That residual is NAMED here rather than papered over. 644 nxa_lock_take() 645 var need: i64 = size 646 if need <= 0 { need = 1 } 647 need = (need + NXA_ALIGN - 1) / NXA_ALIGN * NXA_ALIGN + NXA_GAP 648 if nxa_st[0] + need > nxa_st[1] { 649 let c: i64 = __syscall(SYS_MMAP, 0, NXA_CHUNK, 3, 0x22, -1, 0) 650 if c <= 0 { 651 // chunk refused -- degrade to the plain allocator, and only die if THAT fails too. 652 // RELEASE FIRST: this is the one path that leaves the critical section early, and a lock 653 // held across a degraded return would wedge every other allocator in the process. 654 nxa_lock_give() 655 let f2: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x22, -1, 0) 656 if f2 <= 0 { nxa_die("FATAL sys_mmap: kernel refused an arena chunk AND the fallback mapping (ENOMEM).\n" as *u8) } 657 return f2 as *u8 658 } 659 nxa_st[0] = c 660 nxa_st[1] = c + NXA_CHUNK 661 // track the chunk base so arena_reset can munmap post-mark chunks (additive; guarded at cap). 662 if nxa_st[3] < NXA_CHUNKMAX { nxa_st[NXA_CHUNKBASE + nxa_st[3]] = c; nxa_st[3] = nxa_st[3] + 1 } 663 } 664 // ---- RING CANARY (temporary diagnostic) ---- 665 var rk: i64 = 0 666 while rk < NXA_RING { 667 let gs0: i64 = nxa_st[NXA_RBASE + rk * 2] 668 if gs0 != 0 { 669 var bi: i64 = 0 670 var bad: i64 = 0 671 while bi < 8 { 672 let bp: *u8 = (gs0 + bi) as *u8 673 if bp[0] != (199 as u8) { bad = 1; bi = 8 } else { bi = bi + 1 } 674 } 675 if bad == 1 { 676 nxa_report_overrun(nxa_st[NXA_RBASE + rk * 2 + 1], gs0) 677 nxa_st[NXA_RBASE + rk * 2] = 0 678 } 679 } 680 rk = rk + 1 681 } 682 let p: i64 = nxa_st[0] 683 nxa_st[0] = p + need 684 let gs: i64 = p + need - NXA_GAP 685 var gj: i64 = 0 686 while gj < NXA_GAP { let q: *u8 = (gs + gj) as *u8; q[0] = 199 as u8; gj = gj + 1 } 687 let slot: i64 = nxa_st[2] % NXA_RING 688 nxa_st[NXA_RBASE + slot * 2] = gs 689 nxa_st[NXA_RBASE + slot * 2 + 1] = size 690 nxa_st[2] = nxa_st[2] + 1 691 // The ONLY other exit from the critical section is the degraded chunk-refill path above, which 692 // releases before it returns. Every shared write is now behind this pair. 693 nxa_lock_give() 694 return p as *u8 695} 696 697// arena_mark: force the arena warm (so a first chunk + state page exist), then record the current 698// position as the reset barrier. Returns 1. A daemon calls this ONCE after startup, before its loop. 699func sys_arena_mark() -> i64 { 700 let warm: *u8 = sys_mmap(1) // ensures nxa_st + chunk[0] exist; the 1 byte is itself arena scratch 701 if (warm as i64) == 0 { return 0 } 702 nxa_st[4] = 1 703 nxa_st[5] = nxa_st[0] 704 nxa_st[6] = nxa_st[1] 705 nxa_st[7] = nxa_st[3] 706 return 1 707} 708 709// arena_reset: reclaim everything allocated since the mark. munmap post-mark chunks, restore the bump 710// to the mark, ZERO the marked chunk's reclaimed tail (preserves the mmap-returns-zeroed contract for 711// recycled bytes), and CLEAR the ring canary (its stamps may point into a just-munmap'd chunk, and a 712// stale deref on the next alloc would SEGV). Returns 1 on reset, 0 if no mark was set. 713func sys_arena_reset() -> i64 { 714 if (nxa_st as i64) == 0 { return 0 } 715 if nxa_st[4] != 1 { return 0 } 716 var i: i64 = nxa_st[7] 717 while i < nxa_st[3] { 718 let cb: i64 = nxa_st[NXA_CHUNKBASE + i] 719 if cb != 0 { __syscall(11, cb, NXA_CHUNK, 0, 0, 0, 0); nxa_st[NXA_CHUNKBASE + i] = 0 } 720 i = i + 1 721 } 722 nxa_st[3] = nxa_st[7] 723 nxa_st[0] = nxa_st[5] 724 nxa_st[1] = nxa_st[6] 725 var z: i64 = nxa_st[0] 726 while z < nxa_st[1] { let q: *u8 = z as *u8; q[0] = 0 as u8; z = z + 1 } 727 var r: i64 = 0 728 while r < NXA_RING * 2 { nxa_st[NXA_RBASE + r] = 0; r = r + 1 } 729 nxa_st[2] = 0 730 return 1 731} 732 733// mmap anonymous SHARED R/W memory -- ONE region that survives fork() so all 734// children see each other's writes (MAP_SHARED|MAP_ANONYMOUS = 0x21). Allocate 735// in the PARENT before fork. Foundation for the fork-per-connection video relay 736// (peers in separate children share the per-room frame table). 737func sys_mmap_shared(size: i64) -> *u8 { 738 let r: i64 = __syscall(SYS_MMAP, 0, size, 3, 0x21, -1, 0) 739 return r as *u8 740} 741 742// madvise(2) -- prefetch/advice hints for mapped ranges. MADV_WILLNEED=3 batches page-ins so a 743// serial fault loop over a cold file-backed mmap becomes parallel disk readahead (the dp-web-pub 744// stage-2 p95 fix, 2026-08-12). RAW x86_64 NUMBER 28 ON PURPOSE (sys_exit_group's raw-231 pattern): 745// the portable rv64/asm-generic number is 233 and x86ctx_rv64_to_x86_64_syscall has no 233 row in 746// the DEPLOYED compiler, so a portable const would emit x86_64 233 = epoll_ctl (the wrong-syscall- 747// not-an-error class; see the setpgid/flock rows). The 233->28 row is staged in nx_x86_64_ctx.nx and 748// activates on the next nx_cc self-host rebuild; flip this to the portable const AFTER that lands. 749// Signature bite-proven by nx_madvise_probe (0 / -12 ENOMEM / -22 EINVAL). Advisory contract: callers 750// may ignore the return value -- a failed hint costs nothing but the cold-read behaviour it hints away. 751func sys_madvise(addr: *u8, len: i64, advice: i64) -> i64 { 752 return __syscall(28, addr, len, advice, 0, 0, 0) 753} 754 755// openat flavors used by the compiler driver. AT_FDCWD = -100 (declared ABOVE, next to its first 756// reader -- see the miscompile note there; do NOT move it back down). 757// O_RDONLY = 0; O_CREAT|O_WRONLY|O_TRUNC = 0x241 on Linux RV64. 758const O_RDONLY: i64 = 0 759const O_WRONLY_CT: i64 = 0x241 // O_CREAT | O_WRONLY | O_TRUNC 760const O_WRONLY_CA: i64 = 0x441 // O_CREAT | O_WRONLY | O_APPEND 761 762func sys_openat_rd(path: *u8) -> i64 { 763 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY, 0, 0, 0) 764} 765 766// O_RDWR|O_CREAT (NO truncate) -- for offset-addressed persistent files like the metrics ring TSDB 767// (create if missing, then lseek+read/write records in place, never truncating existing history). 768const O_RDWR_CREATE: i64 = 0x42 769func sys_openat_rdwr(path: *u8, mode: i64) -> i64 { 770 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDWR_CREATE, mode, 0, 0) 771} 772 773// ★★★THE FILE MODE IS THE HALF OF THIS INTERFACE THAT WAS NEVER NAMED. The O_ flags above are named 774// consts in hex WITH a decoding comment; the mode passed beside them is a bare literal at every call 775// site. MEASURED 2026-08-14 (coverage_complete=1 corpus_complete=1 over 23,053 files): 776// - 29 organs passed the mode as a bare DECIMAL literal, which no reader decodes as rw-r--r-- 777// without stopping to convert it. ⚠THE FIRST COUNT PUBLISHED HERE WAS 26: the scan was scoped to 778// runtime/_hdl_build/ and the SUBDIRECTORY's count was published as the estate figure -- three 779// more (nx_forge_rag, nx_gpu_export, nx_bvhfk) sat one level up in runtime/. 780// ★A COUNT INHERITS THE SCOPE OF ITS SCAN, AND THE SCOPE IS THE PART NOBODY PRINTS BESIDE IT. 781// ⚠The offending call is deliberately NOT spelled out literally in this comment: prose is source 782// bytes, so writing the pattern here would make every future grep for it match this note; 783// - 10 MORE each define their OWN private 0644 const (IP_ VR_ VP_ LIVE_ FD_ FP_ WL_ PUB_ REG_ HFF_), 784// nine written 0x1a4 and one written 420 -- THE SAME CONSTANT IN TWO DIFFERENT BASES. 785// Ten seats each solved this privately and none put the answer where the next one would look. That is 786// the duplicate-ruler defect precisely: changing the estate's default artifact mode today means finding 787// 39 sites in two notations and hoping none was missed. One name, in the shim every organ already 788// imports, is the entire fix -- and it belongs HERE, beside the flags, not in a 40th private copy. 789const MODE_0644: i64 = 0x1a4 // rw-r--r-- : default mode for a generated artifact 790// rwxr-xr-x : default mode for a created DIRECTORY. A directory without the execute bit cannot be 791// traversed, so MODE_0644 is not merely stricter here -- it is wrong, and the failure surfaces later 792// as an unopenable path rather than as a refused mkdir. Named beside its sibling so the choice is a 793// lookup rather than a recollection; the estate otherwise spells this as a raw 0x1ed at every site. 794const MODE_0755: i64 = 0x1ed 795// Seconds of ZERO PROGRESS on one socket operation before an accepted connection is abandoned. 796// A single-threaded accept-loop daemon that loop-reads to Content-Length can be starved FOREVER by one 797// peer that declares a body it never finishes sending -- a one-request DoS, hostile OR merely buggy. 798// nx_dos_timeout_scan supervises the class and named 16 daemons carrying no timeout at all; the cure is 799// sys_set_socket_timeout(cfd, ACCEPT_TMO_S) folded in right after accept. 800// WHY 30 AND NOT THE 5 THE LOGIN DAEMONS USE: this bound must be wrong in the direction of SERVING, not 801// of dropping. The attack is an UNBOUNDED wait, so ANY finite bound closes it; a short one additionally 802// risks aborting a legitimate slow client. 30s of zero progress on a single recv/send is pathological 803// for every daemon in the class -- including the streaming ones, where data is flowing and the timer 804// never approaches its bound -- while still converting an infinite starvation into a bounded one. 805// It is the calibration nx_galx_bridge already uses for an accepted cfd; named here rather than copied 806// into a 16th private literal, exactly as MODE_0644 above. 807const ACCEPT_TMO_S: i64 = 30 808func sys_openat_wr(path: *u8, mode: i64) -> i64 { 809 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CT, mode, 0, 0) 810} 811 812// Linux O_WRONLY | O_CREAT | O_EXCL. An existing final component, including 813// a symlink, is a conflict; callers acquire ownership only on success. 814const O_WRONLY_CREATE_EXCLUSIVE: i64 = 0x1 | 0x40 | 0x80 815func sys_openat_exclusive(path: *u8, mode: i64) -> i64 { 816 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CREATE_EXCLUSIVE, mode, 0, 0) 817} 818 819// Linux O_DIRECTORY: require a directory, rather than merely an openable node. 820const O_DIRECTORY: i64 = 0x10000 821func sys_openat_directory(path: *u8) -> i64 { 822 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY | O_DIRECTORY, 0, 0, 0) 823} 824 825// Open path for append (create if missing). Used by append-only 826// journals such as .race_telemetry.tsv. RV64 syscall numbers; the 827// x86_64 mirror lives in nx_syscalls_x86_64.nx. 828func sys_openat_append(path: *u8, mode: i64) -> i64 { 829 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA, mode, 0, 0) 830} 831 832// Linux open ABI flags: acquire close-on-exec atomically and refuse a final 833// symlink. Nonblocking also prevents an unexpected FIFO from stalling admission. 834const O_CLOEXEC: i64 = 0x80000 835const O_NOFOLLOW: i64 = 0x20000 836const O_NONBLOCK: i64 = 0x800 837const MODE_0600: i64 = 0x180 838func sys_openat_lock(path: *u8) -> i64 { 839 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_WRONLY_CA | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK, MODE_0600, 0, 0) 840} 841 842// symlinkat(target, AT_FDCWD, linkpath) -- raw x86_64 266 forced RUNTIME (the chdir escape, same as 843// readlinkat below). THE atomic-repoint primitive for release management: create releases/current.new -> 844// sys_renameat over releases/current = an atomic symlink swap (golive/rollback are instant + crash-safe). 845// 0 on success, -errno (notably -EEXIST=-17 if linkpath exists -- create the .new name, then rename). 846func sys_symlinkat(target: *u8, linkpath: *u8) -> i64 { 847 let nbox: *i64 = sys_mmap(16) as *i64 848 nbox[0] = 266 849 let r: i64 = __syscall(nbox[0], target as i64, AT_FDCWD, linkpath as i64, 0, 0, 0) 850 sys_munmap(nbox as *u8, 16) 851 return r 852} 853 854// readlinkat(AT_FDCWD, path, buf, cap) -- raw x86_64 267 forced RUNTIME (the chdir escape: keep the 855// number out of the rv64->x86 constant-translate path). Returns link length (NO NUL appended), -errno 856// on fail. nbox is munmap'd before return: the daemon supervisor calls this hundreds of times PER CYCLE 857// (exe-identity sweeps), and a leaked page per call is exactly the VSZ-balloon class that broke fork. 858func sys_readlinkat(path: *u8, buf: *u8, cap: i64) -> i64 { 859 let nbox: *i64 = sys_mmap(16) as *i64 860 nbox[0] = 267 861 let r: i64 = __syscall(nbox[0], AT_FDCWD, path as i64, buf as i64, cap, 0, 0) 862 sys_munmap(nbox as *u8, 16) 863 return r 864} 865 866// Atomically replace newpath with oldpath (rename(2) on one filesystem: a concurrent reader sees the 867// whole old file or the whole new file, never a torn read). The S-class content-publish primitive: 868// write the new page to a temp file, then sys_renameat(tmp, live) -> hot-swap, NO rm+ln race. 869// renameat2: rv64=276, x86_64=316, flags=0. The known-good compiler translates most rv64 syscall 870// numbers to the x86_64 target but its table MISSES 276 -- verified 2026-06-14 via nx_rename_probe: 871// raw 276 -> -EINVAL (lands on x86_64 `tee`), raw 316 -> renames OK. That silently broke every 872// cst_write_atomic publish (page.html.new written, never swapped in). Try the x86_64 number first 873// (works on every x86_64 build incl. known-good); fall back to the rv64 number for native-rv64 or 874// translating compilers that do map it. flags=0 so renameat2 == renameat semantics. 875func sys_renameat(oldpath: *u8, newpath: *u8) -> i64 { 876 let r: i64 = __syscall(316, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 877 if r == 0 { return 0 } 878 return __syscall(276, AT_FDCWD, oldpath, AT_FDCWD, newpath, 0, 0) 879} 880 881// fsync(2): flush file (or directory) data+metadata to stable storage. 882// PROBE-PROVEN 2026-06-10 (_fsync_probe): rv64 82 is NOT in the compiler's 883// translation table (lands on x86 rename -> -EFAULT both ways); direct 884// x86_64 74 passes through raw (the unlinkat-263 precedent) and behaves as 885// fsync (0 on a valid fd, -9 EBADF on a bad one). Storage commit points 886// fsync the data files AND their directory around rename(2) so a committed 887// segment survives power loss, not just process death. 888func sys_fsync(fd: i64) -> i64 { 889 return __syscall(74, fd, 0, 0, 0, 0, 0) 890} 891 892// flock(2): BSD-style whole-file ADVISORY lock. rv64 32 -> x86_64 73 via the compiler's 893// x86ctx_rv64_to_x86_64_syscall table (nx_x86_64_ctx.nx:961, PROVEN LIVE in flock_deploy.log). 894// op: SYS_LOCK_SH=1 / SYS_LOCK_EX=2 / SYS_LOCK_NB=4 (OR) / SYS_LOCK_UN=8. Returns 0 on success, 895// -errno on failure. Used by the framed-append durability floor to serialize the write-until- 896// complete loop so a partial/short write under contention can NEVER misalign a concurrent appender 897// (O_APPEND single-write atomicity is necessary but not sufficient on every fs -- the lock makes 898// the whole framed record write atomic against other lockers). Additive: no existing caller in 899// this file changes. NOTE: nx_flock.nx is a separate organ importing the LEGACY "syscalls.nx" 900// name; this wrapper lives HERE so organs already on nx_syscalls.nx (e.g. nx_framed_append) get 901// flock without a second import (double-import rc=6 trap). 902const SYS_LOCK_SH: i64 = 1 903const SYS_LOCK_EX: i64 = 2 904const SYS_LOCK_NB: i64 = 4 905const SYS_LOCK_UN: i64 = 8 906func sys_flock(fd: i64, op: i64) -> i64 { 907 return __syscall(32, fd, op, 0, 0, 0, 0) 908} 909 910// newfstatat(2): stat `path` into a 144-byte x86-64 struct stat at `statbuf`. x86_64 nr 262 is passed 911// DIRECTLY (the unlinkat-263 / fsync-74 precedent: stat-family rv64 numbers aren't in the compiler's 912// translation table, so a raw x86_64 number passes through untranslated). Returns 0 on success, <0 913// (e.g. -2 ENOENT) on error. st_mtim.tv_sec @ offset 88, st_mtim.tv_nsec @ 96 (the freshness channel). 914func sys_fstatat(path: *u8, statbuf: *u8) -> i64 { 915 return __syscall(262, AT_FDCWD, path, statbuf, 0, 0, 0) 916} 917 918// utimensat(2): set `path` atime+mtime from `times` (a struct timespec[2] = [atime.sec,atime.nsec, 919// mtime.sec,mtime.nsec]). x86_64 nr 280 passed DIRECTLY. A sovereign `touch`; also makes freshness 920// tests deterministic. Returns 0 on success, <0 on error. 921func sys_utimensat(path: *u8, times: *i64) -> i64 { 922 return __syscall(280, AT_FDCWD, path, times as i64, 0, 0, 0) 923} 924 925// ---- sovereign host control-plane syscalls (x86_64; single unconditional consts, 926// per the known-good-compiler @ifdef finding). The Nishi supervisor uses these to 927// manage the daemon lifecycle WITHOUT any shell (no pkill / mkdir / chmod glue). ---- 928 929// COMPILER NOTE: the known-good compiler BAKES whole function bodies by NAME for some syscalls 930// (proven via emitted .s: a function literally named sys_kill emits number 8, sys_chmod emits 155 931// -- both wrong, regardless of the const referenced). So these wrappers use NON-baked names 932// (nx_kill / nx_chmod). sys_mkdir / sys_renameat are not baked, so those keep the sys_ name. 933 934// DESIGN: __syscall takes the RV64/generic number; the compiler's x86ctx_rv64_to_x86_64_syscall table 935// (nx_x86_64_ctx.nx) translates it to the build target. So pass the RV64 number. These four were added 936// to that sovereign table 2026-06-06 (kill 129->62, mkdirat 34->258, fchmodat 53->268, renameat2 937// 276->316); x86 kill(62) had collided with rv64 lseek(62), x86 fchmodat(268) with rv64 pivot_root(268). 938 939// kill(pid, sig) -- rv64 129 -> x86_64 62. SIGTERM=15 / SIGKILL=9. Host control plane. 940func nx_kill(pid: i64, sig: i64) -> i64 { return __syscall(129, pid, sig, 0, 0, 0, 0) } 941 942// setpgid(pid, pgid) -- put a process in its own PROCESS GROUP so a killer can reach its whole 943// subtree. nx_kill(0 - pgid, sig) signals every member, not just the one process you forked. 944// A BOUND THAT ONLY REACHES THE PROCESS YOU FORKED IS NOT A BOUND ON THE WORK IT STARTED. 945// Per-target const, NOT a bare generic number: x86ctx_rv64_to_x86_64_syscall translates only the 946// numbers it knows and FALLS THROUGH for the rest. MEASURED on the laptop lane 2026-08-10: a bare 947// generic 154 reached x86_64 as 154 and returned -38 (ENOSYS), silently -- and a fix built on it 948// reproduced the original bug exactly. Callers must treat setpgid as BEST-EFFORT. 949@ifdef TARGET_X86_64 950const SYS_SETPGID: i64 = 109 951@endif 952@ifndef TARGET_X86_64 953const SYS_SETPGID: i64 = 154 954@endif 955func sys_setpgid(pid: i64, pgid: i64) -> i64 { return __syscall(SYS_SETPGID, pid, pgid, 0, 0, 0, 0) } 956 957// prlimit64(pid, resource, new_limit, old_limit) -- the Linux RESOURCE-LIMIT primitive = 958// the Job-Object ActiveProcessLimit / memory-limit analog for the sovereign supervisor (M5). 959// x86_64 prlimit64 = 302 (PASSED DIRECTLY, the unlinkat-263 / fsync-74 / fstatat-262 960// precedent: a raw x86_64 number not in the compiler's rv64->x86 swap table passes through 961// untranslated). NOTE: rv64 prlimit64 IS 261 but x86_64 261 = futimesat -- so the naive 962// "261 is the same on both" is WRONG (PROBE-PROVEN: 261 returned EFAULT/EINVAL because it 963// hit futimesat); the build target here is x86_64, so we emit 302 directly. pid=0 => the 964// calling process (a forked child caps ITSELF before running its payload). new_limit / 965// old_limit each point at a struct rlimit64 { rlim_cur: i64, rlim_max: i64 } (16 bytes); 966// pass 0 for old_limit to skip read-back. Returns 0 on success, -errno (e.g. -1 EPERM if 967// raising a hard limit unprivileged) on failure. NON-baked name (the compiler bakes some 968// sys_* bodies by name; the nx_ prefix avoids that trap). 969func nx_prlimit(pid: i64, resource: i64, new_limit: *u8, old_limit: *u8) -> i64 { 970 return __syscall(302, pid, resource, new_limit as i64, old_limit as i64, 0, 0) 971} 972 973// RLIMIT resource ids (Linux generic; identical rv64/x86_64). RLIMIT_AS = address-space 974// (virtual memory) cap -- the cleanest userspace-settable "memory budget" for a supervised 975// job. RLIMIT_CPU = CPU-seconds cap. WNOHANG=1 = wait4 non-blocking liveness poll option. 976const RLIMIT_CPU: i64 = 0 977const RLIMIT_AS: i64 = 9 978const WNOHANG: i64 = 1 979 980// mkdirat -- rv64 34 -> x86_64 258. Create a doc-root directory. mode e.g. 0x1ed (0755). 981func sys_mkdir(path: *u8, mode: i64) -> i64 { return __syscall(34, AT_FDCWD, path, mode, 0, 0, 0) } 982 983// fchmodat -- rv64 53 -> x86_64 268. +x a freshly-deployed daemon binary (mode 0x1ed). flags=0. 984func nx_chmod(path: *u8, mode: i64) -> i64 { return __syscall(53, AT_FDCWD, path, mode, 0, 0, 0) } 985 986// setsid -- x86_64 = 112 (not in the rv64->x86 table, so the literal passes through). Detach a forked 987// process into a NEW session so it survives the SSH/parent close -- sovereign daemonization (no shell setsid). 988func nx_setsid() -> i64 { return __syscall(112, 0, 0, 0, 0, 0, 0) } 989 990// CLOCK_MONOTONIC = 1. ts is 16 bytes {sec: i64, nsec: i64}. 991// Returns 0 / -errno. 992func sys_clock_gettime_mono(ts: *i64) -> i64 { 993 return __syscall(SYS_CLOCK_GETTIME, 1, ts, 0, 0, 0, 0) 994} 995 996// CLOCK_REALTIME = 0 -- wall-clock seconds since the Unix epoch. Use 997// this (NOT monotonic) for anything that must match calendar time: 998// X.509 notBefore/notAfter, logs, TLS timestamps. Monotonic returns 999// time-since-boot, which encodes as ~1970 when (mis)used as an epoch. 1000func sys_clock_gettime_real(ts: *i64) -> i64 { 1001 return __syscall(SYS_CLOCK_GETTIME, 0, ts, 0, 0, 0, 0) 1002} 1003 1004// Wall-clock seconds since the Unix epoch. 1005func sys_now_realtime_sec() -> i64 { 1006 let ts: *i64 = sys_mmap(16) as *i64 1007 sys_clock_gettime_real(ts) 1008 return ts[0] 1009} 1010 1011// Wall-clock milliseconds since the Unix epoch. 1012func sys_now_realtime_ms() -> i64 { 1013 let ts: *i64 = sys_mmap(16) as *i64 1014 sys_clock_gettime_real(ts) 1015 return ts[0] * 1000 + ts[1] / SYS_MAGIC_1000000 1016} 1017 1018// Wall-clock MICROSECONDS since the Unix epoch -- the CROSS-MACHINE stamp. 1019// ★ Use this, never sys_now_us(), for any value one machine writes and ANOTHER machine judges 1020// (fleet beats, lease expiry, telemetry rows). Monotonic counts from each machine's OWN boot, so 1021// subtracting one node's monotonic stamp from another's monotonic now yields the difference of two 1022// unrelated boot epochs -- the remote row then reads as ancient (or future-forged) and a freshness 1023// guard rejects every honest remote node while looking like it is working. 1024func sys_now_realtime_us() -> i64 { 1025 let ts: *i64 = sys_mmap(16) as *i64 1026 sys_clock_gettime_real(ts) 1027 return ts[0] * SYS_MAGIC_1000000 + ts[1] / 1000 1028} 1029 1030// Convenience: monotonic time in milliseconds. Caller does not own 1031// the timespec buffer -- it is mmap'd once per call (cheap; the 1032// underlying syscall already costs more than the page fault). 1033func sys_now_ms() -> i64 { 1034 let ts: *i64 = sys_mmap(16) as *i64 1035 sys_clock_gettime_mono(ts) 1036 let sec_part: i64 = ts[0] * 1000 1037 let nsec_part: i64 = ts[1] / SYS_MAGIC_1000000 1038 return sec_part + nsec_part 1039} 1040 1041// Convenience: monotonic time in microseconds. Used by per-request 1042// elapsed-time tracking in search engines + benches where ms is too 1043// coarse. Same caller-ownership rules as sys_now_ms. 1044func sys_now_us() -> i64 { 1045 let ts: *i64 = sys_mmap(16) as *i64 1046 sys_clock_gettime_mono(ts) 1047 let sec_part: i64 = ts[0] * SYS_MAGIC_1000000 1048 let nsec_part: i64 = ts[1] / 1000 1049 return sec_part + nsec_part 1050} 1051 1052// Alias used by nx_search_onsite_engine etc. Matches `_us` naming 1053// convention. Substrate-canonical name is sys_now_us; this alias 1054// preserves existing call sites without churn. 1055func sys_clock_now_us() -> i64 { 1056 return sys_now_us() 1057} 1058 1059// Read the entire file at `path` into a fresh mmap'd buffer. Returns 1060// a null-terminated *u8 plus writes the byte count to *out_len. On 1061// error (open failure, oversize) returns null and leaves out_len = 0. 1062// Uses a fixed 1 MiB buffer for the first pass; larger sources need a 1063// growth loop. 1064// ---- process control (Linux RV64) ---------------------------- 1065// 1066// Lets NishiLang programs spawn other processes -- prerequisite 1067// for replacing shell scripts (f6_gate.sh) with .nx equivalents. 1068// NishiOS will expose a different process model (capability-based); 1069// these wrappers are the Linux-host compatibility layer. 1070 1071@ifdef TARGET_X86_64 1072const SYS_CLONE: i64 = 56 1073const SYS_EXECVE: i64 = 59 1074const SYS_WAIT4: i64 = 61 1075const SYS_PIPE2: i64 = 293 1076const SYS_DUP3: i64 = 292 1077@endif 1078 1079@ifndef TARGET_X86_64 1080const SYS_CLONE: i64 = 220 1081const SYS_EXECVE: i64 = 221 1082const SYS_WAIT4: i64 = 260 1083const SYS_PIPE2: i64 = 59 1084const SYS_DUP3: i64 = 24 1085@endif 1086 1087// Clone flags (subset). CLONE_VFORK blocks parent until child 1088// exec's or exits, matching fork() semantics closely enough for 1089// our spawn-then-wait patterns. 1090const CLONE_VM: i64 = 0x00000100 1091const CLONE_VFORK: i64 = 0x00004000 1092const SIGCHLD: i64 = 17 1093 1094// Create a child process via Linux clone(). Returns: 1095// > 0 in the parent: child PID 1096// == 0 in the child: child should exec or exit 1097// < 0 on error: -errno 1098// Uses SIGCHLD as the signal that parent receives on child exit 1099// (the libc fork() default); no shared memory or thread flags. 1100// ---- namespace / container family (debt 1785528831) ---------------- 1101// Moved here from nx_syscalls_x86_64.nx so ONE module owns the wrapper set. Their 1102// absence here is why nx_container.nx had to import that module as a SECOND syscall 1103// layer, which put every wrapper in the TU twice and let definition ORDER pick the 1104// winner, silently, until the duplicate-definition guard made it fail closed. 1105func sys_unshare(flags: i64) -> i64 { 1106 return __syscall(SYS_UNSHARE, flags, 0, 0, 0, 0, 0) 1107} 1108func sys_mount(source: *u8, target: *u8, fs_type: *u8, mountflags: i64, data: *u8) -> i64 { 1109 return __syscall(SYS_MOUNT, source, target, fs_type, mountflags, data, 0) 1110} 1111func sys_chroot(path: *u8) -> i64 { 1112 return __syscall(SYS_CHROOT, path, 0, 0, 0, 0, 0) 1113} 1114func sys_getuid() -> i64 { 1115 return __syscall(SYS_GETUID, 0, 0, 0, 0, 0, 0) 1116} 1117func sys_getgid() -> i64 { 1118 return __syscall(SYS_GETGID, 0, 0, 0, 0, 0, 0) 1119} 1120 1121func sys_fork() -> i64 { 1122 return __syscall(SYS_CLONE, SIGCHLD, 0, 0, 0, 0, 0) 1123} 1124 1125// Replace the current process image. `path` is the executable 1126// (absolute or in $PATH if the child first does a fresh clone). 1127// `argv` is a null-terminated array of *u8 (already-marshalled). 1128// `envp` same shape, or null for "inherit parent's env". 1129// Only returns on failure (-errno). 1130// EXEC WITH A CLEAN FD TABLE (seq1785451144). A child inherits every fd its parent held, INCLUDING 1131// listen sockets, across fork AND execve. That is how nx_opaque_login came to hold mgmt s :18098 1132// alongside mgmt itself -- two listeners on one port, connections split between them, a VALID route 1133// answering 404 on some requests. There is no error anywhere in that state, which is why it was 1134// filed as a transport flake for months. 1135// ADDITIVE ON PURPOSE: sys_execve is left byte-identical (910 call sites across 719 files -- a 1136// global change there is unverifiable in one session). Spawners opt in by calling THIS instead. 1137// AUDIT THAT MAKES IT SAFE: zero call sites in the tree dup3 to a target fd above 2, so no exec d 1138// child is deliberately handed a high fd; 0/1/2 are preserved untouched. 1139// Linux child lifetime binding: call in the freshly forked child, before exec. 1140// The expected parent PID is captured before fork, closing the pre-arm death race. 1141// Kernel semantics bind to the creating thread; privileged exec can clear this. 1142const NX_SYS_PRCTL: i64 = 167 1143const NX_PR_SET_PDEATHSIG: i64 = 1 1144const NX_PR_SET_CHILD_SUBREAPER: i64 = 36 1145func sys_prctl(option: i64, arg: i64) -> i64 { 1146 return __syscall(NX_SYS_PRCTL,option,arg,0,0,0,0) 1147} 1148func sys_bind_parent_lifetime(expected_parent: i64, signal: i64) -> i64 { 1149 if expected_parent <= 0 || signal <= 0 { return 0-22 } 1150 let armed: i64=sys_prctl(NX_PR_SET_PDEATHSIG,signal) 1151 if armed < 0 { return armed } 1152 let parent: i64=__syscall(173,0,0,0,0,0,0) 1153 if parent != expected_parent { return 0-10 } 1154 return 0 1155} 1156 1157// Linux waitid observes termination without releasing the child's PID when WNOWAIT is set. 1158// Portable syscall 95 requires the matching x86 backend translation to 247. 1159const SYS_WAITID_PORTABLE: i64 = 95 1160const NX_WAIT_P_PID: i64 = 1 1161const NX_WAIT_EXITED: i64 = 4 1162const NX_WAIT_NOWAIT: i64 = 0x01000000 1163const NX_WAIT_SIGINFO_BYTES: i64 = 128 1164func sys_waitid(idtype: i64, id: i64, info: *u8, options: i64) -> i64 { 1165 return __syscall(SYS_WAITID_PORTABLE,idtype,id,info as i64,options,0,0) 1166} 1167 1168// Post-fork only: the child owns its descriptor table. The buffer bounds a 1169// getdents batch, never the descriptor numbers or number of open handles. 1170const NX_FD_DENT_BUFFER: i64 = 4096 1171const NX_SYS_CLOSE_RANGE: i64 = 436 // Linux x86_64 and asm-generic ABI 1172const NX_FD_UINT_MAX: i64 = 4294967295 1173func sys_close_inherited_proc(first: i64) -> i64 { 1174 let directory: i64=sys_openat_rd("/proc/self/fd") 1175 if directory < 0 { return directory } 1176 let buf: *u8=sys_mmap(NX_FD_DENT_BUFFER) 1177 var result: i64=0 1178 var running: i64=1 1179 while running == 1 { 1180 let n: i64=sys_getdents64(directory,buf,NX_FD_DENT_BUFFER) 1181 if n == (0-4) { continue } 1182 if n <= 0 { result=n; break } 1183 var off: i64=0 1184 while off < n { 1185 if n-off < 20 { result=0-5; running=0; break } 1186 let rec: *u8=buf+off 1187 let size: i64=dirent_reclen(rec) 1188 if size < 20 || size > n-off { result=0-5; running=0; break } 1189 var i: i64=19 1190 var fd: i64=0 1191 var valid: i64=1 1192 while i < size { 1193 let c: i64=rec[i] as i64 1194 if c == 0 { break } 1195 if c < 48 || c > 57 { valid=0; break } 1196 if fd > (2147483647-(c-48))/10 { valid=0; break } 1197 fd=fd*10+c-48; i=i+1 1198 } 1199 if i == 19 || i == size { valid=0 } 1200 if valid == 1 && fd >= first && fd != directory { 1201 // Linux releases the descriptor even when close reports a late 1202 // I/O error; never retry close and risk a reused descriptor. 1203 let closed: i64=sys_close(fd) 1204 if closed < 0 && closed != (0-9) { result=closed; running=0; break } 1205 } 1206 off=off+size 1207 } 1208 } 1209 let closedir: i64=sys_close(directory) 1210 sys_munmap(buf,NX_FD_DENT_BUFFER) 1211 if result == 0 && closedir < 0 { result=closedir } 1212 return result 1213} 1214func sys_close_inherited(first: i64) -> i64 { 1215 if first < 0 { return 0-22 } 1216 let rc: i64=__syscall(NX_SYS_CLOSE_RANGE,first,NX_FD_UINT_MAX,0,0,0,0) 1217 if rc == (0-38) { return sys_close_inherited_proc(first) } 1218 return rc 1219} 1220func sys_execve_clean(path: *u8, argv: *i64, envp: *i64) -> i64 { 1221 let rc: i64=sys_close_inherited(3) 1222 if rc < 0 { return rc } 1223 return sys_execve(path,argv,envp) 1224} 1225 1226func sys_execve(path: *u8, argv: *i64, envp: *i64) -> i64 { 1227 return __syscall(SYS_EXECVE, path, argv, envp, 0, 0, 0) 1228} 1229 1230// Wait for a child to exit. `pid` = -1 waits for ANY child, 1231// otherwise waits for that specific PID. `status` is a caller- 1232// mmapped i64 slot: on exit the low 16 bits carry Linux's w* status 1233// flags (WIFEXITED / WEXITSTATUS). Returns the reaped child's PID 1234// or -errno. 1235func sys_wait4(pid: i64, status: *i64, options: i64) -> i64 { 1236 return __syscall(SYS_WAIT4, pid, status, options, 0, 0, 0) 1237} 1238 1239// Extract exit code from a wait4 status word. Matches the glibc 1240// WEXITSTATUS macro: bits 8-15 of the low 16. 1241func wait_exit_code(status: i64) -> i64 { 1242 return (status >> 8) & 0xFF 1243} 1244 1245// Terminating signal from a wait4 status (0 when the child exited normally). Sibling of 1246// wait_exit_code; RESTORED 2026-07-30 after a stale whole-tree push erased both it and 1247// sys_ignore_sigpipe below, while three files still CALLED them (nx_http_server, nx_sigpipe_gate, 1248// nx_tools_api_serve) -- so the tree could not build until they came back. 1249func wait_term_signal(status: i64) -> i64 { 1250 return status & 0x7f 1251} 1252 1253// THE ONE RULER for "what result code did this process actually produce". Use this, not 1254// wait_exit_code, anywhere the answer becomes a VERDICT. 1255// 1256// WHY IT EXISTS, MEASURED 2026-08-25. wait_exit_code is WEXITSTATUS and is correctly named: 1257// bits 8-15 of the status word. But a child KILLED BY A SIGNAL has no exit status at all, and 1258// those bits are ZERO -- so a SEGFAULTING process is indistinguishable from a clean exit 0 to 1259// every caller that reads only wait_exit_code. Measured live: a gate that SIGSEGV'd mid-run was 1260// served by /api/gate_run as exit_code 0, verdict GREEN. A CRASHED GATE WORE A PASS. 1261// 1262// This is not a new discovery in this estate -- and that is the point. nx_gatekit_lib's 1263// gk_wait_code already carried exactly this rule, with its own measurement recorded (two gates 1264// the 60 s watchdog KILLED journaled `GREEN exit=0 ms=60443`). It was fixed THERE in August and 1265// left unfixed in nx_tool_run, which is the shared exec primitive sitting behind /api/gate_run, 1266// /api/build and 51 other consumers. A LAW APPLIED IN ONE ORGAN AND NOT ITS SIBLING IS HALF A 1267// LAW, AND THE HALF LEFT UNDONE IS THE ONE ON THE PRODUCTION PATH. So the rule now lives HERE, 1268// beside the two accessors it is composed of, and gk_wait_code delegates to it: one ruler. 1269// 1270// Shell convention 128+signal (137 SIGKILL, 139 SIGSEGV) is deliberate: it makes the death both 1271// VISIBLE and NON-ZERO, so every existing caller that branches on rc != 0 sees it with no change. 1272// wait_exit_code is left EXACTLY as it was -- 85 call sites across the corpus (corpus_complete=1) 1273// read it, and silently redefining WEXITSTATUS under them would be the cure being worse. 1274func wait_status_rc(status: i64) -> i64 { 1275 let sig: i64 = wait_term_signal(status) 1276 if sig != 0 { return 128 + sig } 1277 return wait_exit_code(status) 1278} 1279 1280// Ignore SIGPIPE process-wide, so writing to a socket the peer already closed returns -EPIPE 1281// instead of KILLING the process. SIGPIPE default action is TERMINATE, which for a daemon means 1282// every client that walks away mid-response is an outage -- this one call at the listen primitive 1283// is inherited by all 52 consumers of nx_http_server_listen. 1284// rt_sigaction(SIGPIPE, {handler=SIG_IGN}, NULL, 8): syscall 13 on x86-64, which happens to equal 1285// the signal number. SA_RESTORER is deliberately NOT set -- the kernel consults it only when it 1286// DELIVERS a handler frame, and SIG_IGN never delivers one. 1287// PROVEN, not asserted: nx_sigpipe_gate forks a child that writes to a closed pipe and demands 1288// death-by-signal-13 WITHOUT this call and a clean -EPIPE WITH it. 1289// Restore a signal to its DEFAULT disposition. THE INVERSE OF sys_ignore_sigpipe, and it exists 1290// because SIG_IGN is inherited across BOTH fork and execve: a daemon that ignores SIGPIPE hands 1291// that ignore to every child it spawns, FOREVER. That silently corrupted verification -- the 1292// sigpipe gate reported 4/5 RED under /api/gate_run and 5/5 GREEN under a shell, same binary, 1293// same minute, because its DISEASE control (writing to a closed peer must KILL) could not be 1294// observed inside an environment where the kill was already disabled (seq1463). A harness must 1295// not change the state it is verifying; where it must, it has to hand back a clean slate. 1296// ⚠the same inheritance can also produce a FALSE GREEN, which is the far more dangerous half. 1297func sys_default_signal(sig: i64) -> i64 { 1298 let act: *i64 = sys_mmap(64) as *i64 1299 act[0] = 0 1300 act[1] = 0 1301 act[2] = 0 1302 act[3] = 0 1303 return __syscall(13, sig, act as i64, 0, 8, 0, 0) 1304} 1305 1306func sys_ignore_sigpipe() -> i64 { 1307 let act: *i64 = sys_mmap(64) as *i64 1308 act[0] = 1 1309 act[1] = 0 1310 act[2] = 0 1311 act[3] = 0 1312 return __syscall(13, 13, act as i64, 0, 8, 0, 0) 1313} 1314 1315// Create a pipe. `fds` must point at 8+ writable bytes; the kernel 1316// packs BOTH int32 fds into fds[0]: read end = low 32 bits, write end 1317// = HIGH 32 bits (fds[1] is never written -- the old comment claiming 1318// fds[1]=write-end caused a false-pass KAT + a hung gate, 2026-07-16). 1319// Extract: rfd = fds[0] & 0xffffffff; wfd = (fds[0] / 4294967296) & 1320// 0xffffffff. Returns 0 on success, -errno on failure. 1321func sys_pipe2(fds: *i64, flags: i64) -> i64 { 1322 return __syscall(SYS_PIPE2, fds, flags, 0, 0, 0, 0) 1323} 1324 1325// Duplicate `oldfd` onto `newfd`, closing `newfd` first if open. 1326// Used to wire child stdout to a pipe: dup3(pipe_write_end, 1). 1327func sys_dup3(oldfd: i64, newfd: i64, flags: i64) -> i64 { 1328 return __syscall(SYS_DUP3, oldfd, newfd, flags, 0, 0, 0) 1329} 1330 1331// ---- directory listing (Linux RV64 getdents64) --------------- 1332// 1333// Foundation for ls / glob / dir-walk helpers. Linux returns 1334// linux_dirent64 records: 1335// u64 d_ino (inode, ignored here) 1336// s64 d_off (next-record offset) 1337// u16 d_reclen (this record's byte length) 1338// u8 d_type (file type; DT_DIR=4, DT_REG=8, DT_LNK=10) 1339// char d_name[] (null-terminated name, padded so d_reclen 1340// carries us to the next record boundary) 1341// Total struct header: 19 bytes, then name up to d_reclen - 19. 1342 1343@ifdef TARGET_X86_64 1344const SYS_GETDENTS64: i64 = 217 1345@endif 1346@ifndef TARGET_X86_64 1347const SYS_GETDENTS64: i64 = 61 1348@endif 1349 1350const DT_UNKNOWN: i64 = 0 1351const DT_FIFO: i64 = 1 1352const DT_CHR: i64 = 2 1353const DT_DIR: i64 = 4 1354const DT_BLK: i64 = 6 1355const DT_REG: i64 = 8 1356const DT_LNK: i64 = 10 1357const DT_SOCK: i64 = 12 1358 1359// Raw syscall. Returns bytes written on success (0 = end-of-dir), 1360// or -errno on failure. 1361func sys_getdents64(fd: i64, buf: *u8, buf_len: i64) -> i64 { 1362 return __syscall(SYS_GETDENTS64, fd, buf, buf_len, 0, 0, 0) 1363} 1364 1365// Extract fields from a linux_dirent64 record. `rec` points at 1366// the start of the record; fields are at fixed offsets. 1367func dirent_reclen(rec: *u8) -> i64 { 1368 // d_reclen is u16 at offset 16. Read as two bytes little-endian. 1369 let lo: i64 = rec[16] 1370 let hi: i64 = rec[17] 1371 return lo | (hi << 8) 1372} 1373 1374func dirent_type(rec: *u8) -> i64 { 1375 return rec[18] 1376} 1377 1378// Pointer to the null-terminated name inside the record. 1379func dirent_name(rec: *u8) -> *u8 { 1380 let base: i64 = rec as i64 1381 return (base + 19) as *u8 1382} 1383 1384// ---- content-addressed file reader --------------------------- 1385 1386func sys_read_file(path: *u8, out_len: *i64) -> *u8 { 1387 let fd: i64 = sys_openat_rd(path) 1388 if fd < 0 { 1389 *out_len = 0 1390 return 0 as *u8 1391 } 1392 // DEBT-EATEN 2026-07-15: the old fixed 4 GiB cap SILENTLY TRUNCATED bigger files (a 9 GB gguf would 1393 // short-read into plausible-garbage tensors -- the worst failure class). Now the buffer is sized from 1394 // the file itself (lseek END), so ANY size reads fully. Physical pages still allocate on-demand. For 1395 // zero-copy any-size READ-ONLY access prefer sys_map_file (below). 1396 // DEBT-EATEN 2026-08-19 (1787076780): when the size is UNKNOWABLE (lseek END <= 0: /proc files, pipes 1397 // -- AND every empty regular file, which reports 0 just the same) this used to reserve 1398 // SYS_MAGIC_4294967296 of address space per call. Untouched pages were never resident, but the 1399 // mapping WAS: a daemon that read an empty registry every sweep ballooned its VmSize by 4 GiB per 1400 // read (measured: smoke instances at a 4.2 GB base), the leak screens flagged it, and sys_free_file 1401 // could only release what was read. The size-unknowable path now GROWS: start at SYS_READ_GROW_INIT, 1402 // double while the window fills, and hand back an EXACT mapping (total + 16) so sys_free_file 1403 // releases all of it. An empty file costs one small read and a 16-byte arena cell; /proc/stat fits 1404 // the first window; a pipe of any length still reads whole. The known-size path is unchanged. 1405 let fsz: i64 = sys_lseek(fd, 0, 2) 1406 sys_lseek(fd, 0, 0) 1407 var cap: i64 = SYS_READ_GROW_INIT 1408 var grow: i64 = 1 1409 if fsz > 0 { cap = fsz; grow = 0 } 1410 var buf: *u8 = sys_mmap(cap + 16) 1411 var total: i64 = 0 1412 var go: i64 = 1 1413 while go == 1 { 1414 let base: i64 = buf as i64 1415 let tail: *u8 = (base + total) as *u8 1416 let n: i64 = sys_read(fd, tail, cap - total) 1417 if n <= 0 { go = 0 } 1418 if n > 0 { total = total + n } 1419 if total >= cap { 1420 if grow == 0 { go = 0 } else { 1421 // the window filled and the size is unknown: double it, copy, release the old mapping 1422 let ncap: i64 = cap * 2 1423 let nb: *u8 = sys_mmap(ncap + 16) 1424 var ci: i64 = 0 1425 let obase: i64 = buf as i64 1426 let nbase: i64 = nb as i64 1427 while ci < total { let src: *u8 = (obase + ci) as *u8; let dst: *u8 = (nbase + ci) as *u8; dst[0] = src[0]; ci = ci + 1 } 1428 sys_munmap(buf, cap + 16) 1429 buf = nb 1430 cap = ncap 1431 } 1432 } 1433 } 1434 sys_close(fd) 1435 if grow == 1 { 1436 // hand back an EXACT mapping so the paired free releases everything (the doubled window would 1437 // otherwise leave its slack mapped forever -- the address-space leak this change exists to end) 1438 let xb: *u8 = sys_mmap(total + 16) 1439 var xi: i64 = 0 1440 let gbase: i64 = buf as i64 1441 let xbase: i64 = xb as i64 1442 while xi < total { let gsrc: *u8 = (gbase + xi) as *u8; let xdst: *u8 = (xbase + xi) as *u8; xdst[0] = gsrc[0]; xi = xi + 1 } 1443 sys_munmap(buf, cap + 16) 1444 buf = xb 1445 } 1446 // Null-terminate for the lexer. 1447 let bbase: i64 = buf as i64 1448 let term: *u8 = (bbase + total) as *u8 1449 term[0] = 0 1450 *out_len = total 1451 return buf 1452} 1453 1454// PAIRED FREE FOR sys_read_file (2026-08-17). sys_read_file mmaps `cap + 16` where cap is the FILE SIZE 1455// and returns only the pointer -- so any caller that frees it must know the padding, and a caller that 1456// unmaps `len` alone leaks the tail page whenever the file size sits just under a page boundary. 1457// ★A CALLER FORCED TO KNOW ITS ALLOCATOR'S PADDING IS A COUPLING THAT WILL DRIFT -- so the +16 lives 1458// HERE, beside the +16 it mirrors, instead of being retyped at every call site. 1459// Pass the length sys_read_file reported through out_len; this re-derives the mapping from it. 1460// Null-safe by construction: sys_read_file returns 0 on failure, so callers need no extra guard -- 1461// ★A FREE THAT REFUSES NULL IS A FREE NOBODY HAS TO WRAP IN AN IF. 1462// EXACT for every path since 2026-08-19: the size-unknowable fallback (lseek <= 0: /proc, pipes, empty 1463// regular files) now returns a mapping of exactly total + 16, so this releases ALL of it. (It used to 1464// map SYS_MAGIC_4294967296 of address space and release only what was read -- stated then, ended now.) 1465// WHY IT EXISTS: nx_sites_daemon serves /wiki/roadmap by calling sys_read_file PER REQUEST inside a loop 1466// that runs up to NX_SD_MAX_REQ_PER_CONN (64) times per connection and never released it -- an 8,408 B 1467// file became 3 fresh pages and a fresh kernel VMA on every hit, held until the child exited. 1468func sys_free_file(buf: *u8, len: i64) -> i64 { 1469 if (buf as i64) == 0 { return 0 } 1470 if len < 0 { return 0 } 1471 return sys_munmap(buf, len + 16) 1472} 1473 1474// Read-only FILE-BACKED map of the whole file (PROT_READ=1, MAP_PRIVATE=2): any size, zero-copy -- only 1475// touched pages become resident (the lazy-MoE shape: a 9 GB model serves in ~active-set RSS, and load 1476// time is ~0 because nothing is copied). NO NUL pad (a file mapping cannot be extended) -- BINARY 1477// consumers only; text/lexer callers keep sys_read_file. Returns 0 on failure; *out_len = file size. 1478// Read-only by construction (PROT_READ; writes fault -- Rule 26-friendly). 1479func sys_map_file(path: *u8, out_len: *i64) -> *u8 { 1480 *out_len = 0 1481 let fd: i64 = sys_openat_rd(path) 1482 if fd < 0 { return 0 as *u8 } 1483 let fsz: i64 = sys_lseek(fd, 0, 2) 1484 if fsz <= 0 { sys_close(fd); return 0 as *u8 } 1485 let r: i64 = __syscall(SYS_MMAP, 0, fsz, 1, 2, fd, 0) 1486 sys_close(fd) 1487 if r <= 0 { return 0 as *u8 } 1488 *out_len = fsz 1489 return r as *u8 1490} 1491 1492// Sleep for `ms` milliseconds against CLOCK_MONOTONIC (relative). 1493// Returns 0 on success, negative errno on failure. Caller-supplied 1494// budget: ms <= 0 is a no-op; very large values are accepted as-is 1495// (the kernel will saturate to its own clamp). Defined at the bottom 1496// of this file so sys_mmap is in scope (single-pass parser). 1497func sys_sleep_ms(ms: i64) -> i64 { 1498 if ms <= 0 { return 0 } 1499 // struct timespec { sec: i64, nsec: i64 } -- 16 bytes RV64. 1500 let req: *u8 = sys_mmap(16) 1501 let rem: *u8 = sys_mmap(16) 1502 let secs: i64 = ms / 1000 1503 let nsec: i64 = (ms - secs * 1000) * SYS_MAGIC_1000000 // remainder ms -> ns 1504 let req_sec: *i64 = req as *i64 1505 let req_nsec: *i64 = ((req as i64) + 8) as *i64 1506 req_sec[0] = secs 1507 req_nsec[0] = nsec 1508 // clock_nanosleep(CLOCK_MONOTONIC=1, flags=0, req, rem). On EINTR (-4) a signal (e.g. SIGCHLD from a 1509 // reaped child) cut the sleep short and wrote the leftover into rem -- RESUME it, otherwise a caller 1510 // that uses the sleep as a timer (the torrent pool's 2s tick) gets spun into a busy loop by child 1511 // deaths and any tick-based budget collapses to milliseconds. A sleep must sleep its full duration. 1512 var r: i64 = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1513 var guard: i64 = 0 1514 while r == (0 - 4) { 1515 if guard > SYS_MAGIC_100000 { r = 0 } else { 1516 let rs: *i64 = rem as *i64 1517 let rn: *i64 = ((rem as i64) + 8) as *i64 1518 req_sec[0] = rs[0] 1519 req_nsec[0] = rn[0] 1520 r = __syscall(SYS_CLOCK_NANOSLEEP, 1, 0, req as i64, rem as i64, 0, 0) 1521 guard = guard + 1 1522 } 1523 } 1524 sys_munmap(req, 16); sys_munmap(rem, 16) // FREE the timespec pages -- every call mmap'd 2 pages; in a 1525 // long-running poll loop (the supervisor's 15s tick) that leaked ~8KB/iter until mmap -> -12 -> SEGFAULT. 1526 return r 1527} 1528 1529// ---- sockets (RV64 generic syscall numbers) ---------------------- 1530// 1531// Source uses RV64 numbers; the x86_64 backend's 1532// x86ctx_rv64_to_x86_64_syscall table translates at codegen time. 1533// Numbers from arch/arm64/include/asm/unistd.h (RV64 inherits the 1534// generic ABI). 1535 1536// Socket-family syscall numbers via @ifdef macro -- mirrors the 1537// pattern already used for SYS_READ/WRITE/MMAP/etc. above. Without 1538// this gate, --target x86_64 compiled the RV64 numbers as literals 1539// into the `syscall` instruction (e.g. 198 = sched_setaffinity on 1540// x86_64, not socket) and any daemon using sys_socket() died with 1541// ENOSYS before printing its banner -- caught by the nx_signaling 1542// stone S2 deploy on 2026-05-20 (see [[project-cross-isa-syscall- 1543// unification-gap-2026-05-20]]). 1544@ifdef TARGET_X86_64 1545const SYS_SOCKET: i64 = 41 1546const SYS_BIND: i64 = 49 1547const SYS_LISTEN: i64 = 50 1548const SYS_ACCEPT: i64 = 43 1549const SYS_CONNECT: i64 = 42 1550const SYS_SETSOCKOPT: i64 = 54 1551const SYS_SENDTO: i64 = 44 1552const SYS_RECVFROM: i64 = 45 1553const SYS_SHUTDOWN: i64 = 48 1554@endif 1555 1556@ifndef TARGET_X86_64 1557const SYS_SOCKET: i64 = 198 1558const SYS_BIND: i64 = 200 1559const SYS_LISTEN: i64 = 201 1560const SYS_ACCEPT: i64 = 202 1561const SYS_CONNECT: i64 = 203 1562const SYS_SETSOCKOPT: i64 = 208 1563const SYS_SENDTO: i64 = 206 1564const SYS_RECVFROM: i64 = 207 1565const SYS_SHUTDOWN: i64 = 210 1566@endif 1567 1568// Socket-option constants used by nx_http_server / nx_https_server. 1569const SOL_SOCKET: i64 = 1 1570const SO_REUSEADDR: i64 = 2 1571// Receive/send timeouts (Linux x86_64). optval is a struct timeval 1572// {tv_sec: i64, tv_usec: i64} (16 bytes). Essential on PUBLIC sockets: 1573// without them, a single silent/slow client hangs a blocking read 1574// forever -> trivial DoS on a single-threaded accept loop. 1575const SO_SNDTIMEO: i64 = 21 1576const SO_RCVTIMEO: i64 = 20 1577 1578// setsockopt(2) -- set a socket option. Defined BEFORE its first caller 1579// (sys_set_socket_timeout, below): NishiLang forbids forward references, 1580// so the definition must precede every use. 1581func sys_setsockopt(fd: i64, level: i64, optname: i64, 1582 optval: *u8, optlen: i64) -> i64 { 1583 return __syscall(SYS_SETSOCKOPT, fd, level, optname, optval, optlen, 0) 1584} 1585 1586// Set a receive+send timeout (in whole seconds) on a socket fd. 1587// tv is munmap'd before return (LEAK FIXED 2026-07-16): this is called once per PROBE by the daemon 1588// supervisor (35/cycle forever -> ~800MB VSZ/day) and once per CONNECTION by fork-per-connection daemons. 1589// The unfreed page-per-call ballooned VSZ until heuristic overcommit made fork() return -ENOMEM (the 1590// proven pid=-12 failure class) -- likely the historical VSZ pressure behind the vsz_watchdog. 1591func sys_set_socket_timeout(fd: i64, secs: i64) -> i64 { 1592 let tv: *i64 = (sys_mmap(16)) as *i64 1593 tv[0] = secs // tv_sec 1594 tv[1] = 0 // tv_usec 1595 sys_setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, tv as *u8, 16) 1596 sys_setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, tv as *u8, 16) 1597 sys_munmap(tv as *u8, 16) 1598 return 0 1599} 1600 1601// alarm(2): deliver SIGALRM after `secs` seconds (0 cancels a pending alarm). No SIGALRM handler is installed, so 1602// the default action TERMINATES the process. Used as a per-request watchdog inside a forked request-child: a 1603// pathologically-slow page can then never hang the child forever (which would leak its buffers + pile up procs). 1604const SYS_ALARM: i64 = 37 1605func sys_alarm(secs: i64) -> i64 { return __syscall(SYS_ALARM, secs, 0, 0, 0, 0, 0) } 1606 1607const AF_INET: i64 = 2 1608const SOCK_STREAM: i64 = 1 1609const SOCK_DGRAM: i64 = 2 1610 1611func sys_socket(domain: i64, sock_type: i64, protocol: i64) -> i64 { 1612 return __syscall(SYS_SOCKET, domain, sock_type, protocol, 0, 0, 0) 1613} 1614// Pack an AF_INET any-address sockaddr_in (16 bytes) for `port` at `addr`. 1615// RESTORED INTO THE OWNER 2026-08-19: this lived in the old full nx_syscalls_x86_64.nx and was the 1616// one wrapper WITH LIVE CALLERS (nx_nishipages_serve, nx_udp) that the 2026-07-31 alias-stub 1617// consolidation dropped -- both lanes sat NAS-unbuildable ("I do not know the name") until the 1618// rebuild-drain surfaced them. Body verbatim from the old file, including its documented 1619// workaround: NO `as u8` casts on the byte stores -- the array-element-store already truncates 1620// when the lvalue is *u8, and casts on this path once tripped a codegen defect. 1621// (The old file's other two uncalled orphans, sys_pivot_root/sys_umount2, were left dead on a 1622// zero-caller full-tree grep -- restoring an uncalled wrapper is inventory, not capability.) 1623func sockaddr_in_init(addr: *u8, port: i64) -> i64 { 1624 addr[0] = 2 // AF_INET low byte 1625 addr[1] = 0 1626 // Port in network byte order (big-endian). 1627 let hi: i64 = (port >> 8) & 0xFF 1628 let lo: i64 = port & 0xFF 1629 addr[2] = hi 1630 addr[3] = lo 1631 addr[4] = 0 1632 addr[5] = 0 1633 addr[6] = 0 1634 addr[7] = 0 1635 addr[8] = 0 1636 addr[9] = 0 1637 addr[10] = 0 1638 addr[11] = 0 1639 addr[12] = 0 1640 addr[13] = 0 1641 addr[14] = 0 1642 addr[15] = 0 1643 return 0 1644} 1645 1646func sys_bind(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1647 return __syscall(SYS_BIND, fd, addr, addr_len, 0, 0, 0) 1648} 1649func sys_listen(fd: i64, backlog: i64) -> i64 { 1650 return __syscall(SYS_LISTEN, fd, backlog, 0, 0, 0, 0) 1651} 1652// accept(2) -- accept the next pending connection on a listening socket. 1653// Single-arg form (kernel ignores NULL addr/addr_len writes). Existing 1654// nx_http_server callers use this signature; the 3-arg form is provided 1655// as sys_accept_with_addr for outliers needing peer address. 1656func sys_accept(fd: i64) -> i64 { 1657 return __syscall(SYS_ACCEPT, fd, 0, 0, 0, 0, 0) 1658} 1659func sys_accept_with_addr(fd: i64, addr: *u8, addr_len: *i64) -> i64 { 1660 return __syscall(SYS_ACCEPT, fd, addr, addr_len, 0, 0, 0) 1661} 1662// shutdown(2) -- half-close a socket. how: 0=RD, 1=WR, 2=RDWR. 1663func sys_shutdown(fd: i64, how: i64) -> i64 { 1664 return __syscall(SYS_SHUTDOWN, fd, how, 0, 0, 0, 0) 1665} 1666func sys_connect(fd: i64, addr: *u8, addr_len: i64) -> i64 { 1667 return __syscall(SYS_CONNECT, fd, addr, addr_len, 0, 0, 0) 1668} 1669func sys_sendto(fd: i64, buf: *u8, n: i64, flags: i64, 1670 dest_addr: *u8, addr_len: i64) -> i64 { 1671 return __syscall(SYS_SENDTO, fd, buf, n, flags, dest_addr, addr_len) 1672} 1673func sys_recvfrom(fd: i64, buf: *u8, n: i64, flags: i64, 1674 src_addr: *u8, addr_len: *i64) -> i64 { 1675 return __syscall(SYS_RECVFROM, fd, buf, n, flags, src_addr, addr_len) 1676} 1677 1678// ---- SCM_RIGHTS DESCRIPTOR PASSING (sendmsg/recvmsg over AF_UNIX) ----------------------------- 1679// ADDED 2026-08-21 for /compare/trafficsafety TS1. Until now sys_sendmsg was ABSENT-PROVEN from the 1680// whole tree (corpus_complete=1), so the mechanism nginx, HAProxy and Envoy all use for hitless 1681// replacement -- MOVING the listening descriptor rather than re-binding it -- could not be written 1682// at all. SO_REUSEPORT co-binding is an ACCEPT-DISTRIBUTION primitive, NOT a handoff primitive: 1683// LWN documents that changing the set of listening sockets on a port drops connections during the 1684// three-way handshake, so co-binding proves two binders and can never prove zero drops. 1685// 1686// EVERY OFFSET BELOW IS MEASURED, NOT RECALLED. They were read out of the platform's own headers 1687// with offsetof/sizeof/CMSG_LEN compiled for x86_64: 1688// msghdr 56 = name 0 | namelen 8 (u32) | iov 16 | iovlen 24 | control 32 | controllen 40 | flags 48 (u32) 1689// iovec 16 = base 0 | len 8 1690// cmsghdr 16 = len 0 (u64) | level 8 (u32) | type 12 (u32), data at 16 1691// CMSG_LEN(4)=20 CMSG_SPACE(4)=24 sendmsg=46 recvmsg=47 socketpair=53 1692// AF_UNIX=1 SOL_SOCKET=1 SCM_RIGHTS=1 MSG_CMSG_CLOEXEC=1073741824 1693// A WRONG LAYOUT HERE DOES NOT FAIL LOUD. The syscall still returns a positive byte count and 1694// simply transfers no descriptor, which is why the gate for this proves the property by passing a 1695// REAL descriptor between two REAL processes and then USING it, never by reading a return code. 1696// x86_64 Linux numbers, DELIBERATELY UNGUARDED, and the reason is a measurement rather than a 1697// preference. The first draft of this block wrapped these three in the same 1698// @ifdef TARGET_X86_64 / @ifndef pair every other syscall number in this file uses. On an x86 build 1699// that made every call ENOSYS, and the probe that caught it printed why: 1700// CONSTS SYS_SENDMSG=211 SYS_RECVMSG=212 SYS_SOCKETPAIR=199 SYS_WRITE=64 1701// N sendmsg PLAIN via the CONST rc=-38 (211 is unassigned on x86_64) 1702// N2 sendmsg PLAIN via the LITERAL rc=1 1703// SYS_WRITE reading 64 is the tell and it is NOT MINE: the file's own original guarded block 1704// resolves to its RV64 branch when the constant is referenced, on a build whose sys_write plainly 1705// works. So a constant inside these guards is not reliably the value the guard appears to select. 1706// !! A GUARD THAT SILENTLY SELECTS THE OTHER TARGET'S NUMBER IS WORSE THAN NO GUARD: the call still 1707// compiles, still returns, and dispatches a DIFFERENT SYSCALL. Syscall 199 on x86_64 is 1708// fremovexattr, which is why socketpair appeared to answer EFAULT for every input including a NULL 1709// vector and an unsupported domain -- varying the ARGUMENTS can never reveal that the NUMBER is 1710// wrong, because every variant was equally wrong. 1711// => RV64 support for these three is an OPEN, NAMED requirement, blocked on that toolchain 1712// behaviour. It is left undone and stated rather than papered over with a guard measured not to 1713// work. The estate already keeps nx_syscalls_x86_64.nx as the explicit single-target mirror for 1714// exactly this class of problem. 1715const SYS_SENDMSG: i64 = 46 1716const SYS_RECVMSG: i64 = 47 1717const SYS_SOCKETPAIR: i64 = 53 1718const SCM_AF_UNIX: i64 = 1 1719const SCM_SOL_SOCKET: i64 = 1 1720const SCM_RIGHTS_TYPE: i64 = 1 1721const SCM_MSG_CMSG_CLOEXEC: i64 = 1073741824 1722const SCM_MSGHDR_BYTES: i64 = 56 1723const SCM_MSGHDR_OFF_IOV: i64 = 16 1724const SCM_MSGHDR_OFF_IOVLEN: i64 = 24 1725const SCM_MSGHDR_OFF_CTRL: i64 = 32 1726const SCM_MSGHDR_OFF_CTRLLEN: i64 = 40 1727const SCM_IOVEC_BYTES: i64 = 16 1728const SCM_IOVEC_OFF_BASE: i64 = 0 1729const SCM_IOVEC_OFF_LEN: i64 = 8 1730const SCM_CMSG_OFF_LEN: i64 = 0 1731const SCM_CMSG_OFF_LEVEL: i64 = 8 1732const SCM_CMSG_OFF_TYPE: i64 = 12 1733const SCM_CMSG_OFF_DATA: i64 = 16 1734const SCM_CMSG_LEN_1FD: i64 = 20 1735const SCM_CMSG_SPACE_1FD: i64 = 24 1736const SCM_IOV_COUNT_ONE: i64 = 1 1737const SCM_U32_BYTES: i64 = 4 1738const SCM_BYTE_RADIX: i64 = 256 1739const SCM_FDPAIR_BYTES: i64 = 8 1740// One real data byte travels with the ancillary data ON PURPOSE: a sendmsg carrying SCM_RIGHTS and 1741// NO ordinary payload is the classic silent no-transfer, and it returns 0 rather than an error. 1742const SCM_PAYLOAD_BYTES: i64 = 1 1743const SCM_PAYLOAD_BYTE: i64 = 70 1744// Distinguishable refusals, each naming WHICH conjunct failed -- a compound assertion that will not 1745// name its failing conjunct is a false-alarm generator. All are negative and all sit far outside the 1746// errno range, so no caller can confuse one with a kernel error or with a valid descriptor. 1747const SCM_ERR_NO_CMSG: i64 = 0 - 901 1748const SCM_ERR_CMSG_LEN: i64 = 0 - 902 1749const SCM_ERR_CMSG_LEVEL: i64 = 0 - 903 1750const SCM_ERR_CMSG_TYPE: i64 = 0 - 904 1751 1752func scm_zero(base: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { base[i] = 0; i = i + 1 } return 0 } 1753func scm_put_i64(base: *u8, off: i64, v: i64) -> i64 { 1754 let p: *i64 = ((base as i64) + off) as *i64 1755 p[0] = v 1756 return 0 1757} 1758func scm_get_i64(base: *u8, off: i64) -> i64 { 1759 let p: *i64 = ((base as i64) + off) as *i64 1760 return p[0] 1761} 1762// The two cmsg header fields and the descriptor slot itself are 4-byte ints, so they are packed and 1763// unpacked byte by byte in little-endian order. Radix arithmetic rather than bit shifts, matching 1764// sockaddr_in_init's documented style on this exact path. 1765func scm_put_u32(base: *u8, off: i64, v: i64) -> i64 { 1766 var i: i64 = 0 1767 var m: i64 = v 1768 while i < SCM_U32_BYTES { 1769 base[off + i] = m % SCM_BYTE_RADIX 1770 m = m / SCM_BYTE_RADIX 1771 i = i + 1 1772 } 1773 return 0 1774} 1775func scm_get_u32(base: *u8, off: i64) -> i64 { 1776 var v: i64 = 0 1777 var mult: i64 = 1 1778 var i: i64 = 0 1779 while i < SCM_U32_BYTES { 1780 v = v + (base[off + i] as i64) * mult 1781 mult = mult * SCM_BYTE_RADIX 1782 i = i + 1 1783 } 1784 return v 1785} 1786 1787func sys_sendmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 1788 return __syscall(SYS_SENDMSG, fd, msg, flags, 0, 0, 0) 1789} 1790func sys_recvmsg(fd: i64, msg: *u8, flags: i64) -> i64 { 1791 return __syscall(SYS_RECVMSG, fd, msg, flags, 0, 0, 0) 1792} 1793// socketpair(2). sv receives TWO 4-byte descriptors, so it is a *u8 read with scm_get_u32 -- a 1794// single *i64 read would splice both descriptors into one number and the second would vanish. 1795// !! THIS NUMBER IS NOT REACHING socketpair, AND THE FIRST DIAGNOSIS OF THAT WAS WRONG. 1796// Measured 2026-08-21: every call returns -14 (EFAULT) -- with a valid pointer, with a NULL vector, 1797// and with an UNSUPPORTED DOMAIN alike. The first reading of that evidence was "the host refuses 1798// this call for every input", and it was REFUTED by measuring the emitted constants instead of the 1799// arguments. TARGET_X86_64 is hard-pinned UNDEFINED in this toolchain (see nx_syscalls_x86_64.nx 1800// and nx_tokenizer.nx), so the @ifndef branch is what compiles and the x86 backend TRANSLATES RV64 1801// syscall numbers at emit time. Under that translation 53 is RV64 fchmodat, whose SECOND argument 1802// is a path pointer -- and SOCK_STREAM==1 as a path pointer is exactly EFAULT, every time, 1803// regardless of the other arguments. 1804// * VARYING THE ARGUMENTS CAN NEVER REVEAL THAT THE SYSCALL NUMBER IS WRONG: every variant is 1805// equally wrong, so a set of controls that all agree reads as a confident finding about the host. 1806// The control that actually discriminated was PRINTING THE CONSTANT the binary emits. 1807// => The likely correct value here is the RV64 number 199, exactly as sendmsg/recvmsg above needed 1808// their own numbers rather than the guarded pair. That is NOT asserted: it is UNTESTED, and this 1809// comment says so rather than shipping a plausible number with a confident sentence. 1810// => NOTHING DEPENDS ON IT. The descriptor-passing lane uses a NAMED AF_UNIX rendezvous 1811// (sys_unix_listen + sys_unix_connect_fd below), which is proven end to end by nx_scm_rights_gate 1812// and is also what nginx, HAProxy and systemd actually use to move a listener between processes. 1813// socketpair was only ever the convenience. 1814func sys_socketpair(domain: i64, sock_type: i64, protocol: i64, sv: *u8) -> i64 { 1815 return __syscall(SYS_SOCKETPAIR, domain, sock_type, protocol, sv, 0, 0) 1816} 1817 1818// Bind+listen a NAMED AF_UNIX stream socket -- the accepting half of the rendezvous whose 1819// connecting half is nx_unix_connect. Returns the listening fd, or a negative errno. 1820// The caller owns the path: unlink it first (a stale node makes bind return EADDRINUSE) and unlink 1821// it after, because an AF_UNIX bind leaves a filesystem entry that outlives the process. 1822const SCM_SUN_PATH_OFF: i64 = 2 // sockaddr_un = [sa_family: u16][sun_path: 108] 1823const SCM_SUN_BYTES: i64 = 110 1824const SCM_SUN_PATH_MAX: i64 = 107 1825func sys_unix_listen(path: *u8, backlog: i64) -> i64 { 1826 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 1827 if fd < 0 { return fd } 1828 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 1829 var i: i64 = 0 1830 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 1831 sa[0] = SCM_AF_UNIX 1832 sa[1] = 0 1833 var p: i64 = 0 1834 while path[p] != (0 as u8) { 1835 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 1836 sa[SCM_SUN_PATH_OFF + p] = path[p] 1837 p = p + 1 1838 } 1839 let br: i64 = sys_bind(fd, sa, SCM_SUN_PATH_OFF + p + 1) 1840 if br < 0 { sys_close(fd); return br } 1841 let lr: i64 = sys_listen(fd, backlog) 1842 if lr < 0 { sys_close(fd); return lr } 1843 return fd 1844} 1845 1846// The CONNECTING half of the same rendezvous. Returns the connected fd or a negative errno. 1847// RESIDUAL NAMED RATHER THAN LEFT SILENT: nx_unix_socket.nx already carries an nx_unix_connect with 1848// this exact body. It is not composed here because that file also defines a main(), so importing it 1849// would inject a second main into every one of the 52 daemons that reach nx_http_server -- a 1850// resolution-by-definition-order hazard this tree has already been bitten by. The primitive belongs 1851// in the shim; the older standalone file should be reduced to a caller of this one, and that is a 1852// separate change to a file with its own consumers rather than something to fold in silently here. 1853func sys_unix_connect_fd(path: *u8) -> i64 { 1854 let fd: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0) 1855 if fd < 0 { return fd } 1856 let sa: *u8 = sys_mmap(SCM_SUN_BYTES) 1857 var i: i64 = 0 1858 while i < SCM_SUN_BYTES { sa[i] = 0; i = i + 1 } 1859 sa[0] = SCM_AF_UNIX 1860 sa[1] = 0 1861 var p: i64 = 0 1862 while path[p] != (0 as u8) { 1863 if p >= SCM_SUN_PATH_MAX { sys_close(fd); return 0 - 36 } 1864 sa[SCM_SUN_PATH_OFF + p] = path[p] 1865 p = p + 1 1866 } 1867 let cr: i64 = sys_connect(fd, sa, SCM_SUN_PATH_OFF + p + 1) 1868 if cr < 0 { sys_close(fd); return cr } 1869 return fd 1870} 1871 1872// Send ONE open descriptor over a connected AF_UNIX socket. Returns the sendmsg result: the number 1873// of ordinary data bytes sent (SCM_PAYLOAD_BYTES on success) or a negative errno. The descriptor 1874// itself is NOT closed here -- both ends legitimately hold it until the sender chooses to let go, 1875// and that overlap is the entire point: there must be no instant at which zero processes hold the 1876// listening socket. 1877func sys_send_fd(sock: i64, fd: i64) -> i64 { 1878 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 1879 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 1880 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 1881 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 1882 scm_zero(msg, SCM_MSGHDR_BYTES) 1883 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 1884 data[0] = SCM_PAYLOAD_BYTE 1885 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 1886 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 1887 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 1888 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 1889 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 1890 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 1891 scm_put_i64(cbuf, SCM_CMSG_OFF_LEN, SCM_CMSG_LEN_1FD) 1892 scm_put_u32(cbuf, SCM_CMSG_OFF_LEVEL, SCM_SOL_SOCKET) 1893 scm_put_u32(cbuf, SCM_CMSG_OFF_TYPE, SCM_RIGHTS_TYPE) 1894 scm_put_u32(cbuf, SCM_CMSG_OFF_DATA, fd) 1895 let r: i64 = sys_sendmsg(sock, msg, 0) 1896 sys_munmap(msg, SCM_MSGHDR_BYTES) 1897 sys_munmap(iov, SCM_IOVEC_BYTES) 1898 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 1899 sys_munmap(data, SCM_PAYLOAD_BYTES) 1900 return r 1901} 1902 1903// Receive ONE descriptor from a connected AF_UNIX socket. Returns the NEW descriptor number in this 1904// process (>= 0), a negative errno from recvmsg, or one of the SCM_ERR_* codes above. 1905// flags: 0, or SCM_MSG_CMSG_CLOEXEC so the arriving descriptor is not leaked into grandchildren -- 1906// the estate has already lost a port for six days to exactly that inheritance (nx_cloexec_gate). 1907// THE VALIDATION IS THE WHOLE POINT. recvmsg happily returns a positive byte count having delivered 1908// no ancillary data at all, so the kernel's REWRITTEN msg_controllen is read back rather than the 1909// value we asked for, and each of the three cmsg header fields is checked separately so a failure 1910// says which one. 1911func sys_recv_fd(sock: i64, flags: i64) -> i64 { 1912 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES) 1913 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES) 1914 let cbuf: *u8 = sys_mmap(SCM_CMSG_SPACE_1FD) 1915 let data: *u8 = sys_mmap(SCM_PAYLOAD_BYTES) 1916 scm_zero(msg, SCM_MSGHDR_BYTES) 1917 scm_zero(cbuf, SCM_CMSG_SPACE_1FD) 1918 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, data as i64) 1919 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, SCM_PAYLOAD_BYTES) 1920 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64) 1921 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE) 1922 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRL, cbuf as i64) 1923 scm_put_i64(msg, SCM_MSGHDR_OFF_CTRLLEN, SCM_CMSG_SPACE_1FD) 1924 let r: i64 = sys_recvmsg(sock, msg, flags) 1925 var out: i64 = r 1926 if r >= 0 { 1927 out = SCM_ERR_NO_CMSG 1928 if scm_get_i64(msg, SCM_MSGHDR_OFF_CTRLLEN) >= SCM_CMSG_LEN_1FD { 1929 out = SCM_ERR_CMSG_LEN 1930 if scm_get_i64(cbuf, SCM_CMSG_OFF_LEN) == SCM_CMSG_LEN_1FD { 1931 out = SCM_ERR_CMSG_LEVEL 1932 if scm_get_u32(cbuf, SCM_CMSG_OFF_LEVEL) == SCM_SOL_SOCKET { 1933 out = SCM_ERR_CMSG_TYPE 1934 if scm_get_u32(cbuf, SCM_CMSG_OFF_TYPE) == SCM_RIGHTS_TYPE { 1935 out = scm_get_u32(cbuf, SCM_CMSG_OFF_DATA) 1936 } 1937 } 1938 } 1939 } 1940 } 1941 sys_munmap(msg, SCM_MSGHDR_BYTES) 1942 sys_munmap(iov, SCM_IOVEC_BYTES) 1943 sys_munmap(cbuf, SCM_CMSG_SPACE_1FD) 1944 sys_munmap(data, SCM_PAYLOAD_BYTES) 1945 return out 1946} 1947 1948// Ordinary permission bits only. Special privilege bits are never copied by staging. 1949const NX_FILE_PERMISSION_MASK:i64=511 1950const NX_FILE_DESCRIPTOR_INVALID:i64=0-22 1951func sys_fchmod_fd(fd:i64,mode:i64)->i64{ 1952 if fd<0 || mode<0 || mode>NX_FILE_PERMISSION_MASK {return NX_FILE_DESCRIPTOR_INVALID} 1953 return __syscall(52,fd,mode,0,0,0,0) 1954} 1955// Portable descriptor syscall; consumers below use the x86-64 stat ABI layout. 1956const NX_STAT_X64_BYTES:i64=144 1957const NX_STAT_X64_MODE_OFFSET:i64=24 1958const NX_STAT_X64_BLOCK_OFFSET:i64=56 1959const NX_STAT_X64_SIZE_OFFSET:i64=48 1960const NX_STAT_X64_DEVICE_OFFSET:i64=0 1961const NX_STAT_X64_INODE_OFFSET:i64=8 1962const NX_STAT_TYPE_MASK:i64=61440 1963const NX_STAT_REGULAR_FILE:i64=32768 1964func sys_fstat_fd(fd:i64,stat:*u8)->i64{ 1965 if fd<0 || (stat as i64)==0{return NX_FILE_DESCRIPTOR_INVALID} 1966 return __syscall(80,fd,stat,0,0,0,0) 1967} 1968func sys_stat_permissions(stat:*u8)->i64{ 1969 let mode:i64=(stat[NX_STAT_X64_MODE_OFFSET] as i64)+((stat[NX_STAT_X64_MODE_OFFSET+1] as i64)<<8) 1970 return mode & NX_FILE_PERMISSION_MASK 1971}