From 0d0da4ae9f752b1cb5774c89031cde0ccc82ac62 Mon Sep 17 00:00:00 2001 From: cjames23 Date: Sun, 18 Oct 2026 00:48:15 -0700 Subject: [PATCH] gh-159652: Fix import lock leak when the C stack is nearly exhausted 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. --- Lib/importlib/_bootstrap.py | 7 +++++- Lib/test/test_import/__init__.py | 24 ++++++++++++++++++- ...-10-18-00-30-00.gh-issue-159652.Qk3vLm.rst | 6 +++++ Python/import.c | 11 +++++++-- 4 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-18-00-30-00.gh-issue-159652.Qk3vLm.rst diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index 081bb98ec2a9723..3c9d074408f321a 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -498,7 +498,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 17b1e03470dd048..870a461e8091967 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 88d4980991f16a6..1f48cdb5ce3a6ea 100644 --- a/Python/import.c +++ b/Python/import.c @@ -5017,8 +5017,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; }