From b2941a8cb9b059fe78594e84548ae54a57030029 Mon Sep 17 00:00:00 2001 From: dshivashankar Date: Thu, 26 Mar 2026 11:28:51 +0530 Subject: [PATCH] Fix GIL deadlocks during raw C/Perl forks by integrating PyOS fork handlers When `pyperl` executes a raw POSIX `fork()` while background Python threads (such as OpenTelemetry metric exporters) are active, the newly created child process inherits an exact memory clone of the parent. Crucially, if a background thread held the Python GIL or other internal CPython locks (like the import lock) at the exact time of the fork, the child process inherits those locks in a permanently locked state held by non-existent "ghost" threads. When the child process subsequently attempts to execute Python code, or even attempts to terminate (which triggers `Py_Finalize` for garbage collection), it attempts to acquire the GIL, resulting in a permanent deadlock. Furthermore, because the `fork()` originates from C/Perl, Python's internal fork lifecycle is bypassed. High-level fork safety mechanisms registered via `os.register_at_fork()` (which are designed to gracefully pause background threads before a fork) are never executed. This patch implements `pthread_atfork` hooks at the C-extension layer to properly synchronize raw C-level forks with the Python interpreter (leveraging APIs introduced in Python 3.7+). * **`_atfork_prepare`:** Safely acquires the Python GIL via `PyGILState_Ensure()` and explicitly calls `PyOS_BeforeFork()`. This forces the Python interpreter to execute all registered Python-level `before` fork handlers, allowing background threads to safely park themselves and relinquish internal locks *before* the OS duplicates the memory. * **`_atfork_parent` / `_atfork_child`:** Calls the respective `PyOS_AfterFork_Parent()` and `PyOS_AfterFork_Child()` C-API functions. This triggers the Python-level `after_in_parent` and `after_in_child` resume handlers, sanitizes the interpreter state in the new process, and finally releases the GIL back to the system via `PyGILState_Release()`. By acquiring the GIL and delegating the fork preparation to Python's native C-API, we guarantee the child process inherits a strictly safe, single-threaded Python memory state. --- lang_lock.c | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lang_lock.c b/lang_lock.c index 106faf9..59f0846 100644 --- a/lang_lock.c +++ b/lang_lock.c @@ -12,6 +12,40 @@ #ifdef MULTI_PERL perl_key last_py_tstate; +#ifndef WIN32 +#include +#include +/* Thread-local GIL state across prepare → parent/child (same thread). */ +static __thread PyGILState_STATE atfork_gstate; + +static void +_atfork_prepare(void) +{ +#if PY_VERSION_HEX >= 0x03070000 + atfork_gstate = PyGILState_Ensure(); + PyOS_BeforeFork(); +#endif +} + +static void +_atfork_parent(void) +{ +#if PY_VERSION_HEX >= 0x03070000 + PyOS_AfterFork_Parent(); + PyGILState_Release(atfork_gstate); +#endif +} + +static void +_atfork_child(void) +{ +#if PY_VERSION_HEX >= 0x03070000 + PyOS_AfterFork_Child(); + PyGILState_Release(atfork_gstate); +#endif +} +#endif + void lang_lock_init() { @@ -23,6 +57,13 @@ lang_lock_init() #endif Py_FatalError("Can't create TSD key for py_tstate"); } +#ifndef WIN32 + static bool atfork_registered = false; + if (!atfork_registered) { + (void) pthread_atfork(_atfork_prepare, _atfork_parent, _atfork_child); + atfork_registered = true; + } +#endif } #else /* MULTI_PERL */