https://github.com/python/cpython/commit/48b1e6026b7853913e60c2bc917c6c1ea6307d75
commit: 48b1e6026b7853913e60c2bc917c6c1ea6307d75
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: brettcannon <[email protected]>
date: 2026-10-27T22:12:45Z
summary:

[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 926b0bf3110bf86550ef248cf911e24fc123ec64)

Co-authored-by: Cary Hawkins <[email protected]>

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 9d911e1dcaba61d..758fd5ab1c6f4c3 100644
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -446,7 +446,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 3c47be43a95b722..5015605c88cf605 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -43,7 +43,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,
@@ -780,6 +784,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 fded9a3e849f67c..43024374cbd784f 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -4356,8 +4356,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]

Reply via email to