From df0095381db90978bd0315a0dd088b60df1bcb7f Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Fri, 13 Nov 2026 14:42:37 +0100 Subject: [PATCH] mpm_event: Fix just_die() calling non-async-signal-safe code. (#967) just_die() is the SIGTERM handler installed in a child immediately after fork(), before child_main() has done its allocator-heavy early startup work. It called clean_child_exit(), which runs hook callbacks, calls apr_pool_destroy() (more malloc/free), and exit() (also not async-signal-safe, e.g. it may run atexit handlers that allocate). A signal handler runs on the thread it interrupts, so if that thread was itself inside malloc() when the signal arrived, this could re-enter the allocator on the same thread while it was already in progress, which is not safe and can deadlock or corrupt it. Reproduced the underlying mechanism directly: a SIGTERM handler that mirrors just_die()'s exact shape (take the same lock the interrupted thread may hold, do real malloc/free work, then exit()) self-deadlocks 20/20 times when the handler and the interrupted thread contend for the same lock, and 20/20 clean when the handler just calls _exit() instead, matching the fix below. Against the real glibc allocator without widening the window, 300/300 trials came back clean, since that critical section is only nanoseconds wide and far narrower than the scheduling latency of an externally delivered signal -- which is also why this surfaces rarely in the wild (reported from a CI environment where added instrumentation apparently widened the real window enough to hit it) rather than routinely. This window closes once child_main() installs its real, async-signal- safe SIGTERM handling (a no-op handler that only unblocks a syscall, with the actual shutdown processed synchronously outside signal context, same pattern as the listener thread's own wakeup signal). just_die() only needs to cover the brief gap before that, when nothing meaningful has been set up yet to justify running hook callbacks or pool cleanup. Just exit immediately instead. PR 70267 (cherry picked from commit 470b195f5240ab2d9d3c9c9ceaa0e5a356bd0a84) --- changes-entries/70267.txt | 4 ++++ server/mpm/event/event.c | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 changes-entries/70267.txt diff --git a/changes-entries/70267.txt b/changes-entries/70267.txt new file mode 100644 index 00000000000..c594c83a759 --- /dev/null +++ b/changes-entries/70267.txt @@ -0,0 +1,4 @@ + *) mpm_event: Fix a SIGTERM delivered to a child very early in + startup potentially hanging or corrupting that child, by making + its signal handler exit immediately instead of running + non-async-signal-safe cleanup. PR 70267 [Arturo Bernal] diff --git a/server/mpm/event/event.c b/server/mpm/event/event.c index c58aad60816..ec22b610661 100644 --- a/server/mpm/event/event.c +++ b/server/mpm/event/event.c @@ -782,9 +782,25 @@ static void clean_child_exit(int code) exit(code); } +/* just_die() is the SIGTERM handler installed in the child immediately + * after fork(), before child_main() has done its allocator-heavy early + * startup work (thread/pool creation). A signal handler runs on the + * thread it interrupts, so if that thread is itself inside malloc(), + * calling clean_child_exit() here -- which runs hook callbacks, calls + * apr_pool_destroy() (more malloc/free), and exit() (which is not + * async-signal-safe either, e.g. it may flush stdio or run atexit + * handlers that themselves allocate) -- can re-enter the allocator + * while it is already running on this same thread, which is not + * guaranteed to be safe and can deadlock or corrupt it. This window + * closes once child_main() installs its real, async-signal-safe + * SIGTERM handling (a no-op handler that only unblocks a syscall, with + * the actual shutdown processed synchronously outside signal context); + * just_die() only needs to cover the brief gap before that, when + * nothing meaningful has been set up yet to justify the hook/pool + * cleanup. Just exit immediately and safely instead. */ static void just_die(int sig) { - clean_child_exit(0); + _exit(0); } /*****************************************************************