From 2baafffeb1edcaae454a22ebae9f2179a18e88d5 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 23 Oct 2026 13:00:52 -0400 Subject: [PATCH 1/3] Reset subinterpreters after fork instead of deleting them. --- Include/internal/pycore_pystate.h | 12 +- Include/internal/pycore_qsbr.h | 2 +- ...26-10-23-12-00-00.gh-issue-139759.fork.rst | 2 + Modules/posixmodule.c | 101 +++++++++--- Python/ceval_gil.c | 20 +-- Python/pylifecycle.c | 9 +- Python/pystate.c | 147 ++++++++---------- Python/qsbr.c | 24 ++- 8 files changed, 188 insertions(+), 129 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-12-00-00.gh-issue-139759.fork.rst diff --git a/Include/internal/pycore_pystate.h b/Include/internal/pycore_pystate.h index 0a764d886c6a31c..1749dc703880a33 100644 --- a/Include/internal/pycore_pystate.h +++ b/Include/internal/pycore_pystate.h @@ -22,8 +22,9 @@ extern "C" { // transitions between "attached" and "detached" on its own PyThreadState. // // The "suspended" states are used to implement stop-the-world pauses and to -// merge biased reference counts on behalf of detached threads. They are only -// used in `--disable-gil` builds. +// merge biased reference counts on behalf of detached threads. They are +// used for global pauses in all builds and per-interpreter pauses in +// `--disable-gil` builds. // They are similar to the "detached" state in that the thread is not allowed // to call most Python APIs. A suspended thread trying to attach marks itself // as "suspended-waiting". Only the thread responsible for suspending it may @@ -161,12 +162,12 @@ extern void _PyThreadState_Suspend(PyThreadState *tstate); // stop the world while holding the thread in the suspended state. // Returns 1 on success, 0 if the thread was not in the "detached" state. extern int _PyThreadState_TrySuspendDetached(PyThreadState *tstate); +#endif // Resume a thread suspended by _PyThreadState_TrySuspendDetached() or a // stop-the-world pause: switch it back to "detached" or "detached-waiting" // and wake it if it is waiting to attach. extern void _PyThreadState_ResumeDetached(PyThreadState *tstate); -#endif // Mark the thread state as "shutting down". This is used during interpreter // and runtime finalization. The thread may no longer attach to the @@ -245,7 +246,9 @@ extern void _PyThreadState_Bind(PyThreadState *tstate); PyAPI_FUNC(PyThreadState *) _PyThreadState_NewBound( PyInterpreterState *interp, int whence); -extern PyThreadState * _PyThreadState_RemoveExcept(PyThreadState *tstate); +extern PyThreadState * _PyThreadState_RemoveExcept( + PyThreadState *tstate, + int is_after_fork); extern void _PyThreadState_DeleteList(PyThreadState *list, int is_after_fork); extern void _PyThreadState_ClearMimallocHeaps(PyThreadState *tstate); @@ -267,7 +270,6 @@ extern PyThreadState * _PyThreadState_Swap( extern PyStatus _PyInterpreterState_Enable(_PyRuntimeState *runtime); #ifdef HAVE_FORK -extern PyStatus _PyInterpreterState_DeleteExceptMain(_PyRuntimeState *runtime); extern void _PySignal_AfterFork(void); #endif diff --git a/Include/internal/pycore_qsbr.h b/Include/internal/pycore_qsbr.h index 91d3415614b154d..5cf6fa65c96a20b 100644 --- a/Include/internal/pycore_qsbr.h +++ b/Include/internal/pycore_qsbr.h @@ -180,7 +180,7 @@ extern void _Py_qsbr_fini(PyInterpreterState *interp); extern void -_Py_qsbr_after_fork(struct _PyThreadStateImpl *tstate); +_Py_qsbr_after_fork(PyInterpreterState *interp); #ifdef __cplusplus } diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-12-00-00.gh-issue-139759.fork.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-12-00-00.gh-issue-139759.fork.rst new file mode 100644 index 000000000000000..a6456665323b69e --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-23-12-00-00.gh-issue-139759.fork.rst @@ -0,0 +1,2 @@ +Fix crashes and hangs when calling :func:`os.fork` with existing +subinterpreters. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index a619de79ca73b26..ff9c202a4f8f795 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -723,7 +723,9 @@ PyOS_BeforeFork(void) _PyImport_AcquireLock(interp); _PyEval_StopTheWorldAll(&_PyRuntime); HEAD_LOCK(&_PyRuntime); - _PyWarnings_BeforeFork(interp); + for (PyInterpreterState *i = PyInterpreterState_Head(); i != NULL; i = i->next) { + _PyWarnings_BeforeFork(i); + } } void @@ -753,6 +755,78 @@ reset_asyncio_state(_PyThreadStateImpl *tstate) tstate->asyncio_running_task = NULL; } +static PyStatus +clear_threads_after_fork(PyThreadState *tstate) +{ + struct after_fork_state { + PyThreadState *tstate; + PyThreadState *deleted; + PyThreadState *running_main; + }; + + size_t count = 0; + for (PyInterpreterState *interp = PyInterpreterState_Head(); interp != NULL; + interp = interp->next) { + count++; + } + struct after_fork_state *states = PyMem_RawCalloc(count, sizeof(*states)); + if (states == NULL) { + return _PyStatus_NO_MEMORY(); + } + + // Unlink all vanished threads before running any finalizers. Keep the + // subinterpreters busy until their dead thread states have been cleared. + size_t next = 1; + for (PyInterpreterState *interp = PyInterpreterState_Head(); interp != NULL; + interp = interp->next) { + size_t index = (interp == tstate->interp) ? 0 : next++; + struct after_fork_state *state = &states[index]; + state->running_main = interp->threads.main; + if (state->running_main != NULL + && state->running_main->thread_id != tstate->thread_id) { + state->running_main = NULL; + } + state->tstate = (index == 0) ? tstate : PyThreadState_New(interp); + if (state->tstate == NULL) { + PyMem_RawFree(states); + return _PyStatus_NO_MEMORY(); + } + state->deleted = _PyThreadState_RemoveExcept(state->tstate, + /*is_after_fork=*/1); + if (index != 0) { + _Py_atomic_store_ptr(&interp->threads.main, state->tstate); + } + Py_ssize_t thread_count = 0; + _Py_FOR_EACH_TSTATE_UNLOCKED(interp, p) { +#ifdef PY_HAVE_THREAD_NATIVE_ID + p->native_thread_id = tstate->native_thread_id; +#endif + if (p->_whence == _PyThreadState_WHENCE_THREADING + || p->_whence == _PyThreadState_WHENCE_THREADING_DAEMON) { + thread_count++; + } + } + _Py_atomic_store_ssize(&interp->threads.count, thread_count); + } + + // Clearing thread states can call destructors, so resume before doing so. + _PyEval_StartTheWorldAll(&_PyRuntime); + _PyThreadState_DeleteList(states[0].deleted, /*is_after_fork=*/1); + for (size_t i = 1; i < count; i++) { + struct after_fork_state *state = &states[i]; + PyInterpreterState *interp = state->tstate->interp; + PyThreadState_Swap(state->tstate); + _PyThreadState_DeleteList(state->deleted, /*is_after_fork=*/1); + run_at_forkers(interp->after_forkers_child, 0); + _Py_atomic_store_ptr(&interp->threads.main, state->running_main); + PyThreadState_Clear(state->tstate); + PyThreadState_Swap(tstate); + PyThreadState_Delete(state->tstate); + } + PyMem_RawFree(states); + return _PyStatus_OK(); +} + void PyOS_AfterFork_Child(void) @@ -774,11 +848,6 @@ PyOS_AfterFork_Child(void) tstate->native_thread_id = PyThread_get_thread_native_id(); #endif -#ifdef Py_GIL_DISABLED - _Py_brc_after_fork(tstate->interp); - _Py_qsbr_after_fork((_PyThreadStateImpl *)tstate); -#endif - // Ideally we could guarantee tstate is running main. _PyInterpreterState_ReinitRunningMain(tstate); @@ -794,32 +863,20 @@ PyOS_AfterFork_Child(void) _Py_jit_debug_mutex = (PyMutex){0}; #endif - // Thread-state cleanup can run finalizers that issue warnings. - _PyWarnings_AfterFork(tstate->interp); - reset_remotedebug_data(tstate); reset_asyncio_state((_PyThreadStateImpl *)tstate); - // Remove the dead thread states. We "start the world" once we are the only - // thread state left to undo the stop the world call in `PyOS_BeforeFork`. - // That needs to happen before `_PyThreadState_DeleteList`, because that - // may call destructors. - PyThreadState *list = _PyThreadState_RemoveExcept(tstate); - _PyEval_StartTheWorldAll(&_PyRuntime); - _PyThreadState_DeleteList(list, /*is_after_fork=*/1); + status = clear_threads_after_fork(tstate); + if (_PyStatus_EXCEPTION(status)) { + goto fatal_error; + } _PyImport_ReInitLock(tstate->interp); _PyImport_ReleaseLock(tstate->interp); _PySignal_AfterFork(); - status = _PyInterpreterState_DeleteExceptMain(runtime); - if (_PyStatus_EXCEPTION(status)) { - goto fatal_error; - } - assert(_PyThreadState_GET() == tstate); - status = _PyPerfTrampoline_AfterFork_Child(); if (_PyStatus_EXCEPTION(status)) { goto fatal_error; diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index 49b2ede4ed3e60f..511e2c125b9ff39 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -618,24 +618,26 @@ PyEval_ReleaseThread(PyThreadState *tstate) } #ifdef HAVE_FORK -/* This function is called from PyOS_AfterFork_Child to re-initialize the - GIL and pending calls lock. */ +/* This function is called from PyOS_AfterFork_Child to re-initialize all GILs. */ PyStatus _PyEval_ReInitThreads(PyThreadState *tstate) { assert(tstate->interp == _PyInterpreterState_Main()); - struct _gil_runtime_state *gil = tstate->interp->ceval.gil; - if (!gil_created(gil)) { - return _PyStatus_OK(); + for (PyInterpreterState *interp = PyInterpreterState_Head(); interp != NULL; + interp = interp->next) { + struct _gil_runtime_state *gil = interp->ceval.gil; + if (interp->ceval.own_gil && gil_created(gil)) { + recreate_gil(gil); + } + _Py_FOR_EACH_TSTATE_UNLOCKED(interp, p) { + p->holds_gil = 0; + p->gil_requested = 0; + } } - recreate_gil(gil); take_gil(tstate); - struct _pending_calls *pending = &tstate->interp->ceval.pending; - _PyMutex_at_fork_reinit(&pending->mutex); - return _PyStatus_OK(); } #endif diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 8888f523559123a..7dbf524e563e76f 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -2357,9 +2357,8 @@ make_pre_finalization_calls(PyThreadState *tstate, int subinterpreters) } /* Stop the world to prevent other threads from creating threads or - * atexit callbacks. On the default build, this is simply locked by - * the GIL. For pending calls, we acquire the dedicated mutex, because - * Py_AddPendingCall() can be called without an attached thread state. + * atexit callbacks. For pending calls, we acquire the dedicated mutex, + * because Py_AddPendingCall() can be called without an attached thread state. */ PyMutex_Lock(&interp->ceval.pending.mutex); _PyEval_StopTheWorldAll(interp->runtime); @@ -2444,7 +2443,7 @@ _Py_Finalize(_PyRuntimeState *runtime) except if wait_for_thread_shutdown() has been cancelled by CTRL+C. We start the world once we are the only thread state left, before we call destructors. */ - PyThreadState *list = _PyThreadState_RemoveExcept(tstate); + PyThreadState *list = _PyThreadState_RemoveExcept(tstate, /*is_after_fork=*/0); for (PyThreadState *p = list; p != NULL; p = p->next) { _PyThreadState_SetShuttingDown(p); } @@ -2825,7 +2824,7 @@ Py_EndInterpreter(PyThreadState *tstate) when they attempt to take the GIL (ex: PyEval_RestoreThread()). */ _PyInterpreterState_SetFinalizing(interp, tstate); - PyThreadState *list = _PyThreadState_RemoveExcept(tstate); + PyThreadState *list = _PyThreadState_RemoveExcept(tstate, /*is_after_fork=*/0); for (PyThreadState *p = list; p != NULL; p = p->next) { _PyThreadState_SetShuttingDown(p); } diff --git a/Python/pystate.c b/Python/pystate.c index 81360f4999849d7..92be077d71ae9d8 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -26,6 +26,7 @@ #include "pycore_stats.h" // FT_STAT_WORLD_STOP_INC() #include "pycore_time.h" // _PyTime_Init() #include "pycore_uniqueid.h" // _PyObject_FinalizePerThreadRefcounts() +#include "pycore_warnings.h" // _PyWarnings_AfterFork() /* -------------------------------------------------------------------------- @@ -319,9 +320,6 @@ _Py_COMP_DIAG_POP &(runtime)->atexit.mutex, \ &(runtime)->audit_hooks.mutex, \ &(runtime)->allocators.mutex, \ - &(runtime)->_main_interpreter.types.mutex, \ - &(runtime)->_main_interpreter.code_state.mutex, \ - &(runtime)->_main_interpreter.dict_state.watcher_mutex, \ } static void @@ -410,15 +408,53 @@ _PyRuntimeState_ReInitThreads(_PyRuntimeState *runtime) for (size_t i = 0; i < Py_ARRAY_LENGTH(locks); i++) { _PyMutex_at_fork_reinit(locks[i]); } -#ifdef Py_GIL_DISABLED + PyThreadState *tstate = current_fast_get(); + PyThread_ident_t ident = tstate->interp->imports.lock.thread; for (PyInterpreterState *interp = runtime->interpreters.head; interp != NULL; interp = interp->next) { + PyMutex *interp_locks[] = { + &interp->types.mutex, + &interp->code_state.mutex, + &interp->dict_state.watcher_mutex, + &interp->ceval.pending.mutex, + &interp->xi.data_lookup.registry.mutex, + &interp->mem_free_queue.mutex, + &interp->asyncio_tasks_lock, + &interp->stoptheworld.mutex, +#ifdef Py_GIL_DISABLED + &interp->atexit.ll_callbacks_lock, + &interp->gc.stats_mutex, + &interp->imports.lazy_mutex, + &interp->codecs.search_path_mutex, + &interp->func_state.mutex, + &interp->cached_objects.interned_mutex, + &interp->cached_objects.descriptor_mutex, + &interp->unique_ids.mutex, + &interp->tlbc_indices.mutex, +#ifdef Py_STATS + &interp->pystats_mutex, +#endif +#endif + }; + for (size_t i = 0; i < Py_ARRAY_LENGTH(interp_locks); i++) { + _PyMutex_at_fork_reinit(interp_locks[i]); + } + _PyRecursiveMutex_at_fork_reinit(&interp->imports.lock, + interp->imports.lock.thread == ident); + _PyWarnings_AfterFork(interp); + if (interp->ceval.pending.handling_thread != NULL + && interp->ceval.pending.handling_thread->thread_id != tstate->thread_id) { + interp->ceval.pending.handling_thread = NULL; + } +#ifdef Py_GIL_DISABLED + _Py_brc_after_fork(interp); + _Py_qsbr_after_fork(interp); for (int i = 0; i < NUM_WEAKREF_LIST_LOCKS; i++) { _PyMutex_at_fork_reinit(&interp->weakref_locks[i]); } - } #endif + } _PyThread_AfterFork(&runtime->threads); @@ -1082,50 +1118,6 @@ PyInterpreterState_Delete(PyInterpreterState *interp) } -#ifdef HAVE_FORK -/* - * Delete all interpreter states except the main interpreter. If there - * is a current interpreter state, it *must* be the main interpreter. - */ -PyStatus -_PyInterpreterState_DeleteExceptMain(_PyRuntimeState *runtime) -{ - struct pyinterpreters *interpreters = &runtime->interpreters; - - PyThreadState *tstate = _PyThreadState_Swap(runtime, NULL); - if (tstate != NULL && tstate->interp != interpreters->main) { - return _PyStatus_ERR("not main interpreter"); - } - - HEAD_LOCK(runtime); - PyInterpreterState *interp = interpreters->head; - interpreters->head = NULL; - while (interp != NULL) { - if (interp == interpreters->main) { - interpreters->main->next = NULL; - interpreters->head = interp; - interp = interp->next; - continue; - } - - // XXX Won't this fail since PyInterpreterState_Clear() requires - // the "current" tstate to be set? - PyInterpreterState_Clear(interp); // XXX must activate? - zapthreads(interp); - PyInterpreterState *prev_interp = interp; - interp = interp->next; - free_interpreter(prev_interp); - } - HEAD_UNLOCK(runtime); - - if (interpreters->head == NULL) { - return _PyStatus_ERR("missing main interpreter"); - } - _PyThreadState_Swap(runtime, tstate); - return _PyStatus_OK(); -} -#endif - static inline void set_main_thread(PyInterpreterState *interp, PyThreadState *tstate) { @@ -2076,22 +2068,39 @@ PyThreadState_DeleteCurrent(void) // these thread states. Instead, it returns the removed thread states as a // linked list. // -// Note that if there is a current thread state, it *must* be the one -// passed as argument. Also, this won't touch any interpreters other -// than the current one, since we don't know which thread state should -// be kept in those other interpreters. +// After fork(), keep all states bound to the surviving OS thread, including +// detached states that may still be referenced by its C stack. PyThreadState * -_PyThreadState_RemoveExcept(PyThreadState *tstate) +_PyThreadState_RemoveExcept(PyThreadState *tstate, int is_after_fork) { assert(tstate != NULL); PyInterpreterState *interp = tstate->interp; _PyRuntimeState *runtime = interp->runtime; -#ifdef Py_GIL_DISABLED assert(runtime->stoptheworld.world_stopped); -#endif HEAD_LOCK(runtime); + if (is_after_fork) { + PyThreadState *list = NULL; + PyThreadState **link = &interp->threads.head; + PyThreadState *prev = NULL; + while (*link != NULL) { + PyThreadState *p = *link; + if (p->thread_id == tstate->thread_id) { + p->prev = prev; + prev = p; + link = &p->next; + } + else { + *link = p->next; + p->next = list; + list = p; + } + } + HEAD_UNLOCK(runtime); + return list; + } + /* Remove all thread states, except tstate, from the linked list of thread states. */ PyThreadState *list = interp->threads.head; @@ -2245,43 +2254,27 @@ tstate_deactivate(PyThreadState *tstate) static int tstate_try_attach(PyThreadState *tstate) { -#ifdef Py_GIL_DISABLED int expected = _Py_THREAD_DETACHED; return _Py_atomic_compare_exchange_int(&tstate->state, &expected, _Py_THREAD_ATTACHED); -#else - assert(tstate->state == _Py_THREAD_DETACHED); - tstate->state = _Py_THREAD_ATTACHED; - return 1; -#endif } static int tstate_try_attach_detached(PyThreadState *tstate, int *state) { -#ifdef Py_GIL_DISABLED assert(*state == _Py_THREAD_DETACHED || *state == _Py_THREAD_DETACHED_WAITING); return _Py_atomic_compare_exchange_int(&tstate->state, state, _Py_THREAD_ATTACHED); -#else - assert(tstate->state == _Py_THREAD_DETACHED); - tstate->state = _Py_THREAD_ATTACHED; - return 1; -#endif } static void tstate_set_detached(PyThreadState *tstate, int detached_state) { assert(_Py_atomic_load_int_relaxed(&tstate->state) == _Py_THREAD_ATTACHED); -#ifdef Py_GIL_DISABLED _Py_atomic_store_int(&tstate->state, detached_state); -#else - tstate->state = detached_state; -#endif } static void @@ -2438,9 +2431,7 @@ void _PyThreadState_SetShuttingDown(PyThreadState *tstate) { _Py_atomic_store_int(&tstate->state, _Py_THREAD_SHUTTING_DOWN); -#ifdef Py_GIL_DISABLED _PyParkingLot_UnparkAll(&tstate->state); -#endif } #ifdef Py_GIL_DISABLED @@ -2452,6 +2443,7 @@ _PyThreadState_TrySuspendDetached(PyThreadState *tstate) return _Py_atomic_compare_exchange_int(&tstate->state, &expected, _Py_THREAD_SUSPENDED); } +#endif void _PyThreadState_ResumeDetached(PyThreadState *tstate) @@ -2476,7 +2468,6 @@ _PyThreadState_ResumeDetached(PyThreadState *tstate) _PyParkingLot_UnparkAll(&tstate->state); } } -#endif // Decrease stop-the-world counter of remaining number of threads that need to // pause. If we are the final thread to pause, notify the requesting thread. @@ -2489,7 +2480,6 @@ decrement_stoptheworld_countdown(struct _stoptheworld_state *stw) } } -#ifdef Py_GIL_DISABLED // Interpreter for _Py_FOR_EACH_STW_INTERP(). For global stop-the-world events, // we start with the first interpreter and then iterate over all interpreters. // For per-interpreter stop-the-world events, we only operate on the one @@ -2623,22 +2613,17 @@ start_the_world(struct _stoptheworld_state *stw) _PyRWMutex_RUnlock(&runtime->stoptheworld_mutex); } } -#endif // Py_GIL_DISABLED void _PyEval_StopTheWorldAll(_PyRuntimeState *runtime) { -#ifdef Py_GIL_DISABLED stop_the_world(&runtime->stoptheworld); -#endif } void _PyEval_StartTheWorldAll(_PyRuntimeState *runtime) { -#ifdef Py_GIL_DISABLED start_the_world(&runtime->stoptheworld); -#endif } void diff --git a/Python/qsbr.c b/Python/qsbr.c index a50d66e3e9e7ae4..b1ae8113fe6a51d 100644 --- a/Python/qsbr.c +++ b/Python/qsbr.c @@ -35,6 +35,7 @@ #include "pycore_interp.h" // PyInterpreterState #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_qsbr.h" +#include "pycore_runtime.h" // _PyRuntimeState #include "pycore_tstate.h" // _PyThreadStateImpl #include "pycore_stats.h" // FT_STAT_QSBR_POLL_INC() @@ -200,11 +201,20 @@ _Py_qsbr_reserve(PyInterpreterState *interp) // If there are no free entries, we pause all threads, grow the array, // and update the pointers in PyThreadState to entries in the new array. if (qsbr == NULL) { - _PyEval_StopTheWorld(interp); + // After fork(), thread states may be created during a global pause. + PyThreadState *tstate = _PyThreadState_GET(); + int world_stopped = tstate != NULL + && interp->runtime->stoptheworld.requester == tstate + && interp->runtime->stoptheworld.world_stopped; + if (!world_stopped) { + _PyEval_StopTheWorld(interp); + } if (grow_thread_array(shared) == 0) { qsbr = qsbr_allocate(shared); } - _PyEval_StartTheWorld(interp); + if (!world_stopped) { + _PyEval_StartTheWorld(interp); + } } // Return an index rather than the pointer because the array may be @@ -272,16 +282,18 @@ _Py_qsbr_fini(PyInterpreterState *interp) } void -_Py_qsbr_after_fork(_PyThreadStateImpl *tstate) +_Py_qsbr_after_fork(PyInterpreterState *interp) { - struct _qsbr_thread_state *this_qsbr = tstate->qsbr; - struct _qsbr_shared *shared = this_qsbr->shared; + unsigned long thread_id = _PyThreadState_GET()->thread_id; + struct _qsbr_shared *shared = &interp->qsbr; _PyMutex_at_fork_reinit(&shared->mutex); for (Py_ssize_t i = 0; i != shared->size; i++) { struct _qsbr_thread_state *qsbr = &shared->array[i].qsbr; - if (qsbr != this_qsbr && qsbr->allocated) { + if (qsbr->allocated && + (qsbr->tstate == NULL || qsbr->tstate->thread_id != thread_id)) { + qsbr->seq = QSBR_OFFLINE; qsbr->tstate = NULL; qsbr->allocated = false; qsbr->freelist_next = shared->freelist; From 4aa86a0116ffc150dfa94eb107e28495e03a05fe Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 23 Oct 2026 13:20:24 -0400 Subject: [PATCH 2/3] Remove bad assertions. Legacy subinterpreters are allowed to call fork() --- Python/ceval_gil.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index 511e2c125b9ff39..5a281f000a4fb3d 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -622,8 +622,6 @@ PyEval_ReleaseThread(PyThreadState *tstate) PyStatus _PyEval_ReInitThreads(PyThreadState *tstate) { - assert(tstate->interp == _PyInterpreterState_Main()); - for (PyInterpreterState *interp = PyInterpreterState_Head(); interp != NULL; interp = interp->next) { struct _gil_runtime_state *gil = interp->ceval.gil; From 0bf7b2eb770ee33c9b262a43d3a906caf298eeb4 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 23 Oct 2026 13:23:40 -0400 Subject: [PATCH 3/3] gh-139759: Test forking with an unclosed subinterpreter Port the regression test from https://github.com/python/cpython/pull/145545. Wait for the child during cleanup to avoid leaving a zombie process. Co-authored-by: Aurel Wildfellner <201101774+aurelwildfellner@users.noreply.github.com> --- Lib/test/test_interpreters/test_stress.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_interpreters/test_stress.py b/Lib/test/test_interpreters/test_stress.py index 56bfc12f951f795..941ff9801cad046 100644 --- a/Lib/test/test_interpreters/test_stress.py +++ b/Lib/test/test_interpreters/test_stress.py @@ -1,9 +1,11 @@ +import os import threading import unittest from test import support from test.support import import_helper from test.support import threading_helper +from test.support import warnings_helper # Raise SkipTest if subinterpreters not supported. _interpreters = import_helper.import_module('_interpreters') from concurrent import interpreters @@ -82,6 +84,26 @@ def test_create_interpreter_no_memory(self): with support.inject_memory_error_cm(0, 1): _interpreters.create() + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + @support.requires_fork() + def test_unclosed_interpreter_on_fork(self): + interp = interpreters.create() + interp.exec("pass") + + fds = os.pipe() + pid = os.fork() + + if pid == 0: + os.close(fds[0]) + os.write(fds[1], b"OK") + os._exit(0) + else: + os.close(fds[1]) + self.addCleanup(os.close, fds[0]) + self.addCleanup(support.wait_process, pid, exitcode=0) + value = os.read(fds[0], 100) + self.assertEqual(value, b"OK") + if __name__ == '__main__': # Test needs to be a package, so we can do relative imports.