nx_syscalls_mapping_candidate_t181.nx source
↩ module page · 1946 lines · 105305 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}