code wiki / (root) / nx_cloexec_gate.nx

nx_cloexec_gate.nx source

↩ module page · 140 lines · 6843 B

1// nx_cloexec_gate.nx -- proves every listening socket this tree creates is FD_CLOEXEC, so a forked+exec'd 2// child can never inherit it and hold the port hostage. 3// 4// THE OUTAGE THIS GATE EXISTS FOR (measured live 2026-07-30): `netstat -tlnp` showed BOTH 5// 127.0.0.1:18098 LISTEN 707/nx_hostctl <- the SUPERVISOR 6// 127.0.0.1:18098 LISTEN 24311/./nx_opaque_l <- a login daemon whose argv names ONLY port 9091 7// while nx_mgmt_api -- whose port 18098 is -- was NOT LISTENING AT ALL and its restart counter climbed 8// 6 -> 7. THE CHAIN: /api/deploy forks+execs nx_hostctl from inside nx_mgmt_api, which was holding its 9// listening socket with no FD_CLOEXEC. hostctl inherited the fd; hostctl is the long-lived supervisor, so 10// it held mgmt's port FOREVER and re-leaked it into every child it subsequently spawned. Restarting the 11// victim can never help -- it is an outage that SURVIVES EVERY RESTART and reads as a causeless crash-loop. 12// 13// SO_REUSEPORT DOES NOT RESCUE IT: the kernel permits co-binding only when EVERY socket on the port set 14// SO_REUSEPORT, so one inherited legacy socket locks out even a REUSEPORT binder. 15// 16// T1 is the CONTROL: a raw socket with no fix MUST read FD_CLOEXEC CLEAR. Without it, T3/T4 could pass on a 17// kernel that sets the bit by default and would prove nothing. T5 is the neg-control on the READER itself. 18// license_tier: ORIGINAL No hw writes (Rule 26). expect_exit: 0 19import "nx_syscalls.nx" 20import "nx_http_server.nx" 21 22const CX_LOG: *u8 = "knowledge/status/cloexec_gate.log" 23const CX_MODE: i64 = 420 24const CX_FCNTL: i64 = 72 25const CX_F_GETFD: i64 = 1 26const CX_F_SETFD: i64 = 2 27const CX_FD_CLOEXEC: i64 = 1 28const CX_AF_INET: i64 = 2 29const CX_SOCK_STREAM: i64 = 1 30 31func cx_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 32func cx_w(fd: i64, s: *u8) -> i64 { let n: i64 = cx_len(s); sys_write(fd, s, n); return 0 } 33func cx_p(s: *u8) -> i64 { return cx_w(1, s) } 34func cx_pn(v: i64) -> i64 { 35 let t: *u8 = sys_mmap(32) 36 var m: i64 = v 37 if m < 0 { sys_write(1, "-" as *u8, 1); m = 0 - m } 38 var k: i64 = 0 39 if m == 0 { t[0] = 48 as u8; k = 1 } 40 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 41 let o: *u8 = sys_mmap(32) 42 var i: i64 = 0 43 while i < k { o[i] = t[k-1-i]; i = i + 1 } 44 sys_write(1, o, k) 45 return 0 46} 47func cx_ck(name: *u8, c: i64) -> i64 { 48 if c == 1 { cx_p(" PASS " as *u8) } else { cx_p(" FAIL " as *u8) } 49 cx_p(name); cx_p("\n" as *u8) 50 return c 51} 52 53// Read the FD_CLOEXEC bit back from the kernel. Returns 1 set, 0 clear, -1 on error. 54func cx_is_cloexec(fd: i64) -> i64 { 55 if fd < 0 { return 0 - 1 } 56 let fl: i64 = __syscall(CX_FCNTL, fd, CX_F_GETFD, 0, 0, 0, 0) 57 if fl < 0 { return 0 - 1 } 58 return fl & CX_FD_CLOEXEC 59} 60 61// Build a sockaddr_in for 127.0.0.1:<port>. family LE, port BE, addr BE. 62func cx_addr(port: i64) -> *u8 { 63 let a: *u8 = sys_mmap(16) 64 var i: i64 = 0 65 while i < 16 { a[i] = 0 as u8; i = i + 1 } 66 a[0] = CX_AF_INET as u8 67 a[1] = 0 as u8 68 a[2] = ((port / 256) % 256) as u8 69 a[3] = (port % 256) as u8 70 a[4] = 127 as u8 71 a[5] = 0 as u8 72 a[6] = 0 as u8 73 a[7] = 1 as u8 74 return a 75} 76 77func main() -> i64 { 78 cx_p("=== nx_cloexec_gate -- can a forked child inherit a listening socket and hold the port hostage? ===\n" as *u8) 79 var pass: i64 = 0 80 var total: i64 = 0 81 82 // T1 CONTROL: a raw socket with NO fix must read the bit CLEAR. This is what makes T3/T4 meaningful -- 83 // if the kernel set FD_CLOEXEC by default, those would pass without our code doing anything. 84 let raw: i64 = sys_socket(CX_AF_INET, CX_SOCK_STREAM, 0) 85 var t1: i64 = 0 86 if raw >= 0 { if cx_is_cloexec(raw) == 0 { t1 = 1 } } 87 pass = pass + cx_ck("T1 CONTROL: a raw socket() is NOT FD_CLOEXEC (the disease state is reachable)" as *u8, t1); total = total + 1 88 89 // T2 the mechanism itself: setting the bit is observable through the same reader. 90 var t2: i64 = 0 91 if raw >= 0 { 92 __syscall(CX_FCNTL, raw, CX_F_SETFD, CX_FD_CLOEXEC, 0, 0, 0) 93 if cx_is_cloexec(raw) == 1 { t2 = 1 } 94 } 95 pass = pass + cx_ck("T2 fcntl(F_SETFD, FD_CLOEXEC) sets the bit and the reader observes it" as *u8, t2); total = total + 1 96 if raw >= 0 { sys_close(raw) } 97 98 // T3 THE REAL PRIMITIVE: nx_http_server_listen must hand back an already-CLOEXEC fd. This calls the 99 // SHIPPING function, not a mirror of it, so it cannot drift from what daemons actually run. 100 let v1: *i64 = sys_mmap(16) 101 let fd1: i64 = nx_http_server_listen(cx_addr(39001), 4, v1) 102 var t3: i64 = 0 103 if fd1 >= 0 { if cx_is_cloexec(fd1) == 1 { t3 = 1 } } 104 pass = pass + cx_ck("T3 nx_http_server_listen returns an FD_CLOEXEC listener (52+ daemons inherit this)" as *u8, t3); total = total + 1 105 106 // T4 same for the hot/SO_REUSEPORT listener -- it matters MORE there, because co-binding means two 107 // owners of one port and twice the chance a child inherits one. 108 let v2: *i64 = sys_mmap(16) 109 let fd2: i64 = nx_http_server_listen_hot(cx_addr(39002), 4, v2) 110 var t4: i64 = 0 111 if fd2 >= 0 { if cx_is_cloexec(fd2) == 1 { t4 = 1 } } 112 pass = pass + cx_ck("T4 nx_http_server_listen_hot returns an FD_CLOEXEC listener (SO_REUSEPORT path)" as *u8, t4); total = total + 1 113 114 // T5 NEG-CONTROL on the READER: a fresh un-fixed socket must still read CLEAR even now. If this ever 115 // reports 1, cx_is_cloexec is returning a constant and T3/T4 are decoration. 116 let raw2: i64 = sys_socket(CX_AF_INET, CX_SOCK_STREAM, 0) 117 var t5: i64 = 0 118 if raw2 >= 0 { if cx_is_cloexec(raw2) == 0 { t5 = 1 } } 119 pass = pass + cx_ck("T5 NEG-CONTROL: the reader still reports CLEAR for an unfixed fd (not a constant)" as *u8, t5); total = total + 1 120 if raw2 >= 0 { sys_close(raw2) } 121 122 // T6 NON-VACUITY: the listeners must have actually BOUND. A bind failure would make T3/T4 skip their 123 // assertion and score 0 anyway, but state it explicitly so an inconclusive run can never read as green. 124 var t6: i64 = 0 125 if fd1 >= 0 { if fd2 >= 0 { t6 = 1 } } 126 pass = pass + cx_ck("T6 NON-VACUITY: both listeners really bound (a skipped test is not a passed one)" as *u8, t6); total = total + 1 127 if fd1 >= 0 { sys_close(fd1) } 128 if fd2 >= 0 { sys_close(fd2) } 129 130 cx_p("---- nx_cloexec_gate " as *u8); cx_pn(pass); cx_p(" / " as *u8); cx_pn(total); cx_p(" ----\n" as *u8) 131 if pass == total { 132 let lg: i64 = sys_openat_append(CX_LOG, CX_MODE) 133 if lg >= 0 { cx_w(lg, "NX-CLOEXEC verdict=GREEN listening sockets are FD_CLOEXEC; a forked child cannot hold the port hostage\n" as *u8); sys_close(lg) } 134 cx_p("verdict=GREEN (no forked child can inherit a listening socket from this tree)\n" as *u8) 135 return 0 136 } 137 cx_p("verdict=RED\n" as *u8) 138 sys_exit(1) 139 return 1 140}