Skip to content

Commit 48b1e60

Browse files
[3.14] gh-159652: Fix import lock leak when the C stack is nearly exhausted (GH-159660) (GH-160780)
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. (cherry picked from commit 926b0bf) Co-authored-by: Cary Hawkins <hawkinscary23@gmail.com>
1 parent 8a2f9f5 commit 48b1e60

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
@@ -446,7 +446,12 @@ def _get_module_lock(name):
446446
lock = _ModuleLock(name)
447447

448448
def cb(ref, name=name):
449-
_imp.acquire_lock()
449+
try:
450+
_imp.acquire_lock()
451+
except RecursionError:
452+
# Not enough stack left to take the import lock safely.
453+
# Leave the dead weakref; _get_module_lock() replaces it.
454+
return
450455
try:
451456
# bpo-31070: Check if another thread created a new lock
452457
# 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
@@ -43,7 +43,11 @@
4343
Py_GIL_DISABLED,
4444
no_rerun,
4545
force_not_colorized_test_class,
46-
catch_unraisable_exception
46+
catch_unraisable_exception,
47+
infinite_recursion,
48+
run_with_limited_c_stack,
49+
skip_emscripten_stack_overflow,
50+
skip_wasi_stack_overflow
4751
)
4852
from test.support.import_helper import (
4953
forget, make_legacy_pyc, unlink, unload, ready_to_import,
@@ -780,6 +784,24 @@ def run():
780784
finally:
781785
del sys.path[0]
782786

787+
@cpython_only
788+
@skip_emscripten_stack_overflow()
789+
@skip_wasi_stack_overflow()
790+
@run_with_limited_c_stack(depth=100_000)
791+
def test_acquire_lock_near_c_stack_limit(self):
792+
# _imp.acquire_lock() must refuse to take the import lock when there
793+
# is not enough C stack left to release it again.
794+
def recurse(_):
795+
_imp.acquire_lock()
796+
_imp.release_lock()
797+
list(map(recurse, [None]))
798+
799+
with infinite_recursion(100_000):
800+
with self.assertRaisesRegex(RecursionError,
801+
'acquiring the import lock'):
802+
recurse(None)
803+
self.assertFalse(_imp.lock_held())
804+
783805
@unittest.skipUnless(sys.platform == "win32", "Windows-specific")
784806
def test_dll_dependency_import(self):
785807
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
@@ -4356,8 +4356,15 @@ static PyObject *
43564356
_imp_acquire_lock_impl(PyObject *module)
43574357
/*[clinic end generated code: output=1aff58cb0ee1b026 input=60e9c1b4ab471ead]*/
43584358
{
4359-
PyInterpreterState *interp = _PyInterpreterState_GET();
4360-
_PyImport_AcquireLock(interp);
4359+
PyThreadState *tstate = _PyThreadState_GET();
4360+
// Leave enough stack for importlib to release the lock.
4361+
if (_Py_ReachedRecursionLimitWithMargin(tstate, 2)) {
4362+
_PyErr_SetString(tstate, PyExc_RecursionError,
4363+
"maximum recursion depth exceeded while "
4364+
"acquiring the import lock");
4365+
return NULL;
4366+
}
4367+
_PyImport_AcquireLock(tstate->interp);
43614368
Py_RETURN_NONE;
43624369
}
43634370

0 commit comments

Comments
 (0)