code wiki / _hdl_build / nx_scm_probe.nx
nx_scm_probe.nx source
↩ module page · 182 lines · 10257 B
1// nx_scm_probe.nx -- ISOLATE why sys_socketpair returns -14 (EFAULT) on this host.
2// Built BEFORE forming a hypothesis: five variants, each changing exactly one thing, plus two
3// CONTROLS that are known-good on this platform (pipe2 also has the kernel write two ints into a
4// caller buffer; socket() proves AF_UNIX itself is available). A verdict is not a diagnosis, so
5// this prints numbers and reaches no conclusion. license_tier: ORIGINAL
6import "nx_syscalls.nx"
7import "nx_unix_socket.nx"
8
9const SP_NUM_SCRATCH: i64 = 24
10const SP_ASCII_ZERO: i64 = 48
11const SP_B10: i64 = 10
12const SP_FDPAIR_BYTES: i64 = 8
13const SP_PAGE_BYTES: i64 = 4096
14const SP_PIPEBOX_BYTES: i64 = 16
15const SP_NR_SOCKETPAIR: i64 = 53
16const SP_NR_SENDMSG: i64 = 46
17const SP_NR_RECVMSG: i64 = 47
18// sockaddr_un = [sa_family: u16][sun_path: 108], so the path starts at offset 2.
19const SP_SUN_PATH_OFF: i64 = 2
20const SP_SUN_BYTES: i64 = 110
21const SP_BACKLOG: i64 = 4
22
23func p_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
24func p_num(v: i64) -> i64 {
25 if v == 0 { sys_write(1, "0" as *u8, 1); return 0 }
26 var m: i64 = v
27 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m }
28 let d: *u8 = sys_mmap(SP_NUM_SCRATCH); var k: i64 = 0
29 while m > 0 { d[k] = ((SP_ASCII_ZERO + (m - (m / SP_B10) * SP_B10)) as u8); m = m / SP_B10; k = k + 1 }
30 var j: i64 = k - 1
31 while j >= 0 { sys_write(1, ((d as i64)+j) as *u8, 1); j = j - 1 }
32 return 0
33}
34
35func main() -> i64 {
36 // CONTROL 1: does AF_UNIX exist at all on this host?
37 let s1: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0)
38 p_puts("C1 socket(AF_UNIX,SOCK_STREAM) rc=" as *u8); p_num(s1); p_puts("\n" as *u8)
39 if s1 >= 0 { sys_close(s1) }
40
41 // CONTROL 2: can the kernel write two ints into a buffer we allocated? pipe2 is the incumbent
42 // that already does exactly this, all over the tree.
43 let f: *i64 = (sys_mmap(SP_PIPEBOX_BYTES)) as *i64
44 f[0] = 0
45 let pr: i64 = sys_pipe2(f, 0)
46 p_puts("C2 pipe2 rc=" as *u8); p_num(pr); p_puts(" packed=" as *u8); p_num(f[0]); p_puts("\n" as *u8)
47
48 // A: the shim wrapper with an 8-byte arena buffer -- the failing case.
49 let a: *u8 = sys_mmap(SP_FDPAIR_BYTES)
50 a[0] = 0
51 p_puts("A addr=" as *u8); p_num(a as i64); p_puts("\n" as *u8)
52 let ra: i64 = sys_socketpair(SCM_AF_UNIX, SOCK_STREAM, 0, a)
53 p_puts("A sys_socketpair(8B arena) rc=" as *u8); p_num(ra); p_puts("\n" as *u8)
54
55 // B: same wrapper, page-sized buffer -- isolates "is it the allocation size".
56 let b: *u8 = sys_mmap(SP_PAGE_BYTES)
57 b[0] = 0
58 let rb: i64 = sys_socketpair(SCM_AF_UNIX, SOCK_STREAM, 0, b)
59 p_puts("B sys_socketpair(page) rc=" as *u8); p_num(rb); p_puts("\n" as *u8)
60
61 // C: raw __syscall, bypassing the wrapper -- isolates "is it the wrapper's declaration".
62 let rc2: i64 = __syscall(SP_NR_SOCKETPAIR, SCM_AF_UNIX, SOCK_STREAM, 0, b, 0, 0)
63 p_puts("C raw __syscall(53) rc=" as *u8); p_num(rc2); p_puts("\n" as *u8)
64
65 // D: the *i64 buffer shape pipe2 uses, handed to the same wrapper -- isolates "is it the
66 // pointer TYPE at the call site".
67 let e: *i64 = (sys_mmap(SP_PIPEBOX_BYTES)) as *i64
68 e[0] = 0
69 let rd: i64 = sys_socketpair(SCM_AF_UNIX, SOCK_STREAM, 0, e as *u8)
70 p_puts("D sys_socketpair(i64 buf) rc=" as *u8); p_num(rd); p_puts(" packed=" as *u8); p_num(e[0]); p_puts("\n" as *u8)
71
72 // E: raw __syscall on a page from the SHARED mapper, no arena involvement.
73 let g: *u8 = sys_mmap_shared(SP_PAGE_BYTES)
74 g[0] = 0
75 let re: i64 = __syscall(SP_NR_SOCKETPAIR, SCM_AF_UNIX, SOCK_STREAM, 0, g, 0, 0)
76 p_puts("E raw on shared page rc=" as *u8); p_num(re); p_puts("\n" as *u8)
77
78 // F: a POINTER IN THE SAME 4th SYSCALL SLOT that the kernel only READS. setsockopt puts optval
79 // at arg 4 exactly as socketpair puts usockvec there. If this returns 0 the slot delivers a
80 // usable address and the socketpair failure is not about argument marshalling; if it fails the
81 // same way, the whole tree has been calling setsockopt into the void and nobody checked, because
82 // sys_set_socket_timeout discards its return value.
83 let s2: i64 = sys_socket(AF_INET, SOCK_STREAM, 0)
84 let tv: *i64 = (sys_mmap(SP_PIPEBOX_BYTES)) as *i64
85 tv[0] = 1
86 tv[1] = 0
87 let rf: i64 = sys_setsockopt(s2, SOL_SOCKET, SO_RCVTIMEO, tv as *u8, SP_PIPEBOX_BYTES)
88 p_puts("F setsockopt(ptr in arg4, kernel READS) rc=" as *u8); p_num(rf); p_puts("\n" as *u8)
89 if s2 >= 0 { sys_close(s2) }
90
91 // G: the same call with the pointer cast to i64 FIRST -- isolates "does a pointer-typed
92 // argument survive the call differently from an integer-typed one".
93 let rg: i64 = __syscall(SP_NR_SOCKETPAIR, SCM_AF_UNIX, SOCK_STREAM, 0, (g as i64), 0, 0)
94 p_puts("G raw with (ptr as i64) rc=" as *u8); p_num(rg); p_puts("\n" as *u8)
95
96 // H: is the address itself intact? Print the low and high halves of a page pointer so a
97 // 32-bit truncation somewhere in the call path would be visible as a plausible-but-wrong value
98 // rather than having to be inferred.
99 p_puts("H page addr=" as *u8); p_num(g as i64)
100 p_puts(" high32=" as *u8); p_num((g as i64) / SYS_MAGIC_4294967296); p_puts("\n" as *u8)
101
102 // I: THE DISCRIMINATOR. Does the kernel reach its DOMAIN check before touching our pointer?
103 // AF_INET has no socketpair support, so a kernel that parses the arguments must answer
104 // EOPNOTSUPP (-95). If this ALSO returns -14 the call is failing before the domain is even
105 // looked at, which points at the argument path; if it returns -95 the arguments are arriving
106 // intact and the EFAULT belongs to the copy back to userspace.
107 let ri: i64 = __syscall(SP_NR_SOCKETPAIR, AF_INET, SOCK_STREAM, 0, g, 0, 0)
108 p_puts("I socketpair(AF_INET) rc=" as *u8); p_num(ri); p_puts("\n" as *u8)
109
110 // J: same family, datagram type -- separates "this socket TYPE" from "this call".
111 let rj: i64 = __syscall(SP_NR_SOCKETPAIR, SCM_AF_UNIX, SOCK_DGRAM, 0, g, 0, 0)
112 p_puts("J socketpair(AF_UNIX,SOCK_DGRAM) rc=" as *u8); p_num(rj); p_puts("\n" as *u8)
113
114 // K: a deliberately NULL vector. A kernel that copies to userspace must answer EFAULT here.
115 // If K and A give the SAME answer, EFAULT is what this call returns for every input and the
116 // pointer we passed was never the variable.
117 let rk: i64 = __syscall(SP_NR_SOCKETPAIR, SCM_AF_UNIX, SOCK_STREAM, 0, 0, 0, 0)
118 p_puts("K socketpair(NULL vec) rc=" as *u8); p_num(rk); p_puts("\n" as *u8)
119
120 // L: THE PIVOT'S PRECONDITION. A NAMED AF_UNIX rendezvous is what nginx, HAProxy and systemd
121 // actually use to move a listener between processes; socketpair is only a convenience. Prove
122 // bind+listen on a filesystem path works here before rebuilding the round trip on it.
123 sys_mkdir("/tmp/nx_scm_probe" as *u8, MODE_0755)
124 sys_unlinkat("/tmp/nx_scm_probe/rv.sock" as *u8)
125 let ls: i64 = sys_socket(SCM_AF_UNIX, SOCK_STREAM, 0)
126 let sa: *u8 = sys_mmap(SP_PAGE_BYTES)
127 var z: i64 = 0
128 while z < SP_SUN_BYTES { sa[z] = 0; z = z + 1 }
129 sa[0] = SCM_AF_UNIX
130 sa[1] = 0
131 let pth: *u8 = "/tmp/nx_scm_probe/rv.sock" as *u8
132 var q: i64 = 0
133 while pth[q] != (0 as u8) { sa[SP_SUN_PATH_OFF + q] = pth[q]; q = q + 1 }
134 let bl: i64 = sys_bind(ls, sa, SP_SUN_PATH_OFF + q + 1)
135 p_puts("L bind(AF_UNIX path) rc=" as *u8); p_num(bl); p_puts("\n" as *u8)
136 let li: i64 = sys_listen(ls, SP_BACKLOG)
137 p_puts("L listen rc=" as *u8); p_num(li); p_puts("\n" as *u8)
138 let cs: i64 = nx_unix_connect(pth)
139 p_puts("L connect rc=" as *u8); p_num(cs); p_puts("\n" as *u8)
140 let acc: i64 = sys_accept(ls)
141 p_puts("L accept rc=" as *u8); p_num(acc); p_puts("\n" as *u8)
142 // M/N/O: IS sendmsg AVAILABLE AT ALL ON THIS HOST? Measured on a REAL connected socket pair, so
143 // no result can be blamed on a bad fd. sendto is the control: it is the same family, the same
144 // socket and the same direction, so if sendto works and sendmsg does not, the difference is the
145 // syscall itself and not the setup.
146 if acc >= 0 { if cs >= 0 {
147 let one: *u8 = sys_mmap(SP_PAGE_BYTES)
148 one[0] = 65
149 let nt: i64 = sys_sendto(cs, one, 1, 0, 0 as *u8, 0)
150 p_puts("M sendto on a live AF_UNIX socket rc=" as *u8); p_num(nt); p_puts("\n" as *u8)
151 // plain sendmsg, ONE iovec, NO ancillary data -- the simplest possible use of the call.
152 let msg: *u8 = sys_mmap(SCM_MSGHDR_BYTES)
153 let iov: *u8 = sys_mmap(SCM_IOVEC_BYTES)
154 scm_zero(msg, SCM_MSGHDR_BYTES)
155 scm_put_i64(iov, SCM_IOVEC_OFF_BASE, one as i64)
156 scm_put_i64(iov, SCM_IOVEC_OFF_LEN, 1)
157 scm_put_i64(msg, SCM_MSGHDR_OFF_IOV, iov as i64)
158 scm_put_i64(msg, SCM_MSGHDR_OFF_IOVLEN, SCM_IOV_COUNT_ONE)
159 // WHAT NUMBER IS THE SHIM ACTUALLY EMITTING? The syscall constants are behind target
160 // guards, so "it is 46" is a claim about the macro processor, not an observation. Print
161 // them, and call the SAME syscall twice -- once through the constant and once through the
162 // literal -- so a guard that resolved the wrong branch is visible instead of inferred.
163 p_puts("CONSTS SYS_SENDMSG=" as *u8); p_num(SYS_SENDMSG)
164 p_puts(" SYS_RECVMSG=" as *u8); p_num(SYS_RECVMSG)
165 p_puts(" SYS_SOCKETPAIR=" as *u8); p_num(SYS_SOCKETPAIR)
166 p_puts(" SYS_WRITE=" as *u8); p_num(SYS_WRITE)
167 p_puts("\n" as *u8)
168 let sm: i64 = sys_sendmsg(cs, msg, 0)
169 p_puts("N sendmsg PLAIN via the CONST rc=" as *u8); p_num(sm); p_puts("\n" as *u8)
170 let sml: i64 = __syscall(SP_NR_SENDMSG, cs, msg, 0, 0, 0, 0)
171 p_puts("N2 sendmsg PLAIN via the LITERAL 46 rc=" as *u8); p_num(sml); p_puts("\n" as *u8)
172 let rm: i64 = sys_recvmsg(acc, msg, 0)
173 p_puts("O recvmsg PLAIN via the CONST rc=" as *u8); p_num(rm); p_puts("\n" as *u8)
174 let rml: i64 = __syscall(SP_NR_RECVMSG, acc, msg, 0, 0, 0, 0)
175 p_puts("O2 recvmsg PLAIN via the LITERAL 47 rc=" as *u8); p_num(rml); p_puts("\n" as *u8)
176 } }
177 if acc >= 0 { sys_close(acc) }
178 if cs >= 0 { sys_close(cs) }
179 if ls >= 0 { sys_close(ls) }
180 sys_unlinkat("/tmp/nx_scm_probe/rv.sock" as *u8)
181 return 0
182}