Bug #22248
open`Errno::NOERROR` from `connect(2)` once a non-main Ractor has more than one Ruby thread
Description
Summary¶
On x86_64-linux, socket IO in a non-main Ractor fails spuriously as soon as that Ractor contains a second Ruby thread. The connecting thread raises Errno::NOERROR — errno 0 surfaced as a SystemCallError, whose message is literally Success:
Errno::NOERROR: Failed to open TCP connection to 127.0.0.1:38975
(Success - connect(2) for "127.0.0.1" port 38975)
With a raw TCPSocket instead of Net::HTTP the VM can abort outright:
What the second thread does doesn't seem to matter. A thread that only calls sleep, one that only burns CPU, one doing pipe IO, and one doing socket IO all trigger it. Nothing else I varied matters: a Ractor with a single thread never fails, no matter how much socket work it does or how loaded the machine is, and a second thread in the main Ractor never causes it, but a second thread in a non-main Ractor does.
This bug made my CI jobs flaky in roughly 10% of builds, where the "second thread" was nothing more exotic than a stub HTTP server standing in for an upstream service alongside the client under test.
Reproduction¶
See this gist: https://gist.github.com/airhorns/dc676921e02272cc41f8aa15da040e45
Note that the interleaving is important to more reliably trigger the bug. On a shared machine the failure rate swings enormously with host load: run in blocks, the same arrangement scored 196/200 in one build and 0/200 in the next, so blocked counts are not comparable to each other and a 0 says nothing.
Architecture¶
I have only reproduced this on x86_64-linux. On aarch64-linux — official ruby:4.0, byte-identical revision 03b6d3f889 — roughly 1500 exchanges of the failing arrangements are clean, including 300 of the arrangement that fails 89/200 on x86, under 1- and 2-CPU quotas, with competing busy loops, and with 8 neighbour threads. arm64-darwin is clean too. So expect to need an x86_64 Linux host, ideally a loaded one.
Environment¶
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
Socket.tcp_fast_fallback = true (default; false reproduces identically)
RUBY_MAX_CPU unset (=1 does not help)
shared CI host, nproc 16, 1-minute load average 3.1-5.9 during the run
Relation to Bug #21195¶
Bug #21195 is the same shape — errno lost around io_internal_wait, fixed for 3.3 and 3.4 in 2025 — and the errno == 0 assertion text is identical. This is a live path in 4.0.6, reached through ordinary socket connect and read, with or without Happy Eyeballs, so I am filing it separately rather than commenting there. I could not find an existing report for the Ractor variant.