I filed FB25058707 and wanted to make sure it reaches the right people, because it causes silent datagram loss that's hard to trace.
On macOS, when a dual-stack UDP socket (AF_INET6 with IPV6_V6ONLY=0) binds [::]:0, the kernel sometimes gives it a port that an AF_INET socket already has bound to 127.0.0.1. Datagrams sent to 127.0.0.1 on that port then go to the AF_INET socket, so the dual-stack socket never receives them. An explicit bind of the same socket to that port fails with EADDRINUSE; only port-0 assignment hands it out.
This program binds 1000 AF_INET sockets to 127.0.0.1:0, then binds dual-stack sockets to [::]:0 and checks where a datagram to 127.0.0.1:port lands whenever the port is already held. It's loopback only and runs in a few seconds:
/*
* A dual-stack UDP socket (AF_INET6, IPV6_V6ONLY=0) bound to [::]:0 can be
* assigned a port already bound by an AF_INET socket on 127.0.0.1. Datagrams
* to 127.0.0.1:port then reach the AF_INET socket, not the dual-stack one.
* Loopback only. Build: cc -O2 -o dualstack_port0 dualstack_port0.c
*/
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#define HELD 1000
#define TRIALS 2000
static int held[65536];
static void die(const char *what) { perror(what); exit(1); }
static int port_of(int s) {
struct sockaddr_storage ss;
socklen_t len = sizeof(ss);
if (getsockname(s, (struct sockaddr *)&ss, &len) != 0) die("getsockname");
if (ss.ss_family == AF_INET) return ntohs(((struct sockaddr_in *)&ss)->sin_port);
return ntohs(((struct sockaddr_in6 *)&ss)->sin6_port);
}
static int bind_v4(in_addr_t addr) {
int s = socket(AF_INET, SOCK_DGRAM, 0);
if (s < 0) die("socket AF_INET");
struct sockaddr_in sin = { .sin_len = sizeof(sin), .sin_family = AF_INET, .sin_addr.s_addr = addr };
if (bind(s, (struct sockaddr *)&sin, sizeof(sin)) != 0) die("bind AF_INET");
return s;
}
static int bind_dual(void) {
int s = socket(AF_INET6, SOCK_DGRAM, 0), off = 0;
if (s < 0) die("socket AF_INET6");
if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &off, sizeof(off)) != 0) die("IPV6_V6ONLY");
struct sockaddr_in6 sin6 = { .sin6_len = sizeof(sin6), .sin6_family = AF_INET6, .sin6_addr = in6addr_any };
if (bind(s, (struct sockaddr *)&sin6, sizeof(sin6)) != 0) die("bind AF_INET6");
return s;
}
/* Returns 1 if s receives token within 100ms, skipping other datagrams. */
static int got(int s, const char *token) {
struct timeval tv = { .tv_sec = 0, .tv_usec = 100000 };
if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) die("SO_RCVTIMEO");
char buf[64];
for (;;) {
ssize_t n = recv(s, buf, sizeof(buf) - 1, 0);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
die("recv");
}
buf[n] = 0;
if (strcmp(buf, token) == 0) return 1;
}
}
int main(void) {
struct rlimit rl;
if (getrlimit(RLIMIT_NOFILE, &rl) != 0) die("getrlimit");
rl.rlim_cur = rl.rlim_max < 4096 ? rl.rlim_max : 4096;
if (setrlimit(RLIMIT_NOFILE, &rl) != 0) die("setrlimit");
memset(held, -1, sizeof(held));
for (int i = 0; i < HELD;) {
int s = bind_v4(htonl(INADDR_LOOPBACK)), p = port_of(s);
if (held[p] >= 0) { close(s); continue; }
held[p] = s;
i++;
}
int sender = bind_v4(htonl(INADDR_LOOPBACK));
for (int dual = 1; dual >= 0; dual--) {
int collisions = 0, to_held = 0, to_wild = 0;
for (int i = 0; i < TRIALS; i++) {
int w = dual ? bind_dual() : bind_v4(htonl(INADDR_ANY)), p = port_of(w);
if (held[p] >= 0) {
collisions++;
char token[32];
snprintf(token, sizeof(token), "%d-%d", dual, i);
struct sockaddr_in to = { .sin_len = sizeof(to), .sin_family = AF_INET, .sin_port = htons(p), .sin_addr.s_addr = htonl(INADDR_LOOPBACK) };
if (sendto(sender, token, strlen(token), 0, (struct sockaddr *)&to, sizeof(to)) < 0) die("sendto");
to_held += got(held[p], token);
to_wild += got(w, token);
}
close(w);
}
printf("%s: %d/%d wildcard binds got a port held by a 127.0.0.1 socket; the probe reached that socket %d times, the wildcard socket %d times\n",
dual ? "AF_INET6 V6ONLY=0 [::]:0" : "AF_INET 0.0.0.0:0 ", collisions, TRIALS, to_held, to_wild);
}
return 0;
}
On macOS 27.0 (26A428):
AF_INET6 V6ONLY=0 [::]:0: 122/2000 wildcard binds got a port held by a 127.0.0.1 socket; the probe reached that socket 122 times, the wildcard socket 0 times
AF_INET 0.0.0.0:0 : 0/2000 wildcard binds got a port held by a 127.0.0.1 socket; the probe reached that socket 0 times, the wildcard socket 0 times
macOS 15.8.1 x86_64 gives the same pattern, and a Go version of the test reproduces on 15.7.9 and 26.6.2 as well. FreeBSD 15.1 gives 0/2000 with a dual-stack socket.
From the public XNU source (xnu-12377.121.6), in6_pcbsetport checks candidate ports with in6_pcblookup_local, which skips PCBs without INP_IPV6, while in6_pcbbind does an IPv4 PCB lookup for dual-stack wildcard binds. That would explain why an explicit bind is refused but port 0 isn't.
This is the cause of the long-standing Go issue golang/go#67226, since Go's net.ListenUDP("udp", 0.0.0.0:0) creates exactly this socket, and of intermittent failures in QUIC client tests. Using an AF_INET socket for IPv4-only traffic avoids it, but there's no workaround for a socket that needs both families on one port.