Bug #21790
open`Socket.getaddrinfo` hangs after `fork()` on macOS 26.1 (Tahoe) for IPv4-only hosts
Description
Ruby's Socket.getaddrinfo hangs indefinitely in forked child processes on macOS 26.1 (Tahoe) when resolving IPv4-only hostnames. This is a regression that does not occur on macOS 15.x (Sonoma) or earlier.
Ruby version:
ruby 3.3.8 (2025-04-09 revision b200bad6cd) [arm64-darwin24]
Also confirmed this affects Ruby 3.2.6 and 3.4.1.
Reproducible script:
require "socket"
require "timeout"
puts "Ruby #{RUBY_VERSION} on #{RUBY_PLATFORM}"
Socket.getaddrinfo("api.segment.io", 443, nil, :STREAM)
puts "Parent: DNS completed"
pid = fork do
puts "Child: Attempting DNS resolution..."
begin
Timeout.timeout(90) do
Socket.getaddrinfo("api.segment.io", 443, nil, :STREAM)
end
puts "Child: SUCCESS"
exit 0
rescue Timeout::Error
puts "Child: FAILED - hung for 90 seconds"
exit 1
end
end
Process.wait(pid)
Note: Remove the Timeout.timeout(90) wrapper to observe the hang indefinitely. The timeout is included only to allow the script to exit for testing purposes.
Result of reproduce process:
Ruby 3.3.8 on arm64-darwin24
Parent: DNS completed
Child: Attempting DNS resolution...
Child: FAILED - hung for 90 seconds
The child process hangs with one thread consuming 100% CPU.
Expected result: The child process should complete DNS resolution successfully, as it does on macOS 15.x and earlier.
Analysis:
Stack trace shows:
Main thread: Blocked in wait_getaddrinfo → _pthread_cond_wait
DNS thread: Spinning in _gai_nat64_second_pass → nw_path_access_agent_cache → _os_log_preferences_refresh → SIGSEGV
The crash occurs in macOS's NAT64 synthesis code path. Ruby's signal handler catches the SIGSEGV but cannot recover, causing the DNS thread to spin.
Key observations:
- Only affects IPv4-only hosts. Hosts with IPv6 (like google.com) work correctly.
- Using
AF_INETinstead ofAF_UNSPECworks.Socket.getaddrinfo("api.segment.io", 443, Socket::AF_INET, :STREAM)succeeds. - Python is not affected. Python calls
getaddrinfo()synchronously without a background thread. - Parent must do DNS before fork. If the parent has not called getaddrinfo(), the child works correctly.
Workaround:
- Use
resolv-replaceto bypass the native DNS resolver:require "resolv-replace"
Impact:
This breaks all Ruby applications using pre-forking worker models (Resque, Unicorn, Puma, Sidekiq, Passenger) on macOS Tahoe.
Apple Bug Report:
Filed with Apple as Feedback Assistant #FB21364061
Files
No data to display