gh-159652: Fix import lock leak when the C stack is nearly exhausted (GH-159660)

When an import failed with a stack overflow RecursionError, releasing the import locks on the error path could itself raise RecursionError, because the cleanup runs in a new C frame. This left the global import lock or a module lock held forever, and later imports in other threads blocked indefinitely.

_imp.acquire_lock() now raises RecursionError when there is not enough stack left to release the lock. importlib calls it before taking any import lock, so this covers the per-module locks too. The module lock weakref callback skips cleanup in that case instead of
reporting an unraisable exception.
This commit is contained in:
Cary Hawkins authored and GitHub committed 2026-10-27 14:43:36 -07:00
1 parent 4521718ca5
commit 926b0bf311
4 files changed
+44 -4

No files matched your search

+6 -1
View File
@@ -440,7 +440,12 @@ def _get_module_lock(name):
lock = _ModuleLock(name)
def cb(ref, name=name):
_imp.acquire_lock()
try:
_imp.acquire_lock()
except RecursionError:
# Not enough stack left to take the import lock safely.
# Leave the dead weakref; _get_module_lock() replaces it.
return
try:
# bpo-31070: Check if another thread created a new lock
# after the previous lock was destroyed
+23 -1
View File
@@ -45,7 +45,11 @@ from test.support import (
Py_GIL_DISABLED,
no_rerun,
force_not_colorized_test_class,
catch_unraisable_exception
catch_unraisable_exception,
infinite_recursion,
run_with_limited_c_stack,
skip_emscripten_stack_overflow,
skip_wasi_stack_overflow
)
from test.support.import_helper import (
forget, make_legacy_pyc, unlink, unload, ready_to_import,
@@ -782,6 +786,24 @@ class ImportTests(unittest.TestCase):
finally:
del sys.path[0]
@cpython_only
@skip_emscripten_stack_overflow()
@skip_wasi_stack_overflow()
@run_with_limited_c_stack(depth=100_000)
def test_acquire_lock_near_c_stack_limit(self):
# _imp.acquire_lock() must refuse to take the import lock when there
# is not enough C stack left to release it again.
def recurse(_):
_imp.acquire_lock()
_imp.release_lock()
list(map(recurse, [None]))
with infinite_recursion(100_000):
with self.assertRaisesRegex(RecursionError,
'acquiring the import lock'):
recurse(None)
self.assertFalse(_imp.lock_held())
@unittest.skipUnless(sys.platform == "win32", "Windows-specific")
def test_dll_dependency_import(self):
from _winapi import GetModuleFileName
@@ -0,0 +1,6 @@
Fix a hang when an import fails with :exc:`RecursionError` because the C
stack is nearly exhausted: releasing the import locks could itself fail and
leave the global import lock or a module lock held forever, so that later
imports in other threads blocked indefinitely. The import system now raises
:exc:`RecursionError` before taking an import lock when there is not enough
stack left to release it.
+9 -2
View File
@@ -4993,8 +4993,15 @@ static PyObject *
_imp_acquire_lock_impl(PyObject *module)
/*[clinic end generated code: output=1aff58cb0ee1b026 input=60e9c1b4ab471ead]*/
{
PyInterpreterState *interp = _PyInterpreterState_GET();
_PyImport_AcquireLock(interp);
PyThreadState *tstate = _PyThreadState_GET();
// Leave enough stack for importlib to release the lock.
if (_Py_ReachedRecursionLimitWithMargin(tstate, 2)) {
_PyErr_SetString(tstate, PyExc_RecursionError,
"maximum recursion depth exceeded while "
"acquiring the import lock");
return NULL;
}
_PyImport_AcquireLock(tstate->interp);
Py_RETURN_NONE;
}