Issue #21790 has been updated by adamoffat (Adam Moffat). Update: Apple has formally declined to fix this (still present in macOS 27 beta). I'm the reporter of the underlying Apple Feedback (FB21364061). Posting a status update since this issue is the canonical landing spot for anyone hitting it. Apple's position (via DTS): I raised this on the Apple Developer Forums and got a definitive answer from Apple DTS (https://developer.apple.com/forums/thread/834537). Summary: calling `getaddrinfo` in a child after fork without exec is officially unsupported, so they consider this a compatibility issue rather than a bug they'll fix, and they confirmed it is still not fixed in the macOS 27 beta. So this is permanent on macOS 26+; it will not be resolved at the OS level. Confirmed root cause (and it is not Ruby-specific): The trigger is os_log state that is initialized in the parent and inherited invalid across fork. The parent initializes it either explicitly, or implicitly via its own first getaddrinfo of an IPv4-only host. In the child, an AF_UNSPEC lookup of an IPv4-only host enters the NAT64 synthesis path and dereferences that stale state: ``` _os_log_preferences_refresh (libsystem_trace.dylib) <- faults, EXC_BAD_ACCESS os_log_type_enabled (libsystem_trace.dylib) nw_path_access_agent_cache (Network) nw_path_evaluator_evaluate / nw_path_snapshot_path nw_nat64_v4_address_requires_synthesis _gai_nat64_second_pass (libsystem_info.dylib) si_addrinfo -> getaddrinfo ``` Minimal reproduction in plain C (no Ruby), which crashes with the identical stack, proving the bug is in the OS and affects any runtime that initializes os_log before forking: ``` #include <netdb.h> #include <os/log.h> #include <unistd.h> int main(void) { os_log_t log = os_log_create("com.example.repro", "repro"); os_log(log, "init"); struct addrinfo hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM }, *res; getaddrinfo("api.stripe.com", "443", &hints, &res); // parent, IPv4-only host if (fork() == 0) { getaddrinfo("api.stripe.com", "443", &hints, &res); // child: crashes } } ``` Python is affected identically (crashes with signal 11). Verified across macOS 26.1 through 26.5.1 and 27 beta; not reproducible on macOS 15.x. Boundaries: only AF_UNSPEC lookups of IPv4-only hostnames are affected. IPv6-capable hosts, an AF_INET hint, numeric literals, and localhost are all immune. Workarounds (for anyone landing here): - require "resolv-replace" before any DNS. Bypasses the system resolver entirely with the pure-Ruby Resolv. Caveat: Resolv only reads /etc/resolv.conf, so it ignores macOS scoped resolvers, which can break VPN split-DNS / internal hostnames. - Set OS_ACTIVITY_MODE=disable in the worker's environment. This suppresses the crash (the os_log_type_enabled check short-circuits before the faulting preferences refresh) while keeping the native resolver, so scoped/VPN DNS still works. Lighter than monkeypatching sockets. Tradeoff: disables unified-logging output for that process tree, which is negligible for typical worker processes. One Ruby-side question worth considering, independent of the OS bug: in Ruby 3.x the faulting DNS helper thread does not crash the process cleanly; it spins at 100% CPU and the main thread blocks in wait_getaddrinfo forever, so the process becomes an unkillable hang. (Ruby 2.6 aborts instead.) Even though the underlying getaddrinfo-after-fork is unsupported, would it be worth making this fail fast (raise or exit) rather than spin indefinitely? The longer-term clean fix is presumably a fork-safe userspace resolver (#19430). ---------------------------------------- Bug #21790: `Socket.getaddrinfo` hangs after `fork()` on macOS 26.1 (Tahoe) for IPv4-only hosts https://bugs.ruby-lang.org/issues/21790#change-117704 * Author: adamoffat (Adam Moffat) * Status: Third Party's Issue * ruby -v: 3.3.8 * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN ---------------------------------------- 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:** ``` ruby 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_INET` instead of `AF_UNSPEC` works. `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-replace` to 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-------------------------------- stack_trace.txt (66.6 KB) ruby_dns_fork_bug.rb (1.02 KB) ruby_3.2.6_crash_output.txt (1.79 KB) python_dns_fork_test.py (1.8 KB) python_dns_fork_test.py (1.97 KB) python_crash_output.txt (1.28 KB) -- https://bugs.ruby-lang.org/