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}