From c54969e90bf403fed02a05aa36a94a89ce8f7a1c Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Tue, 27 Oct 2026 19:11:33 -0700 Subject: [PATCH 1/3] Avoid a crash in forked children on macOS with the binary gem 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 #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. --- ...-macOS-API-ccache-in-forked-children.patch | 94 +++++++++++++++++++ spec/pg/connection_spec.rb | 16 ++++ 2 files changed, 110 insertions(+) create mode 100644 ports/patches/krb5/1.22.2/0003-Avoid-the-macOS-API-ccache-in-forked-children.patch 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..f1f68a8b0 --- /dev/null +++ b/ports/patches/krb5/1.22.2/0003-Avoid-the-macOS-API-ccache-in-forked-children.patch @@ -0,0 +1,94 @@ +From 87d0303eb17df767d234e0bee684084141303a8f Mon Sep 17 00:00:00 2001 +From: Douglas Eichelberger +Date: Tue, 27 Oct 2026 18:55:48 -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. In a child forked from a process +that already used them, those calls can crash the child: a Ruby +process using libpq with the default gssencmode=prefer crashes in +xpc_connection_resume() under get_primary_name() when it forks and +connects again. + +Record the pid of the first API ccache use. In a child forked after +that, report the API collection as empty and fail cache resolution with +KRB5_FCC_NOFILE before calling the CCAPI framework or XPC. +--- + lib/krb5/ccache/cc_api_macos.c | 33 +++++++++++++++++++++++++++++++++ + 1 file changed, 33 insertions(+) + +diff --git a/lib/krb5/ccache/cc_api_macos.c b/lib/krb5/ccache/cc_api_macos.c +index 34b1c350f..78b764be1 100644 +--- a/lib/krb5/ccache/cc_api_macos.c ++++ b/lib/krb5/ccache/cc_api_macos.c +@@ -64,6 +64,29 @@ struct api_macos_ptcursor { + cc_ccache_iterator_t iter; + }; + ++/* ++ * The CCAPI framework and XPC are not safe to use in a child process forked ++ * after the parent used them; calling them there can crash the child. Record ++ * the pid of the first use, and in a forked child report the collection as ++ * empty instead of contacting the credential cache service. ++ */ ++static k5_once_t first_use_once = K5_ONCE_INIT; ++static pid_t first_use_pid; ++ ++static void ++record_first_use(void) ++{ ++ first_use_pid = getpid(); ++} ++ ++static krb5_boolean ++in_forked_child(void) ++{ ++ if (k5_once(&first_use_once, record_first_use) != 0) ++ return FALSE; ++ return first_use_pid != getpid(); ++} ++ + /* Map a CCAPI error code to a com_err code. */ + static krb5_error_code + ccerr2mit(uint32_t err) +@@ -96,6 +119,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; +@@ -171,6 +196,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 +281,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; +@@ -576,6 +607,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 +-- +2.56.0 + diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index dbc64ca76..3b83b12d3 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -579,6 +579,22 @@ 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) + + described_class.connect( @conninfo ) { |conn| conn.exec("SELECT 1") } + pid = fork do + described_class.connect( @conninfo ) { |conn| conn.exec("SELECT 1") } + exit!(0) + rescue Exception + exit!(1) + end + Process.wait( pid ) + expect( $?.termsig ).to be_nil + expect( $?.exitstatus ).to eq( 0 ) + end end context "connect_start" do From 7068d547db9f975c9be2de9fb717ebb0f8dfe510 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Tue, 27 Oct 2026 19:47:43 -0700 Subject: [PATCH 2/3] Extend the krb5 fork guard to inherited API ccache handles 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. --- ...-macOS-API-ccache-in-forked-children.patch | 99 ++++++++++++++++--- 1 file changed, 86 insertions(+), 13 deletions(-) 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 index f1f68a8b0..efd7a89fd 100644 --- 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 @@ -1,4 +1,4 @@ -From 87d0303eb17df767d234e0bee684084141303a8f Mon Sep 17 00:00:00 2001 +From 4efc55a4fcd8d2d88192553cfbf39e7aadaa7b7d Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Tue, 27 Oct 2026 18:55:48 -0700 Subject: [PATCH] Avoid the macOS API ccache in forked children @@ -11,25 +11,27 @@ xpc_connection_resume() under get_primary_name() when it forks and connects again. Record the pid of the first API ccache use. In a child forked after -that, report the API collection as empty and fail cache resolution with -KRB5_FCC_NOFILE before calling the CCAPI framework or XPC. +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. --- - lib/krb5/ccache/cc_api_macos.c | 33 +++++++++++++++++++++++++++++++++ - 1 file changed, 33 insertions(+) + lib/krb5/ccache/cc_api_macos.c | 63 ++++++++++++++++++++++++++++------ + 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/lib/krb5/ccache/cc_api_macos.c b/lib/krb5/ccache/cc_api_macos.c -index 34b1c350f..78b764be1 100644 +index 34b1c350f..a7184546a 100644 --- a/lib/krb5/ccache/cc_api_macos.c +++ b/lib/krb5/ccache/cc_api_macos.c -@@ -64,6 +64,29 @@ struct api_macos_ptcursor { +@@ -64,6 +64,30 @@ struct api_macos_ptcursor { cc_ccache_iterator_t iter; }; +/* + * The CCAPI framework and XPC are not safe to use in a child process forked + * after the parent used them; calling them there can crash the child. Record -+ * the pid of the first use, and in a forked child report the collection as -+ * empty instead of contacting the credential cache service. ++ * the pid of the first use. In a forked child, 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 k5_once_t first_use_once = K5_ONCE_INIT; +static pid_t first_use_pid; @@ -51,7 +53,7 @@ index 34b1c350f..78b764be1 100644 /* Map a CCAPI error code to a com_err code. */ static krb5_error_code ccerr2mit(uint32_t err) -@@ -96,6 +119,8 @@ make_cache(const char *residual, cc_context_t cc_context, +@@ -96,6 +120,8 @@ make_cache(const char *residual, cc_context_t cc_context, *ccache_out = NULL; if (cc_context == NULL) { @@ -60,7 +62,16 @@ index 34b1c350f..78b764be1 100644 err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL); if (err != ccNoError) return KRB5_FCC_INTERNAL; -@@ -171,6 +196,9 @@ get_primary_name(krb5_context context, char **name_out) +@@ -133,6 +159,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 +199,9 @@ get_primary_name(krb5_context context, char **name_out) *name_out = NULL; @@ -70,7 +81,7 @@ index 34b1c350f..78b764be1 100644 if (uname(&un) == 0) { release = strtol(un.release, &end, 10); if (end != un.release && release < 20) { -@@ -253,6 +281,9 @@ api_macos_gen_new(krb5_context context, krb5_ccache *cache_out) +@@ -253,6 +284,9 @@ api_macos_gen_new(krb5_context context, krb5_ccache *cache_out) *cache_out = NULL; @@ -80,7 +91,52 @@ index 34b1c350f..78b764be1 100644 err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL); if (err) goto cleanup; -@@ -576,6 +607,8 @@ api_macos_ptcursor_next(krb5_context context, krb5_cc_ptcursor ptcursor, +@@ -291,6 +325,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 +356,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 +372,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 +479,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 +615,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; @@ -89,6 +145,23 @@ index 34b1c350f..78b764be1 100644 defresidual = defname + 4; /* If the default cache name is a subsidiary cache, yield that cache if it +@@ -640,10 +681,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 From 387e9c51dce86bba09efefcbb0f80dbec8c6fed8 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Tue, 27 Oct 2026 20:37:30 -0700 Subject: [PATCH 3/3] Skip the macOS API ccache in every forked child 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 #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:) 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. --- ...-macOS-API-ccache-in-forked-children.patch | 73 ++++++++++--------- spec/pg/connection_spec.rb | 63 +++++++++++++--- 2 files changed, 92 insertions(+), 44 deletions(-) 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 index efd7a89fd..06ea50170 100644 --- 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 @@ -1,59 +1,64 @@ -From 4efc55a4fcd8d2d88192553cfbf39e7aadaa7b7d Mon Sep 17 00:00:00 2001 +From 492efc330a8c25e3dde03fc06d39c187fbb1e192 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger -Date: Tue, 27 Oct 2026 18:55:48 -0700 +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. In a child forked from a process -that already used them, those calls can crash the child: a Ruby -process using libpq with the default gssencmode=prefer crashes in -xpc_connection_resume() under get_primary_name() when it forks and -connects again. +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 of the first API ccache use. In a child forked after +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 | 63 ++++++++++++++++++++++++++++------ - 1 file changed, 53 insertions(+), 10 deletions(-) + 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..a7184546a 100644 +index 34b1c350f..59e36fa86 100644 --- a/lib/krb5/ccache/cc_api_macos.c +++ b/lib/krb5/ccache/cc_api_macos.c -@@ -64,6 +64,30 @@ struct api_macos_ptcursor { +@@ -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 -+ * after the parent used them; calling them there can crash the child. Record -+ * the pid of the first use. In a forked child, 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. ++ * 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 k5_once_t first_use_once = K5_ONCE_INIT; -+static pid_t first_use_pid; ++static pid_t load_pid; + -+static void -+record_first_use(void) ++static void __attribute__((constructor)) ++record_load_pid(void) +{ -+ first_use_pid = getpid(); ++ load_pid = getpid(); +} + +static krb5_boolean +in_forked_child(void) +{ -+ if (k5_once(&first_use_once, record_first_use) != 0) -+ return FALSE; -+ return first_use_pid != getpid(); ++ return load_pid != getpid(); +} + /* Map a CCAPI error code to a com_err code. */ static krb5_error_code ccerr2mit(uint32_t err) -@@ -96,6 +120,8 @@ make_cache(const char *residual, cc_context_t cc_context, +@@ -96,6 +118,8 @@ make_cache(const char *residual, cc_context_t cc_context, *ccache_out = NULL; if (cc_context == NULL) { @@ -62,7 +67,7 @@ index 34b1c350f..a7184546a 100644 err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL); if (err != ccNoError) return KRB5_FCC_INTERNAL; -@@ -133,6 +159,8 @@ oom: +@@ -133,6 +157,8 @@ oom: static uint32_t open_cache(struct api_macos_cache_data *data) { @@ -71,7 +76,7 @@ index 34b1c350f..a7184546a 100644 if (data->cache != NULL) return ccNoError; return cc_context_open_ccache(data->cc_context, data->residual, -@@ -171,6 +199,9 @@ get_primary_name(krb5_context context, char **name_out) +@@ -171,6 +197,9 @@ get_primary_name(krb5_context context, char **name_out) *name_out = NULL; @@ -81,7 +86,7 @@ index 34b1c350f..a7184546a 100644 if (uname(&un) == 0) { release = strtol(un.release, &end, 10); if (end != un.release && release < 20) { -@@ -253,6 +284,9 @@ api_macos_gen_new(krb5_context context, krb5_ccache *cache_out) +@@ -253,6 +282,9 @@ api_macos_gen_new(krb5_context context, krb5_ccache *cache_out) *cache_out = NULL; @@ -91,7 +96,7 @@ index 34b1c350f..a7184546a 100644 err = cc_initialize(&cc_context, ccapi_version_max, NULL, NULL); if (err) goto cleanup; -@@ -291,6 +325,9 @@ api_macos_initialize(krb5_context context, krb5_ccache cache, +@@ -291,6 +323,9 @@ api_macos_initialize(krb5_context context, krb5_ccache cache, uint32_t err; char *princstr = NULL, *prefix_name = NULL; @@ -101,7 +106,7 @@ index 34b1c350f..a7184546a 100644 /* 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 +356,11 @@ api_macos_close(krb5_context context, krb5_ccache cache) +@@ -319,9 +354,11 @@ api_macos_close(krb5_context context, krb5_ccache cache) { struct api_macos_cache_data *data = cache->data; @@ -116,7 +121,7 @@ index 34b1c350f..a7184546a 100644 free(data->residual); free(data); free(cache); -@@ -333,8 +372,7 @@ api_macos_destroy(krb5_context context, krb5_ccache cache) +@@ -333,8 +370,7 @@ api_macos_destroy(krb5_context context, krb5_ccache cache) { struct api_macos_cache_data *data = cache->data; @@ -126,7 +131,7 @@ index 34b1c350f..a7184546a 100644 cc_ccache_destroy(data->cache); data->cache = NULL; } -@@ -441,7 +479,8 @@ api_macos_end_seq_get(krb5_context context, krb5_ccache cache, +@@ -441,7 +477,8 @@ api_macos_end_seq_get(krb5_context context, krb5_ccache cache, { cc_credentials_iterator_t iter = *cursor; @@ -136,7 +141,7 @@ index 34b1c350f..a7184546a 100644 *cursor = NULL; return 0; } -@@ -576,6 +615,8 @@ api_macos_ptcursor_next(krb5_context context, krb5_cc_ptcursor ptcursor, +@@ -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; @@ -145,7 +150,7 @@ index 34b1c350f..a7184546a 100644 defresidual = defname + 4; /* If the default cache name is a subsidiary cache, yield that cache if it -@@ -640,10 +681,12 @@ api_macos_ptcursor_free(krb5_context context, krb5_cc_ptcursor *ptcursor) +@@ -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) { diff --git a/spec/pg/connection_spec.rb b/spec/pg/connection_spec.rb index 3b83b12d3..60efe065f 100644 --- a/spec/pg/connection_spec.rb +++ b/spec/pg/connection_spec.rb @@ -584,16 +584,59 @@ skip "applies to native binary gems only" unless PG::IS_BINARY_GEM skip "requires fork" unless Process.respond_to?(:fork) - described_class.connect( @conninfo ) { |conn| conn.exec("SELECT 1") } - pid = fork do - described_class.connect( @conninfo ) { |conn| conn.exec("SELECT 1") } - exit!(0) - rescue Exception - exit!(1) - end - Process.wait( pid ) - expect( $?.termsig ).to be_nil - expect( $?.exitstatus ).to eq( 0 ) + 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