Bug #21969
closedfork() + Socket.getaddrinfo() triggers SIGSEGV/SIGABRT via libsystem_trace.dylib on macOS 26 (darwin25) x86_64 and ARM64
Description
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 (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¶
#!/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¶
- Parent process runs continuous
Socket.getaddrinfothreads before forking (primeslibsystem_trace.dylibos_log shared-memory) - Child calls
Socket.getaddrinfo— this triggersos_log_type_enabled→_os_log_preferences_refresh→ stale pointer dereference - macOS 26 (darwin25) only — not reproducible on macOS 14/15
Files
Updated by mame (Yusuke Endoh) 1 day ago
- Is duplicate of Bug #21790: `Socket.getaddrinfo` hangs after `fork()` on macOS 26.1 (Tahoe) for IPv4-only hosts added
Updated by mame (Yusuke Endoh) 1 day ago
- 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.