[ruby-core:125148] [Ruby Bug#21969] fork() + Socket.getaddrinfo() triggers SIGSEGV/SIGABRT via libsystem_trace.dylib on macOS 26 (darwin25) x86_64 and ARM64
Issue #21969 has been reported by saurabhpandit (Saurabh Pandit). ---------------------------------------- Bug #21969: fork() + Socket.getaddrinfo() triggers SIGSEGV/SIGABRT via libsystem_trace.dylib on macOS 26 (darwin25) x86_64 and ARM64 https://bugs.ruby-lang.org/issues/21969 * Author: saurabhpandit (Saurabh Pandit) * Status: Open * ruby -v: 4.0.1 * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- On macOS 26 (darwin25), forked child processes crash with SIGSEGV (x86_64) or SIGABRT (ARM64) when calling `Socket.getaddrinfo` after the parent process has established `libsystem_trace.dylib` os_log shared-memory state through prior DNS activity. This is distinct from [#21790](https://bugs.ruby-lang.org/issues/21790) (NAT64 hang/crash in `_gai_nat64_second_pass`). The crash site here is `_os_log_preferences_refresh` and `os_log_type_enabled` inside `libsystem_trace.dylib` — the OS logging subsystem, not the DNS resolver. The stale shared-memory pointer for os_log preferences is inherited across `fork()` and dereferenced in the child, causing the fault. ### Ruby -v x86_64 ~ root# /opt/puppetlabs/puppet/bin/ruby -v ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [x86_64-darwin25] arm64 ~ root# /opt/puppetlabs/puppet/bin/ruby -v ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin25] ### Crash stack (x86_64, Ruby 4.0.1 embedded — same frames observed on 3.x) ``` [BUG] Segmentation fault at 0x0000000101fd0863 ruby 4.0.1 (2025-02-26 revision ???) [x86_64-darwin25] -- C level backtrace information ------------------------------------------- _os_log_preferences_refresh + 0x2f (libsystem_trace.dylib) _os_log_preferences_check_extended (libsystem_trace.dylib) _os_log_type_enabled (libsystem_trace.dylib) ... (DNS resolution internals) Socket.getaddrinfo (ext/socket) ``` Apple's crash reporter (`asi`) confirms: "crashed on child side of fork pre-exec". ### Signal asymmetry between architectures | Architecture | Signal | Reason | |-----------|---------|---------| | x86_64 | SIGSEGV (11) | Ruby's `sigsegv` handler re-delivers the signal; child exits 11 | | ARM64 | SIGABRT (6) | Ruby's `sigabrt` handler calls `abort();` child exits 6 | Both architectures crash identically — the signal number differs only due to Ruby's signal handler behaviour. Monitoring that watches only for `termsig == 11` **will miss all ARM64 crashes** . ### Reproducer ```ruby #!/usr/bin/env ruby # frozen_string_literal: true # # Minimal Ruby-only fork reproducer for macOS 26 segfault. # Only configurable option: TRIALS=<n> # # Run: ruby reproducer.rb # Crash expected within ~10–20 trials on a primed x86_64 system. # ARM64: may crash on trial 1; check for termsig 6 (SIGABRT) not just 11. require 'socket' TRIALS = Integer(ENV.fetch('TRIALS', '50')) HOST = 'api.segment.io' PORT = 443 PRIME_THREADS = 8 SIGSEGV_NUM = 11 SIGABRT_NUM = 6 # Keep parent DNS activity running while children fork to increase race # likelihood — this primes the os_log shared-memory state in libsystem_trace. def start_prime_threads(host, port, thread_count) stop = false threads = Array.new(thread_count) do Thread.new do until stop begin Socket.getaddrinfo(host, port, nil, :STREAM) rescue nil end end end end [threads, proc { stop = true }] end puts '=== Minimal Ruby fork reproducer ===' puts "Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})" puts "Host=#{HOST} Port=#{PORT} Trials=#{TRIALS}" puts prime_threads, stop_primers = start_prime_threads(HOST, PORT, PRIME_THREADS) sleep 0.2 puts 'Priming active' puts crash_count = 0 begin TRIALS.times do |trial| pid = fork do Socket.getaddrinfo(HOST, PORT, nil, :STREAM) exit 0 rescue exit 0 end _, status = Process.waitpid2(pid) if status.signaled? && [SIGSEGV_NUM, SIGABRT_NUM].include?(status.termsig) crash_count += 1 puts "[Trial #{trial + 1}] CRASH signal=#{status.termsig}" else print '.' end end ensure stop_primers.call prime_threads.each(&:join) end puts puts '=== Summary ===' puts "CRASH: #{crash_count} / #{TRIALS}" exit(crash_count.positive? ? 1 : 0) ``` ### Observed output (x86_64-darwin25) ``` === Minimal Ruby fork reproducer === Ruby 3.4.1 (x86_64-darwin25) Host=api.segment.io Port=443 Trials=50 Priming active ......[Trial 7] CRASH signal=11 ......[Trial 14] CRASH signal=11 ... === Summary === CRASH: 8 / 50 ``` On ARM64, signal=6 (SIGABRT) is reported instead of 11. ### Conditions required 1. Parent process runs continuous `Socket.getaddrinfo` threads before forking (primes `libsystem_trace.dylib` os_log shared-memory) 2. Child calls `Socket.getaddrinfo` — this triggers `os_log_type_enabled` → `_os_log_preferences_refresh` → stale pointer dereference 3. macOS 26 (darwin25) only — not reproducible on macOS 14/15 ---Files-------------------------------- ruby_fork_resolv_getaddrinfo_prime1_x86_64.log (197 KB) ruby_fork_resolv_getaddrinfo_prime1_ARM64.log (202 KB) -- https://bugs.ruby-lang.org/
Issue #21969 has been updated by mame (Yusuke Endoh). Status changed from Open to Third Party's Issue #21790 also identifies `_os_log_preferences_refresh` in `libsystem_trace.dylib` as the crash site (see note #4). The NAT64 code path (`_gai_nat64_second_pass`) appears higher in the call stack, but the actual fault is the same stale shared-memory dereference after fork. So I believe this is a duplicate of #21790. I consider this most likely a macOS bug where `getaddrinfo` is not fork-safe on macOS 26. If you have evidence that this is not an OS-level issue, or if you know a workaround on the Ruby side, I am happy to reconsider. ---------------------------------------- Bug #21969: fork() + Socket.getaddrinfo() triggers SIGSEGV/SIGABRT via libsystem_trace.dylib on macOS 26 (darwin25) x86_64 and ARM64 https://bugs.ruby-lang.org/issues/21969#change-116881 * Author: saurabhpandit (Saurabh Pandit) * Status: Third Party's Issue * ruby -v: 4.0.1 * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- On macOS 26 (darwin25), forked child processes crash with SIGSEGV (x86_64) or SIGABRT (ARM64) when calling `Socket.getaddrinfo` after the parent process has established `libsystem_trace.dylib` os_log shared-memory state through prior DNS activity. This is distinct from [#21790](https://bugs.ruby-lang.org/issues/21790) (NAT64 hang/crash in `_gai_nat64_second_pass`). The crash site here is `_os_log_preferences_refresh` and `os_log_type_enabled` inside `libsystem_trace.dylib` — the OS logging subsystem, not the DNS resolver. The stale shared-memory pointer for os_log preferences is inherited across `fork()` and dereferenced in the child, causing the fault. ### Ruby -v x86_64 ~ root# /opt/puppetlabs/puppet/bin/ruby -v ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [x86_64-darwin25] arm64 ~ root# /opt/puppetlabs/puppet/bin/ruby -v ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [arm64-darwin25] ### Crash stack (x86_64, Ruby 4.0.1 embedded — same frames observed on 3.x) ``` [BUG] Segmentation fault at 0x0000000101fd0863 ruby 4.0.1 (2025-02-26 revision ???) [x86_64-darwin25] -- C level backtrace information ------------------------------------------- _os_log_preferences_refresh + 0x2f (libsystem_trace.dylib) _os_log_preferences_check_extended (libsystem_trace.dylib) _os_log_type_enabled (libsystem_trace.dylib) ... (DNS resolution internals) Socket.getaddrinfo (ext/socket) ``` Apple's crash reporter (`asi`) confirms: "crashed on child side of fork pre-exec". ### Signal asymmetry between architectures | Architecture | Signal | Reason | |-----------|---------|---------| | x86_64 | SIGSEGV (11) | Ruby's `sigsegv` handler re-delivers the signal; child exits 11 | | ARM64 | SIGABRT (6) | Ruby's `sigabrt` handler calls `abort();` child exits 6 | Both architectures crash identically — the signal number differs only due to Ruby's signal handler behaviour. Monitoring that watches only for `termsig == 11` **will miss all ARM64 crashes** . ### Reproducer ```ruby #!/usr/bin/env ruby # frozen_string_literal: true # # Minimal Ruby-only fork reproducer for macOS 26 segfault. # Only configurable option: TRIALS=<n> # # Run: ruby reproducer.rb # Crash expected within ~10–20 trials on a primed x86_64 system. # ARM64: may crash on trial 1; check for termsig 6 (SIGABRT) not just 11. require 'socket' TRIALS = Integer(ENV.fetch('TRIALS', '50')) HOST = 'api.segment.io' PORT = 443 PRIME_THREADS = 8 SIGSEGV_NUM = 11 SIGABRT_NUM = 6 # Keep parent DNS activity running while children fork to increase race # likelihood — this primes the os_log shared-memory state in libsystem_trace. def start_prime_threads(host, port, thread_count) stop = false threads = Array.new(thread_count) do Thread.new do until stop begin Socket.getaddrinfo(host, port, nil, :STREAM) rescue nil end end end end [threads, proc { stop = true }] end puts '=== Minimal Ruby fork reproducer ===' puts "Ruby #{RUBY_VERSION} (#{RUBY_PLATFORM})" puts "Host=#{HOST} Port=#{PORT} Trials=#{TRIALS}" puts prime_threads, stop_primers = start_prime_threads(HOST, PORT, PRIME_THREADS) sleep 0.2 puts 'Priming active' puts crash_count = 0 begin TRIALS.times do |trial| pid = fork do Socket.getaddrinfo(HOST, PORT, nil, :STREAM) exit 0 rescue exit 0 end _, status = Process.waitpid2(pid) if status.signaled? && [SIGSEGV_NUM, SIGABRT_NUM].include?(status.termsig) crash_count += 1 puts "[Trial #{trial + 1}] CRASH signal=#{status.termsig}" else print '.' end end ensure stop_primers.call prime_threads.each(&:join) end puts puts '=== Summary ===' puts "CRASH: #{crash_count} / #{TRIALS}" exit(crash_count.positive? ? 1 : 0) ``` ### Observed output (x86_64-darwin25) ``` === Minimal Ruby fork reproducer === Ruby 3.4.1 (x86_64-darwin25) Host=api.segment.io Port=443 Trials=50 Priming active ......[Trial 7] CRASH signal=11 ......[Trial 14] CRASH signal=11 ... === Summary === CRASH: 8 / 50 ``` On ARM64, signal=6 (SIGABRT) is reported instead of 11. ### Conditions required 1. Parent process runs continuous `Socket.getaddrinfo` threads before forking (primes `libsystem_trace.dylib` os_log shared-memory) 2. Child calls `Socket.getaddrinfo` — this triggers `os_log_type_enabled` → `_os_log_preferences_refresh` → stale pointer dereference 3. macOS 26 (darwin25) only — not reproducible on macOS 14/15 ---Files-------------------------------- ruby_fork_resolv_getaddrinfo_prime1_x86_64.log (197 KB) ruby_fork_resolv_getaddrinfo_prime1_ARM64.log (202 KB) -- https://bugs.ruby-lang.org/
participants (2)
-
mame (Yusuke Endoh) -
saurabhpandit (Saurabh Pandit)