nx_fcntl.nx source
↩ module page · 166 lines · 6483 B
1// nx_fcntl.nx -- fcntl(2) + open-flag constants.
2//
3// The existing fs.nx + sys_openat in syscalls.nx open files with
4// minimal flag awareness. Real callers need to pick read/write/
5// create/append/truncate/sync/cloexec semantics, set close-on-exec
6// on existing fds, mark fds nonblocking, query / change file
7// descriptor flags.
8//
9// This module provides the constants + a thin wrapper around the
10// fcntl(2) syscall (number 25 on RV64).
11//
12// Pairs with fs.nx + nx_proc (so children don't inherit
13// FD_CLOEXEC fds across exec).
14
15// nx_safety_envelope:
16// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
17// sil_target: SIL1
18// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
19// verdict: NOT_YET_EVALUATED
20
21import "syscalls.nx"
22
23// open() / openat() flags (Linux RV64).
24const NX_O_RDONLY: i64 = 0x0
25const NX_O_WRONLY: i64 = 0x1
26const NX_O_RDWR: i64 = 0x2
27const NX_O_CREAT: i64 = 0x40
28const NX_O_EXCL: i64 = 0x80
29const NX_O_TRUNC: i64 = 0x200
30const NX_O_APPEND: i64 = 0x400
31const NX_O_NONBLOCK: i64 = 0x800
32const NX_O_SYNC: i64 = 0x101000
33const NX_O_DIRECTORY: i64 = 0x10000
34const NX_O_CLOEXEC: i64 = 0x80000
35
36// fcntl() commands.
37const NX_F_DUPFD: i64 = 0
38const NX_F_GETFD: i64 = 1
39const NX_F_SETFD: i64 = 2
40const NX_F_GETFL: i64 = 3
41const NX_F_SETFL: i64 = 4
42const NX_F_DUPFD_CLOEXEC: i64 = 1030
43
44// fd flags (for F_GETFD / F_SETFD).
45const NX_FD_CLOEXEC: i64 = 1
46
47// AT_FDCWD passed as dirfd to *at(2) syscalls.
48// Linux value is -100; encoded as the signed two's-complement
49// representation since NishiLang const initialisers can't hold
50// expressions like (0 - 100) yet.
51const NX_AT_FDCWD: i64 = 0xFFFFFFFFFFFFFF9C
52
53// Per-target syscall numbers. Cross-target via @ifdef TARGET_X86_64
54// per [[feedback-hardware-agnostic-is-robustness]] (the bit-level
55// no-proliferation cardinal applied to syscall ABI: same shape as
56// nx_syscalls.nx's SYS_* block).
57
58@ifdef TARGET_X86_64
59const NX_SYS_FCNTL: i64 = 72
60const NX_SYS_OPENAT: i64 = 257
61const NX_SYS_DUP: i64 = 32
62const NX_SYS_DUP3: i64 = 292
63@endif
64
65@ifndef TARGET_X86_64
66// ⚠ fcntl is PINNED TO THE x86_64 NUMBER (72), NOT the RISC-V 25, and that is deliberate.
67// This branch is the LIVE one (TARGET_X86_64 is hard-pinned undefined, see nx_syscalls.nx:72) and
68// every other constant here is a RISC-V number that the nx_cc x86 backend TRANSLATES. The backend's
69// translation table has NO ENTRY FOR fcntl, so a literal 25 falls through to x86 mremap and returns
70// EINVAL -- which made nx_fcntl fail ALWAYS, which made nx_connect_bounded bail on its first line,
71// which took outbound TCP away from EVERY organ built after 2026-08-01 (debt 1785566771).
72// PROVEN: nx_fcntlprobe showed NX_SYS_FCNTL=25 and nx_fcntl(1,F_GETFL)=-22 on a plain fd AND a socket.
73// THE REAL FIX IS IN THE COMPILER -- add fcntl to the x86 syscall translation table (source is
74// laptop-side nishi-core/nxc2). Revert this pin the moment that lands, or RISC-V targets will break.
75const NX_SYS_FCNTL: i64 = 72
76const NX_SYS_OPENAT: i64 = 56
77const NX_SYS_DUP: i64 = 23
78const NX_SYS_DUP3: i64 = 24
79@endif
80
81// fcntl(fd, cmd, arg).
82func nx_fcntl(fd: i64, cmd: i64, arg: i64) -> i64 {
83 return __syscall(NX_SYS_FCNTL, fd, cmd, arg, 0, 0, 0)
84}
85
86// Mark a fd close-on-exec.
87func nx_fcntl_set_cloexec(fd: i64) -> i64 {
88 let cur: i64 = nx_fcntl(fd, NX_F_GETFD, 0)
89 if cur < 0 { return cur }
90 return nx_fcntl(fd, NX_F_SETFD, cur | NX_FD_CLOEXEC)
91}
92
93// Mark a fd nonblocking.
94func nx_fcntl_set_nonblock(fd: i64) -> i64 {
95 let cur: i64 = nx_fcntl(fd, NX_F_GETFL, 0)
96 if cur < 0 { return cur }
97 return nx_fcntl(fd, NX_F_SETFL, cur | NX_O_NONBLOCK)
98}
99
100// dup(oldfd) -> new fd referring to same open file.
101func nx_dup(oldfd: i64) -> i64 {
102 return __syscall(NX_SYS_DUP, oldfd, 0, 0, 0, 0, 0)
103}
104
105// dup3(oldfd, newfd, flags) -- Linux's dup2 replacement. flags
106// can include O_CLOEXEC.
107func nx_dup3(oldfd: i64, newfd: i64, flags: i64) -> i64 {
108 return __syscall(NX_SYS_DUP3, oldfd, newfd, flags, 0, 0, 0)
109}
110
111// openat with flag-aware signature. path is NUL-terminated. mode
112// is the file-creation permission bits used when O_CREAT is set
113// (e.g. 0o644 for "rw-r--r--" -> 0x1A4).
114func nx_openat(dirfd: i64, path: *u8, flags: i64, mode: i64) -> i64 {
115 return __syscall(NX_SYS_OPENAT, dirfd, path as i64, flags, mode, 0, 0)
116}
117
118// Convenience: open in cwd, read-only, no creation.
119func nx_open_rd(path: *u8) -> i64 {
120 return nx_openat(NX_AT_FDCWD, path, NX_O_RDONLY, 0)
121}
122
123// Convenience: open in cwd, write-only, create + truncate, mode 0o644.
124func nx_open_wr(path: *u8) -> i64 {
125 return nx_openat(NX_AT_FDCWD, path, NX_O_WRONLY | NX_O_CREAT | NX_O_TRUNC, 0x1A4)
126}
127
128// Convenience: open in cwd, read-write append, no create.
129func nx_open_append(path: *u8) -> i64 {
130 return nx_openat(NX_AT_FDCWD, path, NX_O_WRONLY | NX_O_APPEND, 0)
131}
132
133// ---- self-test ---------------------------------------------------
134
135func main() -> i64 {
136 // Constants are within Linux range.
137 if NX_O_RDONLY != 0 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
138 if NX_O_WRONLY != 1 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
139 if NX_O_CREAT != 0x40 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
140 if NX_O_TRUNC != 0x200 { return __syscall(93, 4, 0, 0, 0, 0, 0) }
141 if NX_O_CLOEXEC != 0x80000 { return __syscall(93, 5, 0, 0, 0, 0, 0) }
142
143 if NX_F_GETFD != 1 { return __syscall(93, 6, 0, 0, 0, 0, 0) }
144 if NX_F_SETFD != 2 { return __syscall(93, 7, 0, 0, 0, 0, 0) }
145 if NX_FD_CLOEXEC != 1 { return __syscall(93, 8, 0, 0, 0, 0, 0) }
146 if NX_AT_FDCWD != (0 - 100) { return __syscall(93, 9, 0, 0, 0, 0, 0) }
147
148 // Composed flags for "open for write, create if missing,
149 // truncate, close-on-exec".
150 let flags: i64 = NX_O_WRONLY | NX_O_CREAT | NX_O_TRUNC | NX_O_CLOEXEC
151 let expected: i64 = 1 | 0x40 | 0x200 | 0x80000
152 if flags != expected { return __syscall(93, 10, 0, 0, 0, 0, 0) }
153
154 // dup of stderr should produce a fresh fd >= 3 (or possibly an
155 // error if we're somehow out of fds; just check non-positive
156 // means error).
157 let dup_fd: i64 = nx_dup(2)
158 if dup_fd < 0 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
159 sys_close(dup_fd)
160
161 // F_GETFD on stderr should succeed (return 0 or FD_CLOEXEC).
162 let fl: i64 = nx_fcntl(2, NX_F_GETFD, 0)
163 if fl < 0 { return __syscall(93, 12, 0, 0, 0, 0, 0) }
164
165 return 0
166}