From ffa3f90181da40812d617c21ca48440e041c10c7 Mon Sep 17 00:00:00 2001 From: xalestar Date: Sat, 10 Oct 2026 14:03:52 +0800 Subject: [PATCH 1/2] Stop a listen backlog of 1 losing SIGURG On macOS 27.0.1 (xnu-13432.1.9) a TCP socket accepted from a listener with backlog 1 loses the urgent mark of the first urgent byte on about half of the connections. The connection receives the byte, but SIOCATMARK stays 0, recv(MSG_OOB) fails with EINVAL, and EVFILT_EXCEPT with NOTE_OOB does not fire, so the watcher in asyncio.c has no event to turn into SIGURG. A program with plain blocking sockets and no elfuse loses the mark on 94 to 111 of 200 connections at backlog 1, and on none at backlog 0, 2 or 128. sys_listen passes 2 to the host when the guest asks for 1. Linux lets backlog + 1 connections wait (sk_acceptq_is_full) and macOS lets exactly backlog connections wait, measured for backlog 1 to 4, so host backlog 2 holds the 2 connections Linux holds for listen(fd, 1). Verified: the SIGURG case of tests/test-sigio.c failed 19 of 40 runs without the change and 0 of 200 with it. Fixes #402 --- src/syscall/net.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/syscall/net.c b/src/syscall/net.c index fd91d4c1e..b29189f88 100644 --- a/src/syscall/net.c +++ b/src/syscall/net.c @@ -581,7 +581,15 @@ int64_t sys_listen(int fd, int backlog) if (ref_err < 0) return ref_err; - if (listen(host_ref.fd, backlog) < 0) { + /* macOS 27.0 loses the urgent mark of the first urgent byte on about half + * of the connections accepted from a listener with backlog 1, so the guest + * gets neither SIGURG nor recv(MSG_OOB). Backlog 2 does not show it. Linux + * queues backlog + 1 connections (sk_acceptq_is_full) where macOS queues + * backlog, so 2 is also the queue length the guest asked for. + */ + int host_backlog = backlog == 1 ? 2 : backlog; + + if (listen(host_ref.fd, host_backlog) < 0) { host_fd_ref_close(&host_ref); return linux_errno(); } From 1f569c61d48479909a2c964598b67008b93afbae Mon Sep 17 00:00:00 2001 From: xalestar Date: Thu, 15 Oct 2026 02:34:04 +0800 Subject: [PATCH 2/2] Queue backlog + 1 connections on every listener Linux lets backlog + 1 connections wait on a listener (sk_acceptq_is_full, unix_recvq_full) and macOS lets exactly backlog wait, so a guest listen(fd, 4) queued one connection fewer than on Linux, and listen(fd, 0) queued 128 instead of 1, since macOS reads 0 as somaxconn. Measured on the host, for TCP and AF_UNIX alike: backlog 1 to 128 queues exactly that many, and 0, negative, or above 128 queues 128. sys_listen now passes backlog + 1 for every backlog from 0 up, and leaves a negative one to the host, which reads it as somaxconn the way Linux does. Host backlog 1 stays avoided on AF_INET and AF_INET6 only, for the urgent-mark loss in the previous commit, so a TCP listen(fd, 0) queues 2 where Linux queues 1. AF_UNIX gets the exact depth. tests/test-listen-backlog.c connects without accepting and counts what completes. The AF_UNIX cases check the exact depth. The TCP cases check a lower bound, because macOS TCP sometimes completes one or two connections past its queue: 1 run in 30 with plain host sockets. --- src/syscall/net.c | 24 ++++-- tests/test-listen-backlog.c | 145 ++++++++++++++++++++++++++++++++++++ tests/test-matrix.sh | 2 + 3 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 tests/test-listen-backlog.c diff --git a/src/syscall/net.c b/src/syscall/net.c index b29189f88..47fb21de4 100644 --- a/src/syscall/net.c +++ b/src/syscall/net.c @@ -13,6 +13,7 @@ * - Socket options: SOL_SOCKET option constants differ */ +#include #include #include #include @@ -581,13 +582,26 @@ int64_t sys_listen(int fd, int backlog) if (ref_err < 0) return ref_err; + /* Linux lets backlog + 1 connections wait (sk_acceptq_is_full, + * unix_recvq_full) where macOS lets backlog wait and reads 0 as somaxconn. + * Both read a negative backlog as somaxconn, and macOS caps the rest there. + */ + int host_backlog = backlog; + if (backlog >= 0 && backlog < INT_MAX) + host_backlog = backlog + 1; + /* macOS 27.0 loses the urgent mark of the first urgent byte on about half - * of the connections accepted from a listener with backlog 1, so the guest - * gets neither SIGURG nor recv(MSG_OOB). Backlog 2 does not show it. Linux - * queues backlog + 1 connections (sk_acceptq_is_full) where macOS queues - * backlog, so 2 is also the queue length the guest asked for. + * of the TCP connections accepted from a listener with backlog 1, so the + * guest gets neither SIGURG nor recv(MSG_OOB). A TCP listen(fd, 0) queues 2 + * instead of the 1 Linux queues. */ - int host_backlog = backlog == 1 ? 2 : backlog; + if (host_backlog == 1) { + struct sockaddr_storage ss; + socklen_t len = sizeof(ss); + if (getsockname(host_ref.fd, (struct sockaddr *) &ss, &len) == 0 && + (ss.ss_family == AF_INET || ss.ss_family == AF_INET6)) + host_backlog = 2; + } if (listen(host_ref.fd, host_backlog) < 0) { host_fd_ref_close(&host_ref); diff --git a/tests/test-listen-backlog.c b/tests/test-listen-backlog.c new file mode 100644 index 000000000..0fe073ced --- /dev/null +++ b/tests/test-listen-backlog.c @@ -0,0 +1,145 @@ +/* + * Accept-queue depth of listen + * + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Linux lets backlog + 1 connections wait on a listener (sk_acceptq_is_full in + * include/net/sock.h, unix_recvq_full in net/unix/af_unix.c); macOS lets + * exactly backlog wait. Each case connects without accepting and counts the + * connections that complete before one is dropped or refused. + * + * macOS TCP sometimes completes one or two connections past its queue (1 run in + * 30 with plain host sockets), so the TCP cases check only that backlog + 1 + * fit. TCP backlog 0 is left out: elfuse queues 2 there, because host backlog 1 + * loses the urgent mark (src/syscall/net.c, sys_listen). + * + * Passes on real Linux. + * + * Syscalls exercised: socket(198), bind(200), listen(201), connect(203), + * getsockname(204), ppoll(73), getsockopt(209), close(57), + * mkdirat(34), unlinkat(35) + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +int passes = 0, fails = 0; + +/* A dropped TCP SYN shows as a connect that never completes, so this bounds the + * wait for the first connection past the queue. + */ +#define CONNECT_WAIT_MS 500 + +#define MAX_CONNS 16 + +static char sock_dir[] = "/tmp/elfuse-listen-backlog-XXXXXX"; + +/* Return the number of connections that complete against listen(@backlog) with + * none accepted, trying at most @max; -1 when the listener cannot be set up. + */ +static int queue_depth(int family, int backlog, int max) +{ + struct sockaddr_storage ss; + socklen_t len; + int cli[MAX_CONNS]; + int opened = 0, done = 0; + + memset(&ss, 0, sizeof(ss)); + if (family == AF_INET) { + struct sockaddr_in *in = (struct sockaddr_in *) &ss; + in->sin_family = AF_INET; + in->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + len = sizeof(*in); + } else { + struct sockaddr_un *un = (struct sockaddr_un *) &ss; + un->sun_family = AF_UNIX; + snprintf(un->sun_path, sizeof(un->sun_path), "%s/s", sock_dir); + len = sizeof(*un); + } + + int lst = socket(family, SOCK_STREAM, 0); + if (lst < 0) + return -1; + if (bind(lst, (struct sockaddr *) &ss, len) < 0 || + listen(lst, backlog) < 0 || + getsockname(lst, (struct sockaddr *) &ss, &len) < 0) { + close(lst); + return -1; + } + + if (max > MAX_CONNS) + max = MAX_CONNS; + while (opened < max) { + int fd = socket(family, SOCK_STREAM | SOCK_NONBLOCK, 0); + if (fd < 0) + break; + cli[opened++] = fd; + if (connect(fd, (struct sockaddr *) &ss, len) < 0) { + if (errno != EINPROGRESS) + break; + struct pollfd p = {.fd = fd, .events = POLLOUT}; + int err = 0; + socklen_t elen = sizeof(err); + if (poll(&p, 1, CONNECT_WAIT_MS) != 1 || + getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &elen) < 0 || err) + break; + } + done++; + } + + for (int i = 0; i < opened; i++) + close(cli[i]); + close(lst); + if (family == AF_UNIX) + unlink(((struct sockaddr_un *) &ss)->sun_path); + return done; +} + +static void expect_depth(const char *name, int family, int backlog) +{ + TEST(name); + int depth = queue_depth(family, backlog, backlog + 3); + bool ok = family == AF_INET ? depth >= backlog + 1 : depth == backlog + 1; + if (ok) { + PASS(); + return; + } + char msg[64]; + snprintf(msg, sizeof(msg), "backlog %d queued %d, want %s%d", backlog, + depth, family == AF_INET ? ">= " : "", backlog + 1); + FAIL(msg); +} + +int main(void) +{ + printf("test-listen-backlog\n"); + + if (!mkdtemp(sock_dir)) { + FAIL("mkdtemp failed"); + SUMMARY("test-listen-backlog"); + return 1; + } + + expect_depth("TCP backlog 1 queues >= 2", AF_INET, 1); + expect_depth("TCP backlog 4 queues >= 5", AF_INET, 4); + expect_depth("AF_UNIX backlog 0 queues 1", AF_UNIX, 0); + expect_depth("AF_UNIX backlog 1 queues 2", AF_UNIX, 1); + expect_depth("AF_UNIX backlog 4 queues 5", AF_UNIX, 4); + + rmdir(sock_dir); + SUMMARY("test-listen-backlog"); + return fails ? 1 : 0; +} diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 0aa6894f1..21366c87c 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -817,6 +817,8 @@ run_unit_tests() printf "\nNetwork\n" test_check "$runner" "test-net" "0 failed" "$bindir/test-net" + test_check "$runner" "test-listen-backlog" "0 failed" \ + "$bindir/test-listen-backlog" test_rc "$runner" "test-netstat" 0 "$bindir/test-netstat" test_rc "$runner" "test-netlink" 0 "$bindir/test-netlink" test_rc "$runner" "test-uevent-socket" 0 "$bindir/test-uevent-socket"