From f9fedd7884c63307553e3111b914708a55dd99a5 Mon Sep 17 00:00:00 2001 From: Guy Bedford Date: Tue, 18 Aug 2026 17:17:59 -0700 Subject: [PATCH] [NODERAWSOCKETS] Bind-first connect for synchronous getsockname An unbound TCP client connect created a plain async net.Socket, with saddr/sport only recorded on the async 'connect' event, so getsockname() immediately after a non-blocking connect() returned 0.0.0.0:0. Kernel semantics assign the ephemeral source port synchronously at connect(), so callers (e.g. mio) that read the local address right after a non-blocking connect raced the event loop. connect() now binds an ephemeral port first through the same eager synchronous bindHandle path an explicit bind() takes, then connects through the bound handle, making getsockname() correct up front. --- src/lib/libsockfs_node.js | 25 ++-- test/sockets/test_tcp_connect_getsockname.c | 151 ++++++++++++++++++++ test/test_sockets_node.py | 6 + 3 files changed, 171 insertions(+), 11 deletions(-) create mode 100644 test/sockets/test_tcp_connect_getsockname.c diff --git a/src/lib/libsockfs_node.js b/src/lib/libsockfs_node.js index f939744f41bf5..b031e4fd03dc2 100644 --- a/src/lib/libsockfs_node.js +++ b/src/lib/libsockfs_node.js @@ -22,8 +22,10 @@ // net.BoundSocket when the runtime offers it, else the private tcp_wrap binding // as a fallback (net.Server's listen is async and cannot report an assigned // ephemeral port up front, so it can't drive bind on its own). connect() goes -// through net.Socket, adopting the bound handle when one exists so an explicit -// source address/port is honored, and otherwise letting the kernel assign one. +// through net.Socket, adopting the bound handle so an explicit source +// address/port is honored; an unbound client binds an ephemeral port first, +// since the kernel assigns the source port synchronously at connect() and +// getsockname() must report it immediately. // // UDP uses the public node:dgram socket when it exposes a synchronous bindSync // (a recent node addition that ships alongside connectSync), giving the @@ -573,15 +575,16 @@ var NodeSockFSLibrary = { sock.dport = port; sock.state = {{{ SOCK_STATE_CONNECTING }}}; var net = nodeSockHelpers.getNet(); - var conn; - if (sock.bound) { - // A prior bind() produced a real, already-bound handle; connect through - // it so the bound source address/port is honored by the kernel. - conn = new net.Socket({ handle: sock.bound, pauseOnCreate: true, allowHalfOpen: true }); - } else { - // Unbound client: let the kernel assign the source address/port. - conn = new net.Socket({ allowHalfOpen: true }); - } + if (!sock.bound) { + // The kernel assigns the ephemeral source port synchronously at + // connect(), so an unbound client binds an ephemeral port first (the + // same eager bindHandle path an explicit bind() takes) and getsockname() + // is correct immediately, not only once the async connect completes. + nodeSockHelpers.bindHandle(sock, addr.includes(':') ? '::' : '0.0.0.0', 0); + } + // Connect through the bound handle so the bound source address/port is + // honored by the kernel. + var conn = new net.Socket({ handle: sock.bound, pauseOnCreate: true, allowHalfOpen: true }); conn.once('connect', () => { sock.state = {{{ SOCK_STATE_CONNECTED }}}; sock.saddr = conn.localAddress; diff --git a/test/sockets/test_tcp_connect_getsockname.c b/test/sockets/test_tcp_connect_getsockname.c new file mode 100644 index 0000000000000..0502a477922d2 --- /dev/null +++ b/test/sockets/test_tcp_connect_getsockname.c @@ -0,0 +1,151 @@ +/* + * Copyright 2026 The Emscripten Authors. All rights reserved. + * Emscripten is available under two separate licenses, the MIT license and the + * University of Illinois/NCSA Open Source License. Both these licenses can be + * found in the LICENSE file. + * + * getsockname() immediately after a non-blocking connect() on an unbound + * client must report the kernel-assigned ephemeral source port: the kernel + * assigns it synchronously at connect(), not when the connection completes. + * The port must then stay the same once connected. This is plain POSIX and + * also builds and runs natively, so the same code can be checked against the + * host stack. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ +#include +#endif + +int listen_fd = -1; +int client_fd = -1; +int peer_fd = -1; +struct sockaddr_in dest; +bool connected = false; +uint16_t client_port = 0; // network order, recorded right after connect() + +void set_nonblocking(int fd) { + fcntl(fd, F_SETFL, O_NONBLOCK); +} + +void test_success(void) { + printf("done\n"); + if (listen_fd >= 0) close(listen_fd); + if (client_fd >= 0) close(client_fd); + if (peer_fd >= 0) close(peer_fd); +#ifdef __EMSCRIPTEN__ + emscripten_cancel_main_loop(); +#else + exit(0); +#endif +} + +void start_client(void) { + if (client_fd >= 0) close(client_fd); + client_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(client_fd >= 0); + set_nonblocking(client_fd); + connected = false; + int r = connect(client_fd, (struct sockaddr*)&dest, sizeof(dest)); + assert((r == 0 || errno == EINPROGRESS) && "connect"); + + // The ephemeral source port is assigned synchronously at connect(): it must + // be visible here, before the connection completes or any event turn runs. + struct sockaddr_in sa; + socklen_t sl = sizeof(sa); + int g = getsockname(client_fd, (struct sockaddr*)&sa, &sl); + assert(g == 0 && "getsockname after connect"); + assert(ntohs(sa.sin_port) != 0 && "ephemeral port assigned at connect()"); + client_port = sa.sin_port; + printf("connecting from port %u\n", (unsigned)ntohs(client_port)); +} + +void main_loop(void) { + fd_set fdr, fdw; + struct timeval tv = {0}; + FD_ZERO(&fdr); + FD_ZERO(&fdw); + FD_SET(listen_fd, &fdr); + FD_SET(client_fd, &fdw); + select(64, &fdr, &fdw, NULL, &tv); + + if (peer_fd < 0 && FD_ISSET(listen_fd, &fdr)) { + peer_fd = accept(listen_fd, NULL, NULL); + if (peer_fd >= 0) set_nonblocking(peer_fd); + } + + if (!connected && FD_ISSET(client_fd, &fdw)) { + int err = 0; + socklen_t l = sizeof(err); + getsockopt(client_fd, SOL_SOCKET, SO_ERROR, &err, &l); + if (err == ECONNREFUSED || err == ECONNRESET) { + start_client(); + return; + } + assert(err == 0 && "connect failed"); + connected = true; + + // The name must not change when the connection completes. + struct sockaddr_in sa; + socklen_t sl = sizeof(sa); + assert(getsockname(client_fd, (struct sockaddr*)&sa, &sl) == 0); + assert(sa.sin_port == client_port && "port stable across connect completion"); + test_success(); + } +} + +int main(void) { + listen_fd = socket(AF_INET, SOCK_STREAM, 0); + assert(listen_fd >= 0); + + struct sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(0); + inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); + if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) != 0) { + perror("bind"); + return 1; + } + if (listen(listen_fd, 4) != 0) { + perror("listen"); + return 1; + } + + struct sockaddr_in la; + socklen_t ll = sizeof(la); + if (getsockname(listen_fd, (struct sockaddr*)&la, &ll) != 0) { + perror("getsockname"); + return 1; + } + set_nonblocking(listen_fd); + + memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + dest.sin_port = la.sin_port; + inet_pton(AF_INET, "127.0.0.1", &dest.sin_addr); + start_client(); + +#ifdef __EMSCRIPTEN__ + emscripten_set_main_loop(main_loop, 0, 0); +#else + while (1) { + main_loop(); + usleep(1000); + } +#endif + return 0; +} diff --git a/test/test_sockets_node.py b/test/test_sockets_node.py index 97750c862641c..16ec7db2815aa 100644 --- a/test/test_sockets_node.py +++ b/test/test_sockets_node.py @@ -126,6 +126,12 @@ def test_noderawsockets_client_bind(self): server.server_close() thread.join() + def test_noderawsockets_connect_getsockname(self): + # getsockname() immediately after a non-blocking connect() on an unbound + # client reports the ephemeral source port synchronously (kernel semantics: + # the port is assigned at connect(), not when the connection completes). + self.do_runf('sockets/test_tcp_connect_getsockname.c', 'done\n', cflags=['-sNODERAWSOCKETS']) + def test_noderawsockets_client_semantics(self): # EISCONN on a second connect, shutdown(SHUT_WR) leaving reads working, # EPIPE on a write after that, and POLLHUP after a full shutdown(SHUT_RDWR).