diff --git a/depends/packages/capnp.mk b/depends/packages/capnp.mk index 542bf126297..4cdc14cb1d7 100644 --- a/depends/packages/capnp.mk +++ b/depends/packages/capnp.mk @@ -4,6 +4,7 @@ $(package)_download_path=$(native_$(package)_download_path) $(package)_download_file=$(native_$(package)_download_file) $(package)_file_name=$(native_$(package)_file_name) $(package)_sha256_hash=$(native_$(package)_sha256_hash) +$(package)_patches=macos_accept_dead_socket.patch define $(package)_set_vars $(package)_config_opts := -DBUILD_TESTING=OFF @@ -12,6 +13,10 @@ define $(package)_set_vars $(package)_cxxflags += -fdebug-prefix-map=$($(package)_extract_dir)=/usr -fmacro-prefix-map=$($(package)_extract_dir)=/usr endef +define $(package)_preprocess_cmds + patch -p2 < $($(package)_patch_dir)/macos_accept_dead_socket.patch +endef + define $(package)_config_cmds $($(package)_cmake) . endef diff --git a/depends/patches/capnp/macos_accept_dead_socket.patch b/depends/patches/capnp/macos_accept_dead_socket.patch new file mode 100644 index 00000000000..b23982f9dfb --- /dev/null +++ b/depends/patches/capnp/macos_accept_dead_socket.patch @@ -0,0 +1,273 @@ +From 85d8e47fcb156afc3d95ed9033ba470d6189cbff Mon Sep 17 00:00:00 2001 +From: Aaron O'Mullan +Date: Mon, 18 Aug 2025 15:18:21 +0100 +Subject: [PATCH] Fix macOS crash when accept() returns zero-length address + +On macOS, accept() can return a valid file descriptor with addrlen=0 when a +connection is aborted during the accept (e.g., during "happy eyeballs" dual-stack +connection attempts). This is a bug in XNU (the macOS kernel). + +This commit: +1. Adds special handling for zero-length address returns on macOS +2. Adds platform-specific checks for socket connection errors +3. Implements more robust error detection in socket accept logic +4. Includes comprehensive tests for aborted socket connections + +The changes improve socket connection handling, particularly on macOS, by gracefully +managing scenarios where connections are aborted before being fully accepted. + +Based on PR #2365 by Aaron O'Mullan + +(cherry picked from commit 7df5bd078f389ded313479981bd0ae06cbcdfe1b) + +Adapted for 1.x: kj::OwnFd -> kj::AutoCloseFd, kj::none -> nullptr, +and 1.x read()/write() signatures in the new tests. +--- + c++/src/kj/async-io-test.c++ | 198 +++++++++++++++++++++++++++++++++++ + c++/src/kj/async-io-unix.c++ | 18 +++- + 2 files changed, 214 insertions(+), 2 deletions(-) + +diff --git a/c++/src/kj/async-io-test.c++ b/c++/src/kj/async-io-test.c++ +index bef5634b..6e9e113a 100644 +--- a/c++/src/kj/async-io-test.c++ ++++ b/c++/src/kj/async-io-test.c++ +@@ -3426,5 +3426,203 @@ KJ_TEST("pump file to socket") { + doTest(fs->getCurrent().createTemporary()); + } + ++// --------------------------------------------------------------------------------------------- ++// accept() with aborted connection - IPv4 listener ++// --------------------------------------------------------------------------------------------- ++// Test accept() behavior when connections are aborted (send RST) before being accepted. ++// Creates an aborted IPv4 connection followed by a valid one, then verifies proper handling. ++// ++// On Unix platforms, accept() typically returns the aborted connection, which fails on first ++// read (throws "Connection reset by peer" or returns 0 bytes). The test then calls accept() ++// again to get the valid connection. Some platforms may filter out aborted connections. ++ ++#if !_WIN32 ++KJ_TEST("accept() with aborted connection - IPv4") { ++ // -- async-io boilerplate --------------------------------------------------- ++ auto io = kj::setupAsyncIo(); ++ auto& net = io.provider->getNetwork(); ++ ++ auto listenerAddr = net.parseAddress("127.0.0.1", 0).wait(io.waitScope); ++ auto listener = listenerAddr->listen(); ++ uint16_t port = listener->getPort(); ++ ++ // Create a connection that will be aborted (sends RST packet) ++ kj::Thread abortThread([&] { ++ int s = ::socket(AF_INET, SOCK_STREAM, 0); ++ KJ_ASSERT(s >= 0); ++ ++ // Configure socket to send RST on close instead of FIN ++ struct linger lg = {1, 0}; ++ KJ_SYSCALL(setsockopt(s, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg))); ++ ++ sockaddr_in a {}; ++ a.sin_family = AF_INET; ++ a.sin_port = htons(port); ++ a.sin_addr.s_addr = htonl(0x7f000001); ++ KJ_SYSCALL(::connect(s, reinterpret_cast(&a), sizeof(a))); ++ ++ // Close immediately to send RST packet ++ KJ_SYSCALL(::close(s)); ++ }); ++ ++ // Allow aborted connection to reach accept queue first ++ io.provider->getTimer().afterDelay(2 * kj::MILLISECONDS) ++ .wait(io.waitScope); ++ ++ // Create a valid connection that should work ++ auto clientP = net.parseAddress("127.0.0.1", port) ++ .then([](Own addr) { return addr->connect(); }); ++ // Accept first connection - this should be the aborted one ++ auto firstConnection = listener->accept().wait(io.waitScope); ++ KJ_ASSERT(firstConnection); ++ ++ char buffer[16]; ++ bool firstConnectionAborted = false; ++ ++ // Test if first connection is aborted by attempting to read ++ if (kj::runCatchingExceptions([&]() { ++ auto readPromise = firstConnection->tryRead(buffer, 1, sizeof(buffer)); ++ auto bytesRead = readPromise.wait(io.waitScope); ++ if (bytesRead == 0) { ++ firstConnectionAborted = true; ++ } ++ }) != nullptr) { ++ // Read failed with exception (connection was aborted) ++ firstConnectionAborted = true; ++ } ++ ++ kj::Own serverCon; ++ if (firstConnectionAborted) { ++ // First connection was aborted as expected - accept the second (valid) connection ++ serverCon = listener->accept().wait(io.waitScope); ++ } else { ++ // Unexpected: first connection was valid (kernel filtered out aborted one) ++ KJ_LOG(WARNING, "Platform filtered out aborted connection - using first connection"); ++ serverCon = kj::mv(firstConnection); ++ } ++ // Verify we have a working connection ++ auto clientCon = clientP.wait(io.waitScope); ++ KJ_ASSERT(serverCon); ++ ++ // Test data transfer on the valid connection ++ auto writePromise = clientCon->write("hello", 5); ++ auto readPromise2 = serverCon->read(buffer, 5, sizeof(buffer)); ++ ++ writePromise.wait(io.waitScope); ++ auto amount = readPromise2.wait(io.waitScope); ++ KJ_ASSERT(amount == 5); ++ KJ_ASSERT(memcmp(buffer, "hello", 5) == 0); ++ ++ abortThread.detach(); ++} ++#endif // !_WIN32 ++ ++// --------------------------------------------------------------------------------------------- ++// accept() with aborted connection - dual-stack IPv4/IPv6 listener ++// --------------------------------------------------------------------------------------------- ++// Test accept() behavior with aborted cross-protocol connections on dual-stack listeners. ++// Creates an aborted IPv4 connection to an IPv6 listener, followed by a valid IPv6 connection. ++// ++// Expected behavior: ++// - Darwin/macOS: When IPv4 connects to IPv6 listener and gets aborted, accept() returns ++// a socket with addrlen=0. KJ's accept loop detects this Darwin quirk and discards the ++// socket automatically, so first accept() returns the valid connection. ++// - Linux/other Unix: accept() returns the aborted connection, which fails on first ++// read (throws exception or returns 0 bytes). Test then calls accept() again for the ++// valid connection. ++// ++// This test specifically exercises the Darwin addrlen==0 bug workaround in KJ's accept loop. ++ ++#if !_WIN32 ++KJ_TEST("accept() with aborted connection - dual-stack IPv4/IPv6") { ++ if (!systemSupportsAddress("::")) { ++ KJ_LOG(WARNING, "system does not support ipv6; skipping test"); ++ return; ++ } ++ char buffer[16]; ++ // -- async-io boilerplate --------------------------------------------------- ++ auto io = kj::setupAsyncIo(); ++ auto& net = io.provider->getNetwork(); ++ ++ auto listenerAddr = net.parseAddress("::", 0).wait(io.waitScope); ++ auto listener = listenerAddr->listen(); ++ uint16_t port = listener->getPort(); ++ ++ // Create IPv4 connection that will be aborted (sends RST packet) ++ kj::Thread abortThread([&] { ++ int s = ::socket(AF_INET, SOCK_STREAM, 0); ++ KJ_ASSERT(s >= 0); ++ ++ // Configure socket to send RST on close instead of FIN ++ struct linger lg = {1, 0}; ++ KJ_SYSCALL(setsockopt(s, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg))); ++ ++ sockaddr_in a {}; ++ a.sin_family = AF_INET; ++ a.sin_port = htons(port); ++ a.sin_addr.s_addr = htonl(0x7f000001); ++ KJ_SYSCALL(::connect(s, reinterpret_cast(&a), sizeof(a))); ++ ++ // Close immediately to send RST packet ++ KJ_SYSCALL(::close(s)); ++ }); ++ ++ // Allow aborted connection to reach accept queue first ++ io.provider->getTimer().afterDelay(2 * kj::MILLISECONDS) ++ .wait(io.waitScope); ++ ++ // Create valid IPv6 connection ++ auto clientP = net.parseAddress("::1", port) ++ .then([](Own addr) { return addr->connect(); }); ++ // Accept connection - behavior differs by platform ++ auto serverCon = listener->accept().wait(io.waitScope); ++ KJ_ASSERT(serverCon); ++ ++#if __APPLE__ ++ // On Apple platforms: IPv4->IPv6 aborted connections return addrlen=0, ++ // which KJ detects and discards, so first accept() returns the valid connection ++ auto clientCon = clientP.wait(io.waitScope); ++ KJ_ASSERT(clientCon); ++#else ++ // On other platforms: aborted connection is returned by accept(), ++ // need to test if it's valid by attempting to read ++ bool connectionAborted = false; ++ ++ if (kj::runCatchingExceptions([&]() { ++ auto readPromise = serverCon->tryRead(buffer, 1, sizeof(buffer)); ++ auto bytesRead = readPromise.wait(io.waitScope); ++ if (bytesRead == 0) { ++ connectionAborted = true; ++ } ++ }) != nullptr) { ++ // Read failed with exception (connection was aborted) ++ connectionAborted = true; ++ } ++ ++ if (connectionAborted) { ++ // First connection was aborted as expected - accept the second (valid) connection ++ serverCon = listener->accept().wait(io.waitScope); ++ } else { ++ // Unexpected: first connection was valid (kernel filtered out aborted one) ++ KJ_LOG(WARNING, "Platform filtered out aborted connection - using first connection"); ++ } ++ ++ auto clientCon = clientP.wait(io.waitScope); ++ KJ_ASSERT(clientCon); ++#endif ++ ++ // Test data transfer on the valid connection ++ auto writePromise = clientCon->write("hello", 5); ++ auto readPromise = serverCon->read(buffer, 5, sizeof(buffer)); ++ ++ writePromise.wait(io.waitScope); ++ auto amount = readPromise.wait(io.waitScope); ++ KJ_ASSERT(amount == 5); ++ KJ_ASSERT(memcmp(buffer, "hello", 5) == 0); ++ ++ abortThread.detach(); ++} ++#endif // !_WIN32 ++ + } // namespace + } // namespace kj +diff --git a/c++/src/kj/async-io-unix.c++ b/c++/src/kj/async-io-unix.c++ +index 7e1c85fc..a254c2cc 100644 +--- a/c++/src/kj/async-io-unix.c++ ++++ b/c++/src/kj/async-io-unix.c++ +@@ -1343,6 +1343,18 @@ public: + + if (newFd >= 0) { + kj::AutoCloseFd ownFd(newFd); ++ ++ if (addrlen == 0) { ++#if __APPLE__ ++ // A bug in XNU (the macOS kernel) can cause accept() to return a socket but addrlen=0 ++ // The socket is already dead and should be discarded ++ // https://github.com/apple-oss-distributions/xnu/blob/e3723e1f17661b24996789d8afc084c0c3303b26/bsd/kern/uipc_syscalls.c#L663-L691 ++#else ++ KJ_LOG(ERROR, "accept() returned zero-size address?"); ++#endif ++ return acceptImpl(authenticated); ++ } ++ + if (!filter.shouldAllow(reinterpret_cast(&addr), addrlen)) { + // Ignore disallowed address. + return acceptImpl(authenticated); +@@ -1357,8 +1369,10 @@ public: + ownFd.get(), IPPROTO_TCP, TCP_NODELAY, (char*)&one, sizeof(one))) { + case EOPNOTSUPP: + case ENOPROTOOPT: // (returned for AF_UNIX in cygwin) +-#if __FreeBSD__ +- case EINVAL: // (returned for AF_UNIX in FreeBSD) ++#if __APPLE__ || __FreeBSD__ ++ case EINVAL: ++ // On FreeBSD, EINVAL is returned for AF_UNIX sockets. ++ // On macOS, EINVAL may be returned for sockets that are already dead (due to a race with RST). + #endif + break; + default: