Skip to content

Commit 926b0bf

Browse files
authored
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.
1 parent 4521718 commit 926b0bf

4 files changed

Lines changed: 44 additions & 4 deletions

File tree

‎Lib/importlib/_bootstrap.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,12 @@ def _get_module_lock(name):
440440
lock = _ModuleLock(name)
441441

442442
def cb(ref, name=name):
443-
_imp.acquire_lock()
443+
try:
444+
_imp.acquire_lock()
445+
except RecursionError:
446+
# Not enough stack left to take the import lock safely.
447+
# Leave the dead weakref; _get_module_lock() replaces it.
448+
return
444449
try:
445450
# bpo-31070: Check if another thread created a new lock
446451
# after the previous lock was destroyed

‎Lib/test/test_import/__init__.py‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
Py_GIL_DISABLED,
4646
no_rerun,
4747
force_not_colorized_test_class,
48-
catch_unraisable_exception
48+
catch_unraisable_exception,
49+
infinite_recursion,
50+
run_with_limited_c_stack,
51+
skip_emscripten_stack_overflow,
52+
skip_wasi_stack_overflow
4953
)
5054
from test.support.import_helper import (
5155
forget, make_legacy_pyc, unlink, unload, ready_to_import,
@@ -782,6 +786,24 @@ def run():
782786
finally:
783787
del sys.path[0]
784788

789+
@cpython_only
790+
@skip_emscripten_stack_overflow()
791+
@skip_wasi_stack_overflow()
792+
@run_with_limited_c_stack(depth=100_000)
793+
def test_acquire_lock_near_c_stack_limit(self):
794+
# _imp.acquire_lock() must refuse to take the import lock when there
795+
# is not enough C stack left to release it again.
796+
def recurse(_):
797+
_imp.acquire_lock()
798+
_imp.release_lock()
799+
list(map(recurse, [None]))
800+
801+
with infinite_recursion(100_000):
802+
with self.assertRaisesRegex(RecursionError,
803+
'acquiring the import lock'):
804+
recurse(None)
805+
self.assertFalse(_imp.lock_held())
806+
785807
@unittest.skipUnless(sys.platform == "win32", "Windows-specific")
786808
def test_dll_dependency_import(self):
787809
from _winapi import GetModuleFileName
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix a hang when an import fails with :exc:`RecursionError` because the C
2+
stack is nearly exhausted: releasing the import locks could itself fail and
3+
leave the global import lock or a module lock held forever, so that later
4+
imports in other threads blocked indefinitely. The import system now raises
5+
:exc:`RecursionError` before taking an import lock when there is not enough
6+
stack left to release it.

‎Python/import.c‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4993,8 +4993,15 @@ static PyObject *
49934993
_imp_acquire_lock_impl(PyObject *module)
49944994
/*[clinic end generated code: output=1aff58cb0ee1b026 input=60e9c1b4ab471ead]*/
49954995
{
4996-
PyInterpreterState *interp = _PyInterpreterState_GET();
4997-
_PyImport_AcquireLock(interp);
4996+
PyThreadState *tstate = _PyThreadState_GET();
4997+
// Leave enough stack for importlib to release the lock.
4998+
if (_Py_ReachedRecursionLimitWithMargin(tstate, 2)) {
4999+
_PyErr_SetString(tstate, PyExc_RecursionError,
5000+
"maximum recursion depth exceeded while "
5001+
"acquiring the import lock");
5002+
return NULL;
5003+
}
5004+
_PyImport_AcquireLock(tstate->interp);
49985005
Py_RETURN_NONE;
49995006
}
50005007

0 commit comments

Comments
 (0)