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}