diff --git a/ports/patches/krb5/1.22.2/0003-Avoid-the-macOS-API-ccache-in-forked-children.patch b/ports/patches/krb5/1.22.2/0003-Avoid-the-macOS-API-ccache-in-forked-children.patch new file mode 100644 index 000000000..06ea50170 --- /dev/null +++ b/ports/patches/krb5/1.22.2/0003-Avoid-the-macOS-API-ccache-in-forked-children.patch @@ -0,0 +1,172 @@ +From 492efc330a8c25e3dde03fc06d39c187fbb1e192 Mon Sep 17 00:00:00 2001 +From: Douglas Eichelberger +Date: Tue, 27 Oct 2026 20:22:52 -0700 +Subject: [PATCH] Avoid the macOS API ccache in forked children + +The macOS API ccache module talks to the credential cache service +through the CCAPI framework and XPC. Neither is safe to use in a child +forked from a multithreaded process, which every Ruby process is. A +child that connects with libpq and the default gssencmode=prefer +crashes in xpc_connection_resume() under get_primary_name() if the +parent used the API ccache before forking, and the objc runtime aborts +it in cc_initialize() if the parent did not. + +Record the pid when the library loads. In a process forked after +that, report the API collection as empty, treat caches as not found +(KRB5_FCC_NOFILE), and leave CCAPI objects inherited from the parent +unreleased, so the child never calls the CCAPI framework or XPC. +Destroying an inherited cache handle leaves the parent's cache in +place. + +Upstream: https://github.com/krb5/krb5/pull/1558 records the pid at +the first API ccache use, so it covers only children forked after the +parent used it. +--- + lib/krb5/ccache/cc_api_macos.c | 61 ++++++++++++++++++++++++++++------ + 1 file changed, 51 insertions(+), 10 deletions(-) + +diff --git a/lib/krb5/ccache/cc_api_macos.c b/lib/krb5/ccache/cc_api_macos.c +index 34b1c350f..59e36fa86 100644 +--- a/lib/krb5/ccache/cc_api_macos.c ++++ b/lib/krb5/ccache/cc_api_macos.c +@@ -64,6 +64,28 @@ struct api_macos_ptcursor { + cc_ccache_iterator_t iter; + }; + ++/* ++ * The CCAPI framework and XPC are not safe to use in a child process forked ++ * from a multithreaded parent: depending on what the parent did, the child ++ * crashes in XPC or the objc runtime aborts it. Record the pid when the ++ * library loads. In a process forked after that, report the collection as ++ * empty and caches as not found instead of contacting the credential cache ++ * service, and leave CCAPI objects inherited from the parent unreleased. ++ */ ++static pid_t load_pid; ++ ++static void __attribute__((constructor)) ++record_load_pid(void) ++{ ++ load_pid = getpid(); ++} ++ ++static krb5_boolean ++in_forked_child(void) ++{ ++ return load_pid != getpid(); ++} ++ + /* Map a CCAPI error code to a com_err code. */ + static krb5_error_code + ccerr2mit(uint32_t err) +@@ -96,6 +118,8 @@ make_cache(const char *residual, cc_context_t cc_context, + *ccache_out = NULL; + + if (cc_context == NULL) { ++ if (in_forked_child()) ++ return KRB5_FCC_NOFILE; + err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL); + if (err != ccNoError) + return KRB5_FCC_INTERNAL; +@@ -133,6 +157,8 @@ oom: + static uint32_t + open_cache(struct api_macos_cache_data *data) + { ++ if (in_forked_child()) ++ return ccErrCCacheNotFound; + if (data->cache != NULL) + return ccNoError; + return cc_context_open_ccache(data->cc_context, data->residual, +@@ -171,6 +197,9 @@ get_primary_name(krb5_context context, char **name_out) + + *name_out = NULL; + ++ if (in_forked_child()) ++ return KRB5_FCC_NOFILE; ++ + if (uname(&un) == 0) { + release = strtol(un.release, &end, 10); + if (end != un.release && release < 20) { +@@ -253,6 +282,9 @@ api_macos_gen_new(krb5_context context, krb5_ccache *cache_out) + + *cache_out = NULL; + ++ if (in_forked_child()) ++ return KRB5_FCC_NOFILE; ++ + err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL); + if (err) + goto cleanup; +@@ -291,6 +323,9 @@ api_macos_initialize(krb5_context context, krb5_ccache cache, + uint32_t err; + char *princstr = NULL, *prefix_name = NULL; + ++ if (in_forked_child()) ++ return KRB5_FCC_NOFILE; ++ + /* Apple's cc_context_create_ccache() requires a name with type prefix. */ + if (asprintf(&prefix_name, "API:%s", data->residual) < 0) + return ENOMEM; +@@ -319,9 +354,11 @@ api_macos_close(krb5_context context, krb5_ccache cache) + { + struct api_macos_cache_data *data = cache->data; + +- if (data->cache != NULL) +- cc_ccache_release(data->cache); +- cc_context_release(data->cc_context); ++ if (!in_forked_child()) { ++ if (data->cache != NULL) ++ cc_ccache_release(data->cache); ++ cc_context_release(data->cc_context); ++ } + free(data->residual); + free(data); + free(cache); +@@ -333,8 +370,7 @@ api_macos_destroy(krb5_context context, krb5_ccache cache) + { + struct api_macos_cache_data *data = cache->data; + +- open_cache(data); +- if (data->cache != NULL) { ++ if (open_cache(data) == ccNoError) { + cc_ccache_destroy(data->cache); + data->cache = NULL; + } +@@ -441,7 +477,8 @@ api_macos_end_seq_get(krb5_context context, krb5_ccache cache, + { + cc_credentials_iterator_t iter = *cursor; + +- cc_credentials_iterator_release(iter); ++ if (!in_forked_child()) ++ cc_credentials_iterator_release(iter); + *cursor = NULL; + return 0; + } +@@ -576,6 +613,8 @@ api_macos_ptcursor_next(krb5_context context, krb5_cc_ptcursor ptcursor, + defname = krb5_cc_default_name(context); + if (defname == NULL || strncmp(defname, "API:", 4) != 0) + return 0; ++ if (in_forked_child()) ++ return 0; + defresidual = defname + 4; + + /* If the default cache name is a subsidiary cache, yield that cache if it +@@ -640,10 +679,12 @@ api_macos_ptcursor_free(krb5_context context, krb5_cc_ptcursor *ptcursor) + struct api_macos_ptcursor *apt = (*ptcursor)->data; + + if (apt != NULL) { +- if (apt->iter != NULL) +- cc_ccache_iterator_release(apt->iter); +- if (apt->cc_context != NULL) +- cc_context_release(apt->cc_context); ++ if (!in_forked_child()) { ++ if (apt->iter != NULL) ++ cc_ccache_iterator_release(apt->iter); ++ if (apt->cc_context != NULL) ++ cc_context_release(apt->cc_context); ++ } + free(apt->primary); + free(apt); + } +-- +2.56.0 + diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index dbc64ca76..60efe065f 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -579,6 +579,65 @@ end end end + + it "in a forked child after the parent connected" do + skip "applies to native binary gems only" unless PG::IS_BINARY_GEM + skip "requires fork" unless Process.respond_to?(:fork) + + conninfo = "#{@conninfo} gssencmode=prefer" + pid = nil + with_env_vars( KRB5CCNAME: nil, KRB5_CONFIG: nil ) do + described_class.connect( conninfo ) { |conn| conn.exec("SELECT 1") } + pid = fork do + described_class.connect( conninfo ) { |conn| conn.exec("SELECT 1") } + # exit! skips the finalizers that would close the parent's connections. + exit!(0) + rescue Exception => e + $stderr.puts e.full_message + exit!(1) + end + end + _, status = Timeout.timeout(60) { Process.wait2( pid ) } + expect( status ).to be_success + ensure + if pid && !status + Process.kill( :KILL, pid ) + Process.wait( pid ) + end + end + + it "in a forked child of a process that never connected" do + skip "applies to native binary gems only" unless PG::IS_BINARY_GEM + skip "requires fork" unless Process.respond_to?(:fork) + + # This process has connected already, so fork from a fresh one. + # OBJC_DISABLE_INITIALIZE_FORK_SAFETY would hide the objc abort on macOS. + script = <<~RUBY + require "pg" + pid = fork do + PG.connect( ARGV[0] ) { |conn| conn.exec("SELECT 1") } + exit!(0) + rescue Exception => e + $stderr.puts e.full_message + exit!(1) + end + _, status = Process.wait2( pid ) + puts status.inspect + exit( status.success? ? 0 : 1 ) + RUBY + env = { "OBJC_DISABLE_INITIALIZE_FORK_SAFETY" => nil, "KRB5CCNAME" => nil, "KRB5_CONFIG" => nil } + # This process's load path, so that the new one loads the same pg extension. + load_path = $LOAD_PATH.flat_map { |dir| ["-I", dir.to_s] } + io = IO.popen( [env, RbConfig.ruby, *load_path, "-e", script, "#{@conninfo} gssencmode=prefer"], err: [:child, :out] ) + output = Timeout.timeout(60) { io.read } + io.close + expect( $? ).to be_success, output + ensure + if io && !io.closed? + Process.kill( :KILL, io.pid ) + io.close + end + end end context "connect_start" do