https://github.com/python/cpython/commit/926b0bf3110bf86550ef248cf911e24fc123ec64
commit: 926b0bf3110bf86550ef248cf911e24fc123ec64
branch: main
author: Cary Hawkins <[email protected]>
committer: brettcannon <[email protected]>
date: 2026-10-27T14:43:36-07:00
summary:
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.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-00-30-00.gh-issue-159652.Qk3vLm.rst
M Lib/importlib/_bootstrap.py
M Lib/test/test_import/__init__.py
M Python/import.c
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
index cf0462f25556e6f..9b2a77e883acf19 100644
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -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
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index 6ff688aea61d6a8..2ab61827e696234 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -45,7 +45,11 @@
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 @@ def run():
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
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-00-30-00.gh-issue-159652.Qk3vLm.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-00-30-00.gh-issue-159652.Qk3vLm.rst
new file mode 100644
index 000000000000000..2b213c983b87187
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-00-30-00.gh-issue-159652.Qk3vLm.rst
@@ -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.
diff --git a/Python/import.c b/Python/import.c
index 8f4cec08ef765cd..0f01815c71ed369 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -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;
}
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]