mirror of
https://github.com/python/cpython.git
synced 2026-10-28 10:25:05 +08:00
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:
1 parent
4521718ca5
commit
926b0bf311
4 files changed
+44
-4
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user