nx_connect.nx source
↩ module page · 94 lines · 5442 B
1// nx_connect.nx -- BOUNDED, NON-BLOCKING TCP CONNECT. The missing primitive.
2//
3// WHY THIS EXISTS (2026-07-31, measured). Every stall this substrate has blamed on "slow peers" or
4// "firewalled trackers" reduces to ONE fact:
5//
6// **** SO_RCVTIMEO / SO_SNDTIMEO DO NOT BOUND connect(). ****
7//
8// sys_set_socket_timeout() looks like protection and is not. A blocking connect() to a black-holed
9// host runs to the kernel's SYN-retry exhaustion -- ~127 SECONDS -- no matter what socket timeouts
10// are set. There were 100 sys_connect() call sites in runtime/ and NOT ONE of them was bounded.
11//
12// The substrate already knew, and worked AROUND it instead of fixing it. From nx_torrent_get.nx:
13// "Run the wide announce in a DETACHED grandchild (double-fork) so a firewalled tracker's ~127s
14// connect() hang can NEVER stall the download (nx_http_client has no connect timeout -- the very
15// reason peers are fork-bounded)."
16// A double-fork to dodge a missing timeout is a workaround, and it leaks the cost elsewhere: the
17// forking parent still could not run its own reaper, so peer slots went idle and children piled up
18// as zombies (measured: 32 zombies == the entire peer pool, parent 89.2% parked).
19//
20// nx_syscalls.nx's own sys_poll() comment already declared the intent -- "Used by the substrate's own
21// network diagnostics (bounded non-blocking connect)" -- but the function was never written. This is it.
22//
23// MECHANISM: set O_NONBLOCK, connect() (expect -EINPROGRESS), poll(POLLOUT) with a real deadline, then
24// re-issue connect() to learn the OUTCOME. That last step is not optional: poll reports a socket
25// writable both when the handshake SUCCEEDED and when it FAILED (ECONNREFUSED / EHOSTUNREACH). The
26// second connect returns 0 or -EISCONN for an established connection and the actual errno otherwise.
27// The original file flags are SAVED and restored on every exit path, so callers get back exactly the
28// socket they handed in -- blocking-mode callers stay blocking, and no caller needs to know this
29// happened. That is what makes it a drop-in for the 100 existing sites.
30//
31// expect_exit: 0 license_tier: ORIGINAL
32import "nx_syscalls.nx"
33import "nx_fcntl.nx"
34
35// The compiler does not fold `0 - N` in a const initialiser, so errnos are stored POSITIVE and
36// negated at the comparison site.
37// ---- ROOT-FIXED 2026-07-31: this module used to carry its own syscall numbers ---------------------
38// It briefly hard-coded fcntl=72/poll=7 because the shared wrapper returned -EINVAL for every caller.
39// That was NOT an inverted @ifdef guard (my first diagnosis, retracted): the design is deliberate --
40// TARGET_X86_64 is hard-pinned undefined, the @ifndef (RV64) branch is what compiles, and the backend's
41// x86ctx_rv64_to_x86_64_syscall table translates at emit. The real defect was ONE MISSING ROW: rv64 25
42// (fcntl) had no entry, and that translator's default is `return num`, so 25 sailed through to x86_64
43// 25 = mremap -> EINVAL, silently. Fixed in nx_x86_64_ctx.nx and ratcheted by nx_syscall_xlate_gate T7
44// (6/7 RED before the row, 7/7 GREEN after). So the shared wrappers are correct now and this module
45// uses them -- carrying private copies of syscall numbers is exactly how the next one of these hides.
46const NX_CONN_EINPROGRESS: i64 = 115
47const NX_CONN_EISCONN: i64 = 106
48const NX_CONN_POLLOUT: i64 = 4
49const NX_CONN_PFD_SZ: i64 = 16
50
51// Default budget for a peer/tracker connect. A reachable host on any real path completes its SYN
52// handshake well inside this; anything slower is not worth a slot. Chosen against the ~127s kernel
53// SYN-retry ceiling this exists to replace, not by taste.
54const NX_CONN_DEFAULT_MS: i64 = 6000
55
56// connect(fd, addr, addrlen) bounded by timeout_ms. Returns 0 on an ESTABLISHED connection,
57// -1 on timeout, refusal, or any other error. fd is left OPEN in every case: the caller owns it and
58// closes it, exactly as with a raw sys_connect.
59func nx_connect_bounded(fd: i64, addr: *u8, addrlen: i64, timeout_ms: i64) -> i64 {
60 let orig: i64 = nx_fcntl(fd, NX_F_GETFL, 0)
61 if orig < 0 { return 0 - 1 }
62 if nx_fcntl(fd, NX_F_SETFL, orig | NX_O_NONBLOCK) < 0 { return 0 - 1 }
63
64 let cr: i64 = sys_connect(fd, addr, addrlen)
65 if cr == 0 { nx_fcntl(fd, NX_F_SETFL, orig); return 0 }
66 if cr != (0 - NX_CONN_EINPROGRESS) { nx_fcntl(fd, NX_F_SETFL, orig); return 0 - 1 }
67
68 // struct pollfd { i32 fd; i16 events; i16 revents } -- 8 bytes, little-endian.
69 let pfd: *u8 = sys_mmap(NX_CONN_PFD_SZ)
70 pfd[0] = (fd & 255) as u8
71 pfd[1] = ((fd >> 8) & 255) as u8
72 pfd[2] = ((fd >> 16) & 255) as u8
73 pfd[3] = ((fd >> 24) & 255) as u8
74 pfd[4] = (NX_CONN_POLLOUT & 255) as u8
75 pfd[5] = 0 as u8
76 pfd[6] = 0 as u8
77 pfd[7] = 0 as u8
78
79 let pr: i64 = sys_poll(pfd, 1, timeout_ms)
80 if pr <= 0 { nx_fcntl(fd, NX_F_SETFL, orig); return 0 - 1 }
81
82 // Writable does NOT mean connected. Ask connect() what actually happened, while the socket is
83 // still non-blocking so this probe itself can never park.
84 let c2: i64 = sys_connect(fd, addr, addrlen)
85 nx_fcntl(fd, NX_F_SETFL, orig)
86 if c2 == 0 { return 0 }
87 if c2 == (0 - NX_CONN_EISCONN) { return 0 }
88 return 0 - 1
89}
90
91// Same contract at the default budget, for the many call sites that just want "don't hang".
92func nx_connect_ok(fd: i64, addr: *u8, addrlen: i64) -> i64 {
93 return nx_connect_bounded(fd, addr, addrlen, NX_CONN_DEFAULT_MS)
94}