mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-11 21:20:39 +02:00
depends: fix IPC listeners on macOS dying when accepting a dead socket
After `accept()`, kj unconditionally calls `setsockopt(TCP_NODELAY)` on the connection socket. For Unix sockets, kj handles the usual "not supported" error codes, however, on macOS EINVAL is returned when the client has already disconnected and kj only tolerates that code when built for FreeBSD. The unexpected error then goes uncaught in the accept loop, causing the listener to stop accepting new connections. See https://github.com/bitcoin-core/libmultiprocess/issues/319 for more details. Patch is the upstream commit https://github.com/capnproto/capnproto/commit/85d8e47f, the 1.x backport of https://github.com/capnproto/capnproto/commit/7df5bd078 (https://github.com/capnproto/capnproto/pull/2748).
This commit is contained in:
@@ -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
|
||||
|
||||
273
depends/patches/capnp/macos_accept_dead_socket.patch
Normal file
273
depends/patches/capnp/macos_accept_dead_socket.patch
Normal file
@@ -0,0 +1,273 @@
|
||||
From 85d8e47fcb156afc3d95ed9033ba470d6189cbff Mon Sep 17 00:00:00 2001
|
||||
From: Aaron O'Mullan <aaron.omullan@gmail.com>
|
||||
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<sockaddr*>(&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<NetworkAddress> 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<AsyncIoStream> 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<sockaddr*>(&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<NetworkAddress> 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<struct sockaddr*>(&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:
|
||||
Reference in New Issue
Block a user