Repository navigation
Conversation
The krb5 bundled into the macOS binary gems defaults to its API credential cache, which looks up the primary cache over XPC. libpq probes for GSSAPI credentials on each TCP connection attempt while gssencmode=prefer, so a process that connects, forks and connects again crashes the child in xpc_connection_resume(). See ged#663. Patch krb5 so the API ccache records the pid of its first use and, in a forked child, reports no caches instead of calling the CCAPI framework or XPC. Add a spec that connects in a forked child after the parent connected. It fails with SIGSEGV against an unpatched arm64-darwin gem.
dduugg
marked this pull request as ready for review
October 28, 2026 02:30
Handles and cursors the parent opened reached the CCAPI framework in a forked child too. Opening, initializing or destroying them there now fails as not found, and closing them leaves their CCAPI objects unreleased. This matches the version proposed to krb5 upstream.
This was referenced Oct 28, 2026
The krb5 fork guard recorded the pid at the first API ccache use, so it missed a child that was the first to use it. That child is what ged#663 reports: the objc runtime aborts it with "+[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called", unless OBJC_DISABLE_INITIALIZE_FORK_SAFETY is set. Every Ruby process is multithreaded when it forks, so this hits Spring and any other fork where the parent hadn't connected yet. Record the pid when the library loads, so that every child forked after `require "pg"` skips the API ccache. Kerberos through the API ccache is then unavailable in forked children; a file ccache (KRB5CCNAME=FILE:<path>) still works there. A process that loads pg only after it was itself forked from a multithreaded parent can still hit the abort. Add a spec for the child-first case. It forks from a fresh Ruby process with OBJC_DISABLE_INITIALIZE_FORK_SAFETY unset. Both fork specs now pin gssencmode=prefer, clear KRB5CCNAME and KRB5_CONFIG, print the child's error, and time out after 60 seconds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #663 for the binary gems. #538 and #311 are related, but this patch can't reach them (see the end).
Problem
On macOS, a child forked from a Ruby process can crash when it makes a TCP connection attempt with the default
gssencmode=prefer. A closed port is enough, so no server or credentials are needed:There are two failure modes, depending on whether the parent made an attempt before forking:
The parent attempted a connection first, for example an app server whose master connected before forking workers, or a parallel test runner. The child gets SIGSEGV. Every crash report I collected for this case has the same stack:
The child is the first to attempt one, as under Spring (segfault with macos precompiled binary when using with rails spring #663). The objc runtime aborts the child with
+[__NSCFConstantString initialize] may have been in progress in another thread when fork() was called. ... Crashing instead., fromcc_initialize()underapi_macos_ptcursor_next(). A Ruby process is always multithreaded when it forks, so this happens unlessOBJC_DISABLE_INITIALIZE_FORK_SAFETY=YESis set.libpq probes for GSSAPI credentials on each TCP attempt under
gssencmode=prefer. krb5 built for darwin defaults to itsAPI:ccache (krb5 3bb429b), which reaches the credential cache service through Apple's CCAPI framework and XPC. Neither is safe in a child forked from a multithreaded process.Change
ports/patches/krb5/1.22.2/0003-Avoid-the-macOS-API-ccache-in-forked-children.patchrecords the pid when the library loads. In every child forked afterrequire "pg":KRB5_FCC_NOFILE, including handles the parent opened;So the child never calls the CCAPI framework or XPC. Processes that never fork behave as before.
krb5/krb5#1558 proposes a narrower version upstream, which records the pid at the first API ccache use. That keeps the API ccache working in children of single-threaded C programs, but it leaves the child-first abort. Every Ruby process is multithreaded at fork, so the gem's copy records it at load. The patch header links #1558.
Two specs in
spec/pg/connection_spec.rb, limited to binary gems, connect in a forked child: one after the parent connected, and one forked from a fresh Ruby process that never connected, withOBJC_DISABLE_INITIALIZE_FORK_SAFETYunset. Both pingssencmode=prefer, clearKRB5CCNAMEandKRB5_CONFIG, and time out after 60 seconds.Behavior change for Kerberos users: a forked child no longer sees credentials in the API ccache, including a cache named with
KRB5CCNAME=API:<name>. Withgssauthentication the child fails as if it had no credentials, and undergssencmode=preferit connects without GSSAPI encryption. A file ccache still works in children:kinit -c FILE:/tmp/krb5cc_$(id -u)withKRB5CCNAME=FILE:/tmp/krb5cc_$(id -u).Still not covered: a process that loads pg only after it was itself forked from a multithreaded parent, such as a Puma worker whose master didn't load the app, can still hit the abort. Loading pg before forking avoids it, as does
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES.Testing
On macOS 27.0.1 arm64, with the arm64-darwin gem cross-built with rake-compiler-dock 1.11.1 and the repro above:
OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES#inspectexample, run twice throughconnection_sync_spec.rb, whoseuser=\w*regex doesn't match a local user name containing a dot. It fails without this PR too.binary-gems.yml): without0003(run), the child-first spec fails in all four macOS test jobs (macOS 26 arm64 and macOS 15 Intel, Ruby 3.1 and 4.0). The parent-first spec fails on macOS 26 every time, but on macOS 15 Intel in only one of four runs. With this PR (run), all four macOS test jobs pass. In both runs, the GLIBC, yugabyte and Ubuntu Ruby 3.1 jobs failed before testing on the ubuntu-26.04 runner image (docker-compose: command not found, and no Ruby 3.1.7 there); the same jobs passed in master's latest run.Release builds: the
ports/*-<platform>.installedcheckpoints don't track patch contents, so a tree that has built pg before keeps its old krb5 and libpq. Removeports/krb5-*.installedandports/postgresql-*.installedbefore building a release with this patch.Related
Workarounds until a release:
gssencmode=disable(orPGGSSENCMODE=disable), a Unix-socket host, orKRB5CCNAME=FILE:<path>. For the abort only,OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES.The first two commits record the pid at first use, and the third moves it to load time. I can squash them if you prefer.