Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 140 additions & 138 deletions packages/bun-usockets/src/bsd.c
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,81 @@ static int us_internal_bind_and_listen(LIBUS_SOCKET_DESCRIPTOR listenFd, struct
return result;
}

#ifdef _WIN32
#define LIBUS_EAFNOSUPPORT WSAEAFNOSUPPORT
#define LIBUS_EADDRNOTAVAIL WSAEADDRNOTAVAIL
#else
#define LIBUS_EAFNOSUPPORT EAFNOSUPPORT
#define LIBUS_EADDRNOTAVAIL EADDRNOTAVAIL
#endif

/* Stable partition of a getaddrinfo chain: the AF_INET6 entries first, then the
* rest, each group in its original order. Only `ai_next` is rewritten, and every
* node stays in the chain, so freeaddrinfo() on the returned head frees the same
* nodes it would have freed before. */
static struct addrinfo *bsd_addrinfo_inet6_first(struct addrinfo *list) {
struct addrinfo *inet6_head = NULL, **inet6_tail = &inet6_head;
struct addrinfo *other_head = NULL, **other_tail = &other_head;
for (struct addrinfo *a = list, *next; a != NULL; a = next) {
next = a->ai_next;
a->ai_next = NULL;
if (a->ai_family == AF_INET6) {
*inet6_tail = a;
inet6_tail = &a->ai_next;
} else {
*other_tail = a;
other_tail = &a->ai_next;
}
}
*inet6_tail = other_head;
return inet6_head;
}

/* Resolves the address a passive socket binds. Callers walk the chain in order
* and bind the first usable entry.
*
* A named host keeps the resolver's order, so the listener lands on the address
* the machine's own policy puts first (gai.conf on glibc, the prefix policy on
* Windows). Node binds the first dns.lookup() result in the same way. A fixed
* IPv6-first walk put a "localhost" listener on ::1 on hosts configured to
* prefer IPv4, where every other tool connects to 127.0.0.1.
*
* The wildcard (host == NULL) has no resolver order to follow. glibc returns
* 0.0.0.0 before ::, so the chain is reordered to keep the default listener on
* the dual-stack `::`. Returns the getaddrinfo() status. */
static int bsd_listen_getaddrinfo(const char *host, int port, int socktype, struct addrinfo **result) {
struct addrinfo hints;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = socktype;

char port_string[16];
snprintf(port_string, 16, "%d", port);

int rc = getaddrinfo(host, port_string, &hints, result);
if (rc == 0 && host == NULL) {
*result = bsd_addrinfo_inet6_first(*result);
}
return rc;
}

/* Decides whether a failed bind() of one entry lets the walk move on to the
* next one.
*
* The wildcard falls through on any error, as it always did: a `::` bind that
* fails still gets a 0.0.0.0 listener.
*
* A named host falls through only when bind() says the address cannot be bound
* on this host at all: EADDRNOTAVAIL (::1 with IPv6 disabled on lo) or
* EAFNOSUPPORT (no IPv6 stack). This is broader than node, which reports
* EADDRNOTAVAIL for a record that is not local. Every other error is about this
* port (EADDRINUSE, EACCES) and is reported: binding the other loopback family
* instead would hide a port conflict that node reports. */
static int bsd_bind_should_try_next(const char *host, int err) {
return host == NULL || err == LIBUS_EAFNOSUPPORT || err == LIBUS_EADDRNOTAVAIL;
}

/* libuv's uv__sock_reuseaddr: SO_REUSEPORT on the BSDs where that is what
* actually allows a second bind of the same address, SO_REUSEADDR elsewhere. */
int bsd_set_reuseaddr(LIBUS_SOCKET_DESCRIPTOR listenFd) {
Expand Down Expand Up @@ -1173,6 +1248,7 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd
) {

if (bsd_set_reuse(listenFd, options) != 0) {
*error = LIBUS_ERR;
return LIBUS_SOCKET_ERROR;
}

Expand All @@ -1189,6 +1265,7 @@ inline __attribute__((always_inline)) LIBUS_SOCKET_DESCRIPTOR bsd_bind_listen_fd
if (listenAddr->ai_family == AF_INET6) {
int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) != 0) {
*error = LIBUS_ERR;
return LIBUS_SOCKET_ERROR;
}
}
Expand Down Expand Up @@ -1277,16 +1354,8 @@ int bsd_socket_listen_error_is_benign(LIBUS_SOCKET_DESCRIPTOR fd) {

LIBUS_SOCKET_DESCRIPTOR bsd_create_bound_socket(const char *host, int port, int options, int *out_port, int *error) {
bsd_winsock_ensure();
struct addrinfo hints, *result;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

char port_string[16];
snprintf(port_string, 16, "%d", port);

int gai = getaddrinfo(host, port_string, &hints, &result);
struct addrinfo *result;
int gai = bsd_listen_getaddrinfo(host, port, SOCK_STREAM, &result);
if (gai != 0) {
#ifdef _WIN32
*error = gai;
Expand All @@ -1297,36 +1366,32 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_bound_socket(const char *host, int port, int
}

LIBUS_SOCKET_DESCRIPTOR fd = LIBUS_SOCKET_ERROR;
for (int family = AF_INET6; fd == LIBUS_SOCKET_ERROR && family >= AF_INET; family -= (AF_INET6 - AF_INET)) {
for (struct addrinfo *a = result; a != NULL; a = a->ai_next) {
if (a->ai_family != family) {
continue;
}
fd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, NULL);
if (fd == LIBUS_SOCKET_ERROR) {
*error = LIBUS_ERR;
continue;
}
for (struct addrinfo *a = result; a != NULL; a = a->ai_next) {
fd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, error);
if (fd == LIBUS_SOCKET_ERROR) {
continue;
}
#if defined(SO_REUSEADDR) && !defined(_WIN32)
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
#endif
#ifdef IPV6_V6ONLY
if (a->ai_family == AF_INET6) {
int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &enabled, sizeof(enabled));
}
if (a->ai_family == AF_INET6) {
int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void *) &enabled, sizeof(enabled));
}
#endif
int rc;
do
rc = bind(fd, a->ai_addr, (socklen_t) a->ai_addrlen);
while (IS_EINTR(rc));
if (rc != 0) {
*error = LIBUS_ERR;
bsd_close_socket(fd);
fd = LIBUS_SOCKET_ERROR;
continue;
}
int rc;
do
rc = bind(fd, a->ai_addr, (socklen_t) a->ai_addrlen);
while (IS_EINTR(rc));
if (rc == 0) {
break;
}
*error = LIBUS_ERR;
bsd_close_socket(fd);
fd = LIBUS_SOCKET_ERROR;
if (!bsd_bind_should_try_next(host, *error)) {
break;
}
}
Expand Down Expand Up @@ -1359,53 +1424,25 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_bound_socket(const char *host, int port, int

LIBUS_SOCKET_DESCRIPTOR bsd_create_listen_socket(const char *host, int port, int options, int* error) {
bsd_winsock_ensure();
struct addrinfo hints, *result;
memset(&hints, 0, sizeof(struct addrinfo));

hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;

char port_string[16];
snprintf(port_string, 16, "%d", port);

if (getaddrinfo(host, port_string, &hints, &result)) {
struct addrinfo *result;
if (bsd_listen_getaddrinfo(host, port, SOCK_STREAM, &result) != 0) {
return LIBUS_SOCKET_ERROR;
}

LIBUS_SOCKET_DESCRIPTOR listenFd = LIBUS_SOCKET_ERROR;
struct addrinfo *listenAddr;
for (struct addrinfo *a = result; a != NULL; a = a->ai_next) {
if (a->ai_family == AF_INET6) {
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, NULL);
if (listenFd == LIBUS_SOCKET_ERROR) {
continue;
}

listenAddr = a;
if (bsd_bind_listen_fd(listenFd, listenAddr, port, options, error) != LIBUS_SOCKET_ERROR) {
freeaddrinfo(result);
return listenFd;
}

bsd_close_socket(listenFd);
LIBUS_SOCKET_DESCRIPTOR listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, error);
if (listenFd == LIBUS_SOCKET_ERROR) {
continue;
}
}

for (struct addrinfo *a = result; a != NULL; a = a->ai_next) {
if (a->ai_family == AF_INET) {
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, NULL);
if (listenFd == LIBUS_SOCKET_ERROR) {
continue;
}

listenAddr = a;
if (bsd_bind_listen_fd(listenFd, listenAddr, port, options, error) != LIBUS_SOCKET_ERROR) {
freeaddrinfo(result);
return listenFd;
}
if (bsd_bind_listen_fd(listenFd, a, port, options, error) != LIBUS_SOCKET_ERROR) {
freeaddrinfo(result);
return listenFd;
}

bsd_close_socket(listenFd);
bsd_close_socket(listenFd);
if (!bsd_bind_should_try_next(host, *error)) {
Comment on lines +1443 to +1444

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Users whose first loopback entry fails bind() for any reason other than the two allowed errnos now get a listen error where the base silently bound the next family. bsd_bind_should_try_next at bsd.c:1180 lets a named host move on only for EAFNOSUPPORT or EADDRNOTAVAIL; every other code stops the walk at bsd.c:1444 and bsd.c:1391. The new *error writes at bsd.c:1251 and bsd.c:1268 feed that check too, so a failing IPV6_V6ONLY setsockopt or bsd_set_reuse on the AF_INET6 entry now ends the walk. Fix: stop the walk only for the port-conflict codes (EADDRINUSE, and WSAEACCES on Windows) and keep trying the remaining entries for everything else, so sandbox and setsockopt failures on one family still reach the other, as on the base.

Why this was flagged

Trigger: listen(port, "localhost") where getaddrinfo lists ::1 first and the ::1 attempt fails with an errno outside {EAFNOSUPPORT, EADDRNOTAVAIL}. Examples reachable from Bun.listen/Bun.serve/node:net: a macOS sandbox-exec profile that allows network-bind only for ip4 (bind on the AF_INET6 socket returns EPERM), or a container whose IPV6_V6ONLY setsockopt fails at bsd.c:1267. On the base, bsd_create_listen_socket closed the fd and went on to the AF_INET loop on any bsd_bind_listen_fd failure, and the IPV6_V6ONLY path returned without writing *error, so 127.0.0.1 was bound silently. The new code sets *error at bsd.c:1251/1268/1094, then bsd_bind_should_try_next(host, *error) at bsd.c:1444 returns 0 for EPERM/EACCES/ENOPROTOOPT and the loop breaks; the same break exists at bsd.c:1391 for the cluster path. The caller gets a thrown listen error (Listener.rs:538) or Bun.serve's errno-based error instead of a working 127.0.0.1 listener. Remedy: stop only on EADDRINUSE (and WSAEACCES on Windows), continue on all other errors.

Verification: bsd_bind_should_try_next at bsd.c:1177-1179 is host == NULL || err == LIBUS_EAFNOSUPPORT || err == LIBUS_EADDRNOTAVAIL. In bsd_create_listen_socket the walk does if (!bsd_bind_should_try_next(host, *error)) { break; } at bsd.c:1443-1446. On the base, the AF_INET6 loop fell through to the AF_INET loop unconditionally, so any ::1 failure previously ended with a working 127.0.0.1 listener; now the listen call reports the error.

break;
}
}

Expand Down Expand Up @@ -1752,88 +1789,53 @@ LIBUS_SOCKET_DESCRIPTOR bsd_create_udp_socket(const char *host, int port, int op
*err = 0;
}

struct addrinfo hints, *result;
memset(&hints, 0, sizeof(struct addrinfo));

hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;

char port_string[16];
snprintf(port_string, 16, "%d", port);

int gai_result = getaddrinfo(host, port_string, &hints, &result);
struct addrinfo *result;
int gai_result = bsd_listen_getaddrinfo(host, port, SOCK_DGRAM, &result);
if (gai_result != 0) {
if (err != NULL) {
*err = -gai_result;
}
return LIBUS_SOCKET_ERROR;
}

LIBUS_SOCKET_DESCRIPTOR listenFd = LIBUS_SOCKET_ERROR;
struct addrinfo *listenAddr = NULL;
for (struct addrinfo *a = result; a && listenFd == LIBUS_SOCKET_ERROR; a = a->ai_next) {
if (a->ai_family == AF_INET6) {
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, err);
listenAddr = a;
int last_error = 0;
for (struct addrinfo *a = result; a != NULL; a = a->ai_next) {
LIBUS_SOCKET_DESCRIPTOR listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, &last_error);
if (listenFd == LIBUS_SOCKET_ERROR) {
continue;
}
}

for (struct addrinfo *a = result; a && listenFd == LIBUS_SOCKET_ERROR; a = a->ai_next) {
if (a->ai_family == AF_INET) {
listenFd = bsd_create_socket(a->ai_family, a->ai_socktype, a->ai_protocol, err);
listenAddr = a;
int ok = bsd_set_reuse(listenFd, options) == 0;
#ifdef IPV6_V6ONLY
if (ok && a->ai_family == AF_INET6) {
int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
ok = setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) == 0;
}
}

if (listenFd == LIBUS_SOCKET_ERROR) {
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;
}

if (bsd_set_reuse(listenFd, options) != 0) {
if (err != NULL) {
#ifdef _WIN32
*err = WSAGetLastError();
#else
*err = errno;
#endif
if (ok) {
bsd_apply_udp_recv_options(listenFd, a->ai_family, options);
ok = bind(listenFd, a->ai_addr, (socklen_t) a->ai_addrlen) == 0;
}
bsd_close_socket(listenFd);
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;
}

#ifdef IPV6_V6ONLY
if (listenAddr->ai_family == AF_INET6) {
int enabled = (options & LIBUS_SOCKET_IPV6_ONLY) != 0;
if (setsockopt(listenFd, IPPROTO_IPV6, IPV6_V6ONLY, &enabled, sizeof(enabled)) != 0) {
return LIBUS_SOCKET_ERROR;
if (ok) {
freeaddrinfo(result);
if (err != NULL) {
*err = 0;
}
return listenFd;
}
}
#endif

bsd_apply_udp_recv_options(listenFd, listenAddr->ai_family, options);

/* We bind here as well */
if (bind(listenFd, listenAddr->ai_addr, (socklen_t) listenAddr->ai_addrlen)) {
if (err != NULL) {
#ifdef _WIN32
*err = WSAGetLastError();
#else
*err = errno;
#endif
}
last_error = LIBUS_ERR;
bsd_close_socket(listenFd);
freeaddrinfo(result);
return LIBUS_SOCKET_ERROR;
if (!bsd_bind_should_try_next(host, last_error)) {
break;
}
Comment on lines +1827 to +1831

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Operators running Bun.serve with http3 and no hostname can now get a QUIC listener on 0.0.0.0 while TCP sits on ::, where the base failed or retried. The new UDP walk at bsd.c:1829 falls through on any bind() error when host == NULL, so a UDP [::]:N conflict silently yields an IPv4-only QUIC socket. The retry loop in src/runtime/server/mod.rs:3129-3145 only fires when h3_listener is None, so neither the fresh-port retry nor the "Failed to listen on UDP port {h3_port} for HTTP/3" error runs. Fix: when the wildcard UDP walk cannot bind the same family the TCP listener bound, report the error so mod.rs can retry or throw, e.g. limit the wildcard fallthrough in bsd_create_udp_socket to EAFNOSUPPORT/EADDRNOTAVAIL, or have the H3 bind pass the TCP listener's bound family instead of NULL. [also at: packages/bun-usockets/src/bsd.c:1829 - HTTP/3 users whose wildcard UDP port is already held now get a server whose QUIC half silently lands on 0.0.0.0 while TCP stays on ::, instead of the error the base throws.]

Why this was flagged

Trigger: Bun.serve({ port: 0, http3: true }) with no hostname on Linux while another process holds a UDP socket on an IPv6-specific address at the port the TCP listener just got. The TCP walk binds [::]:N (bsd.c:1432-1441). The H3 app then calls uws_h3_app_listen_with_config with the same null host (src/runtime/server/mod.rs:3118-3128), which reaches bsd_create_udp_socket(NULL, N, ...). bind([::]:N) fails with EADDRINUSE; bsd_bind_should_try_next(NULL, EADDRINUSE) at bsd.c:1829 returns true because host == NULL, so the loop binds 0.0.0.0:N and returns success. On the base branch the old UDP code bound the first AF_INET6 entry once and returned LIBUS_SOCKET_ERROR on that failure, so h3_listener stayed None and mod.rs:3130-3137 closed TCP:N and retried with a fresh port (port 0) or threw "Failed to listen on UDP port {h3_port} for HTTP/3" (mod.rs:3140). After the change h3_listener is Some, the retry and the error are skipped, and the server advertises Alt-Svc on port N while QUIC is reachable only over IPv4.

Verification: When Bun.serve({ http3: true }) has no hostname and another process holds a UDP socket on an IPv6-only address, the new UDP walk silently binds 0.0.0.0:N where the base reported the failure. bsd_bind_should_try_next (bsd.c:1176) is return host == NULL || err == LIBUS_EAFNOSUPPORT || err == LIBUS_EADDRNOTAVAIL;, so with host == NULL every bind error moves on to the next entry.

}

freeaddrinfo(result);
if (err != NULL) {
*err = 0;
*err = last_error;
}
return listenFd;
return LIBUS_SOCKET_ERROR;
}

int bsd_connect_udp_socket(LIBUS_SOCKET_DESCRIPTOR fd, const char *host, int port) {
Expand Down
Loading
Loading