Inside Bitcoin
Opening the selected file…
Inside Bitcoin
Opening the selected file…
The code behind the network, with room to understand it.
Release published 2026-10-28 · Snapshot fetched 2026-11-01. Latest stable release at fetch time; development branches and Git history are excluded. Only the selected code window and visible folders load here.
1From 85d8e47fcb156afc3d95ed9033ba470d6189cbff Mon Sep 17 00:00:00 20012From: Aaron O'Mullan <[email protected]>3Date: Mon, 18 Aug 2025 15:18:21 +01004Subject: [PATCH] Fix macOS crash when accept() returns zero-length address6On macOS, accept() can return a valid file descriptor with addrlen=0 when a7connection is aborted during the accept (e.g., during "happy eyeballs" dual-stack8connection attempts). This is a bug in XNU (the macOS kernel).10This commit:111. Adds special handling for zero-length address returns on macOS122. Adds platform-specific checks for socket connection errors133. Implements more robust error detection in socket accept logic144. Includes comprehensive tests for aborted socket connections16The changes improve socket connection handling, particularly on macOS, by gracefully17managing scenarios where connections are aborted before being fully accepted.19Based on PR #2365 by Aaron O'Mullan21(cherry picked from commit 7df5bd078f389ded313479981bd0ae06cbcdfe1b)23Adapted for 1.x: kj::OwnFd -> kj::AutoCloseFd, kj::none -> nullptr,24and 1.x read()/write() signatures in the new tests.25---26c++/src/kj/async-io-test.c++ | 198 +++++++++++++++++++++++++++++++++++27c++/src/kj/async-io-unix.c++ | 18 +++-282 files changed, 214 insertions(+), 2 deletions(-)30diff --git a/c++/src/kj/async-io-test.c++ b/c++/src/kj/async-io-test.c++31index bef5634b..6e9e113a 10064432--- a/c++/src/kj/async-io-test.c++33+++ b/c++/src/kj/async-io-test.c++34@@ -3426,5 +3426,203 @@ KJ_TEST("pump file to socket") {35doTest(fs->getCurrent().createTemporary());36}38+// ---------------------------------------------------------------------------------------------39+// accept() with aborted connection - IPv4 listener40+// ---------------------------------------------------------------------------------------------41+// Test accept() behavior when connections are aborted (send RST) before being accepted.42+// Creates an aborted IPv4 connection followed by a valid one, then verifies proper handling.43+//44+// On Unix platforms, accept() typically returns the aborted connection, which fails on first45+// read (throws "Connection reset by peer" or returns 0 bytes). The test then calls accept()46+// again to get the valid connection. Some platforms may filter out aborted connections.47+48+#if !_WIN3249+KJ_TEST("accept() with aborted connection - IPv4") {50+ // -- async-io boilerplate ---------------------------------------------------51+ auto io = kj::setupAsyncIo();52+ auto& net = io.provider->getNetwork();53+54+ auto listenerAddr = net.parseAddress("127.0.0.1", 0).wait(io.waitScope);55+ auto listener = listenerAddr->listen();56+ uint16_t port = listener->getPort();57+58+ // Create a connection that will be aborted (sends RST packet)59+ kj::Thread abortThread([&] {60+ int s = ::socket(AF_INET, SOCK_STREAM, 0);61+ KJ_ASSERT(s >= 0);62+63+ // Configure socket to send RST on close instead of FIN64+ struct linger lg = {1, 0};65+ KJ_SYSCALL(setsockopt(s, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)));66+67+ sockaddr_in a {};68+ a.sin_family = AF_INET;69+ a.sin_port = htons(port);70+ a.sin_addr.s_addr = htonl(0x7f000001);71+ KJ_SYSCALL(::connect(s, reinterpret_cast<sockaddr*>(&a), sizeof(a)));72+73+ // Close immediately to send RST packet74+ KJ_SYSCALL(::close(s));75+ });76+77+ // Allow aborted connection to reach accept queue first78+ io.provider->getTimer().afterDelay(2 * kj::MILLISECONDS)79+ .wait(io.waitScope);80+81+ // Create a valid connection that should work82+ auto clientP = net.parseAddress("127.0.0.1", port)83+ .then([](Own<NetworkAddress> addr) { return addr->connect(); });84+ // Accept first connection - this should be the aborted one85+ auto firstConnection = listener->accept().wait(io.waitScope);86+ KJ_ASSERT(firstConnection);87+88+ char buffer[16];89+ bool firstConnectionAborted = false;90+91+ // Test if first connection is aborted by attempting to read92+ if (kj::runCatchingExceptions([&]() {93+ auto readPromise = firstConnection->tryRead(buffer, 1, sizeof(buffer));94+ auto bytesRead = readPromise.wait(io.waitScope);95+ if (bytesRead == 0) {96+ firstConnectionAborted = true;97+ }98+ }) != nullptr) {99+ // Read failed with exception (connection was aborted)100+ firstConnectionAborted = true;101+ }102+103+ kj::Own<AsyncIoStream> serverCon;104+ if (firstConnectionAborted) {105+ // First connection was aborted as expected - accept the second (valid) connection106+ serverCon = listener->accept().wait(io.waitScope);107+ } else {108+ // Unexpected: first connection was valid (kernel filtered out aborted one)109+ KJ_LOG(WARNING, "Platform filtered out aborted connection - using first connection");110+ serverCon = kj::mv(firstConnection);111+ }112+ // Verify we have a working connection113+ auto clientCon = clientP.wait(io.waitScope);114+ KJ_ASSERT(serverCon);115+116+ // Test data transfer on the valid connection117+ auto writePromise = clientCon->write("hello", 5);118+ auto readPromise2 = serverCon->read(buffer, 5, sizeof(buffer));119+120+ writePromise.wait(io.waitScope);121+ auto amount = readPromise2.wait(io.waitScope);122+ KJ_ASSERT(amount == 5);123+ KJ_ASSERT(memcmp(buffer, "hello", 5) == 0);124+125+ abortThread.detach();126+}127+#endif // !_WIN32128+129+// ---------------------------------------------------------------------------------------------130+// accept() with aborted connection - dual-stack IPv4/IPv6 listener131+// ---------------------------------------------------------------------------------------------132+// Test accept() behavior with aborted cross-protocol connections on dual-stack listeners.133+// Creates an aborted IPv4 connection to an IPv6 listener, followed by a valid IPv6 connection.134+//135+// Expected behavior:136+// - Darwin/macOS: When IPv4 connects to IPv6 listener and gets aborted, accept() returns137+// a socket with addrlen=0. KJ's accept loop detects this Darwin quirk and discards the138+// socket automatically, so first accept() returns the valid connection.139+// - Linux/other Unix: accept() returns the aborted connection, which fails on first140+// read (throws exception or returns 0 bytes). Test then calls accept() again for the141+// valid connection.142+//143+// This test specifically exercises the Darwin addrlen==0 bug workaround in KJ's accept loop.144+145+#if !_WIN32146+KJ_TEST("accept() with aborted connection - dual-stack IPv4/IPv6") {147+ if (!systemSupportsAddress("::")) {148+ KJ_LOG(WARNING, "system does not support ipv6; skipping test");149+ return;150+ }151+ char buffer[16];152+ // -- async-io boilerplate ---------------------------------------------------153+ auto io = kj::setupAsyncIo();154+ auto& net = io.provider->getNetwork();155+156+ auto listenerAddr = net.parseAddress("::", 0).wait(io.waitScope);157+ auto listener = listenerAddr->listen();158+ uint16_t port = listener->getPort();159+160+ // Create IPv4 connection that will be aborted (sends RST packet)161+ kj::Thread abortThread([&] {162+ int s = ::socket(AF_INET, SOCK_STREAM, 0);163+ KJ_ASSERT(s >= 0);164+165+ // Configure socket to send RST on close instead of FIN166+ struct linger lg = {1, 0};167+ KJ_SYSCALL(setsockopt(s, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)));168+169+ sockaddr_in a {};170+ a.sin_family = AF_INET;171+ a.sin_port = htons(port);172+ a.sin_addr.s_addr = htonl(0x7f000001);173+ KJ_SYSCALL(::connect(s, reinterpret_cast<sockaddr*>(&a), sizeof(a)));174+175+ // Close immediately to send RST packet176+ KJ_SYSCALL(::close(s));177+ });178+179+ // Allow aborted connection to reach accept queue first180+ io.provider->getTimer().afterDelay(2 * kj::MILLISECONDS)181+ .wait(io.waitScope);182+183+ // Create valid IPv6 connection184+ auto clientP = net.parseAddress("::1", port)185+ .then([](Own<NetworkAddress> addr) { return addr->connect(); });186+ // Accept connection - behavior differs by platform187+ auto serverCon = listener->accept().wait(io.waitScope);188+ KJ_ASSERT(serverCon);189+190+#if __APPLE__191+ // On Apple platforms: IPv4->IPv6 aborted connections return addrlen=0,192+ // which KJ detects and discards, so first accept() returns the valid connection193+ auto clientCon = clientP.wait(io.waitScope);194+ KJ_ASSERT(clientCon);195+#else196+ // On other platforms: aborted connection is returned by accept(),197+ // need to test if it's valid by attempting to read198+ bool connectionAborted = false;199+200+ if (kj::runCatchingExceptions([&]() {201+ auto readPromise = serverCon->tryRead(buffer, 1, sizeof(buffer));202+ auto bytesRead = readPromise.wait(io.waitScope);203+ if (bytesRead == 0) {204+ connectionAborted = true;205+ }206+ }) != nullptr) {207+ // Read failed with exception (connection was aborted)208+ connectionAborted = true;209+ }210+211+ if (connectionAborted) {212+ // First connection was aborted as expected - accept the second (valid) connection213+ serverCon = listener->accept().wait(io.waitScope);214+ } else {215+ // Unexpected: first connection was valid (kernel filtered out aborted one)216+ KJ_LOG(WARNING, "Platform filtered out aborted connection - using first connection");217+ }218+219+ auto clientCon = clientP.wait(io.waitScope);220+ KJ_ASSERT(clientCon);221+#endif222+223+ // Test data transfer on the valid connection224+ auto writePromise = clientCon->write("hello", 5);225+ auto readPromise = serverCon->read(buffer, 5, sizeof(buffer));226+227+ writePromise.wait(io.waitScope);228+ auto amount = readPromise.wait(io.waitScope);229+ KJ_ASSERT(amount == 5);230+ KJ_ASSERT(memcmp(buffer, "hello", 5) == 0);231+232+ abortThread.detach();233+}234+#endif // !_WIN32235+236} // namespace237} // namespace kj238diff --git a/c++/src/kj/async-io-unix.c++ b/c++/src/kj/async-io-unix.c++239index 7e1c85fc..a254c2cc 100644240--- a/c++/src/kj/async-io-unix.c++241+++ b/c++/src/kj/async-io-unix.c++242@@ -1343,6 +1343,18 @@ public:244if (newFd >= 0) {245kj::AutoCloseFd ownFd(newFd);246+247+ if (addrlen == 0) {248+#if __APPLE__249+ // A bug in XNU (the macOS kernel) can cause accept() to return a socket but addrlen=0250+ // The socket is already dead and should be discarded
Independent explanations by MyBitcoinExplorer. Not affiliated with or endorsed by Bitcoin Core. Guides are educational and do not replace the source or its tests.
Bitcoin Core source © its contributors, distributed under the MIT license. Original notices and bundled third-party licenses are retained in the source tree.