Project

General

Profile

Actions

Bug #21969

closed

fork() + Socket.getaddrinfo() triggers SIGSEGV/SIGABRT via libsystem_trace.dylib on macOS 26 (darwin25) x86_64 and ARM64

Bug #21969: fork() + Socket.getaddrinfo() triggers SIGSEGV/SIGABRT via libsystem_trace.dylib on macOS 26 (darwin25) x86_64 and ARM64

Added by saurabhpandit (Saurabh Pandit) 1 day ago. Updated 1 day ago.

Status:
Third Party's Issue
Assignee:
-
Target version:
-
[ruby-core:125148]

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

  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


Related issues 1 (0 open1 closed)

Is duplicate of Ruby - Bug #21790: `Socket.getaddrinfo` hangs after `fork()` on macOS 26.1 (Tahoe) for IPv4-only hostsThird Party's IssueActions

Updated by mame (Yusuke Endoh) 1 day ago Actions #1

  • 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 Actions #2 [ruby-core:125149]

  • 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.

Actions

Also available in: PDF Atom