code wiki / (root) / nx_connect.nx

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}