From 805414c81f2c35f5d2410f8adb39c726d10ced49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Sat, 10 Oct 2026 07:51:31 +0200 Subject: [PATCH] fix: don't kill grandchildren of forked processes The atfork child handler ties each forked child to its parent's lifetime. atfork handlers survive fork, so a child that forks again without exec'ing tied the grandchild to the short-lived intermediate process. Double-forking code such as gpgme (used by the gnupg extension to run gpgconf) had its grandchild killed as soon as the intermediate exited. Only protect direct children of the FrankenPHP process. Fixes https://github.com/php/frankenphp/issues/2689 --- frankenphp.c | 4 ++++ frankenphp_test.go | 18 +++++++++++++++++ testdata/double-fork.php | 43 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 testdata/double-fork.php diff --git a/frankenphp.c b/frankenphp.c index cebdcd2952..d57e4de162 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -178,6 +178,10 @@ static void *frankenphp_parent_death_watcher(void *arg) { #endif static void frankenphp_fork_child(void) { + // a fork of a fork (e.g. a daemonizer) must outlive its short-lived parent + if (is_forked_child) { + return; + } frankenphp_mark_fork_child(); #if defined(__linux__) // if the parent process dies between fork() and this prctl() diff --git a/frankenphp_test.go b/frankenphp_test.go index ef47fff4c9..b8988cbf0f 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -508,6 +508,24 @@ func testPhpInfoForkChild(t *testing.T, opts *testOptions) { }, opts) } +func TestDoubleForkGrandchild_module(t *testing.T) { testDoubleForkGrandchild(t, nil) } +func TestDoubleForkGrandchild_worker(t *testing.T) { + testDoubleForkGrandchild(t, &testOptions{workerScript: "double-fork.php"}) +} +func testDoubleForkGrandchild(t *testing.T, opts *testOptions) { + if opts == nil { + opts = &testOptions{} + } + opts.nbParallelRequests = 1 + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/double-fork.php", handler, t) + if body == "pcntl-unavailable" { + t.Skip("pcntl/posix not fully loaded") + } + require.Equal(t, "grandchild-alive", body) + }, opts) +} + func TestPersistentObject_module(t *testing.T) { testPersistentObject(t, nil) } func TestPersistentObject_worker(t *testing.T) { testPersistentObject(t, &testOptions{workerScript: "persistent-object.php"}) diff --git a/testdata/double-fork.php b/testdata/double-fork.php new file mode 100644 index 0000000000..1c4a4fd1b3 --- /dev/null +++ b/testdata/double-fork.php @@ -0,0 +1,43 @@ +