code wiki / _hdl_build / nx_acceptq.nx
nx_acceptq.nx source
↩ module page · 156 lines · 6711 B
1// nx_acceptq.nx -- DETECTOR for the ACCEPT-LOOP WEDGE: a daemon that holds its listen socket but has
2// stopped calling accept().
3//
4// WHY THIS EXISTS (2026-07-31, measured). The mgmt API blocked in wait4 for the entire life of a forked
5// build. Connections completed the TCP handshake -- the KERNEL accepts them into the backlog -- so every
6// port-probe health check reported UP, for six minutes at a time, while no request was served. Three
7// separate lanes filed it as "MGMT :18098 DOWN, ALL SEATS". :18098 was never down. It was blocked.
8//
9// ★★★ THE INSIGHT THIS ORGAN ENCODES: A SUCCESSFUL CONNECT PROVES THE KERNEL ACCEPTED, NOT THE PROCESS.
10// The two are indistinguishable from outside -- unless you read the one number the kernel already keeps:
11// on a LISTEN socket, /proc/net/tcp's rx_queue IS THE COUNT OF COMPLETED-BUT-UNACCEPTED CONNECTIONS.
12// Non-zero means the app is behind. Sustained non-zero means the app is not accepting at all.
13// Measured on the live wedge: rx_queue climbed 8 -> 34 while wedged, and returned to 0 once fixed.
14//
15// This is a pure READER: it opens /proc/net/tcp and nothing else. It cannot perturb the thing it measures,
16// which matters because the failure it hunts is caused by blocking.
17//
18// nx_acceptq -> report every LISTEN socket with a non-zero accept backlog
19// nx_acceptq <port> -> report just that port; exit 1 if it looks wedged
20// expect_exit: 0 license_tier: ORIGINAL
21import "nx_syscalls.nx"
22
23const AQ_CAP: i64 = 1048576
24
25func aq_puts(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 }
26
27func aq_putn(v: i64) -> i64 {
28 let b: *u8 = sys_mmap(32)
29 if v == 0 { b[0] = 48 as u8; sys_write(1, b, 1); return 0 }
30 var n: i64 = 0
31 var x: i64 = v
32 while x > 0 { b[n] = ((x - (x / 10) * 10) + 48) as u8; n = n + 1; x = x / 10 }
33 let r: *u8 = sys_mmap(32)
34 var i: i64 = 0
35 while i < n { r[i] = b[n - 1 - i]; i = i + 1 }
36 sys_write(1, r, n)
37 return 0
38}
39
40// one hex digit -> value, or -1
41func aq_hexv(c: u8) -> i64 {
42 if c >= (48 as u8) { if c <= (57 as u8) { return (c as i64) - 48 } }
43 if c >= (65 as u8) { if c <= (70 as u8) { return (c as i64) - 55 } }
44 if c >= (97 as u8) { if c <= (102 as u8) { return (c as i64) - 87 } }
45 return 0 - 1
46}
47
48// parse `count` hex chars at off; -1 on any non-hex
49func aq_hex(b: *u8, off: i64, count: i64) -> i64 {
50 var v: i64 = 0
51 var i: i64 = 0
52 while i < count {
53 let d: i64 = aq_hexv(b[off + i])
54 if d < 0 { return 0 - 1 }
55 v = v * 16 + d
56 i = i + 1
57 }
58 return v
59}
60
61func aq_is_sp(c: u8) -> i64 { if c == (32 as u8) { return 1 } if c == (9 as u8) { return 1 } return 0 }
62
63// advance past spaces, then past one token; returns offset of the NEXT token start
64func aq_next(b: *u8, off: i64, n: i64) -> i64 {
65 var i: i64 = off
66 while i < n { if aq_is_sp(b[i]) == 0 { break } i = i + 1 }
67 while i < n { if aq_is_sp(b[i]) == 1 { break } i = i + 1 }
68 while i < n { if aq_is_sp(b[i]) == 0 { break } i = i + 1 }
69 return i
70}
71
72// Scan /proc/net/tcp. Layout per row (after the header):
73// sl local_address rem_address st tx_queue:rx_queue ...
74// st == 0A is LISTEN. For a LISTEN socket rx_queue is the ACCEPT BACKLOG DEPTH.
75// want_port < 0 means "report all". Returns the number of wedge-suspect listeners found.
76func aq_scan(want_port: i64) -> i64 {
77 let box: *i64 = (sys_mmap(8)) as *i64
78 box[0] = 0
79 let buf: *u8 = sys_read_file("/proc/net/tcp" as *u8, box)
80 if (buf as i64) == 0 { aq_puts("ACCEPTQ: cannot read /proc/net/tcp\n" as *u8); return 0 - 1 }
81 let n: i64 = box[0]
82 var flagged: i64 = 0
83 var shown: i64 = 0
84 var i: i64 = 0
85 // skip the header line
86 while i < n { if buf[i] == (10 as u8) { i = i + 1; break } i = i + 1 }
87 while i < n {
88 let ls: i64 = i
89 var le: i64 = i
90 while le < n { if buf[le] == (10 as u8) { break } le = le + 1 }
91 if le - ls > 40 {
92 // field 1 = local_address "AAAAAAAA:PPPP"; find the ':' after the sl token
93 let f1: i64 = aq_next(buf, ls, le)
94 var cp: i64 = f1
95 while cp < le { if buf[cp] == (58 as u8) { break } cp = cp + 1 }
96 if cp + 4 < le {
97 let port: i64 = aq_hex(buf, cp + 1, 4)
98 let f2: i64 = aq_next(buf, f1, le) // rem_address
99 let f3: i64 = aq_next(buf, f2, le) // st
100 let st: i64 = aq_hex(buf, f3, 2)
101 let f4: i64 = aq_next(buf, f3, le) // tx_queue:rx_queue
102 var qc: i64 = f4
103 while qc < le { if buf[qc] == (58 as u8) { break } qc = qc + 1 }
104 let rxq: i64 = aq_hex(buf, qc + 1, 8)
105 if st == 10 { // 0x0A = LISTEN
106 var pick: i64 = 0
107 if want_port < 0 { if rxq > 0 { pick = 1 } }
108 if want_port >= 0 { if port == want_port { pick = 1 } }
109 if pick == 1 {
110 shown = shown + 1
111 aq_puts("port=" as *u8); aq_putn(port)
112 aq_puts(" accept_backlog=" as *u8); aq_putn(rxq)
113 if rxq > 0 {
114 flagged = flagged + 1
115 aq_puts(" WEDGE-SUSPECT: connections are COMPLETED but UNACCEPTED -- the port is open and the process is NOT calling accept()\n" as *u8)
116 } else {
117 aq_puts(" ok (listener is draining its backlog)\n" as *u8)
118 }
119 }
120 }
121 }
122 }
123 i = le + 1
124 }
125 if shown == 0 {
126 if want_port < 0 { aq_puts("ACCEPTQ: no listener has a non-zero accept backlog -- nothing is wedged\n" as *u8) }
127 else { aq_puts("ACCEPTQ: no LISTEN socket on that port\n" as *u8) }
128 }
129 return flagged
130}
131
132func main(argc: i64, argv: *i64) -> i64 {
133 var want: i64 = 0 - 1
134 if argc >= 2 {
135 let a: *u8 = argv[1] as *u8
136 var v: i64 = 0
137 var i: i64 = 0
138 var ok: i64 = 1
139 while a[i] != (0 as u8) {
140 if a[i] < (48 as u8) { ok = 0 }
141 if a[i] > (57 as u8) { ok = 0 }
142 if ok == 0 { break }
143 v = v * 10 + ((a[i] as i64) - 48)
144 i = i + 1
145 }
146 if ok == 1 { want = v }
147 }
148 aq_puts("=== nx_acceptq -- accept-loop wedge detector ===\n" as *u8)
149 let f: i64 = aq_scan(want)
150 if f < 0 { return 2 }
151 if f > 0 {
152 aq_puts("verdict=WEDGED listeners_flagged=" as *u8); aq_putn(f); aq_puts("\n" as *u8)
153 return 1
154 }
155 aq_puts("verdict=GREEN\n" as *u8)
156 return 0
157}