From 93d0581f62deb96f825fc7ba5483bf288e2cff66 Mon Sep 17 00:00:00 2001 From: Griffin Date: Wed, 28 Oct 2026 20:51:01 -0700 Subject: [PATCH 1/4] fix: Clean up refused native thread starts --- Lib/test/test_threading.py | 76 +++++++++++++++++++++++++++++++------- Lib/threading.py | 8 ++-- 2 files changed, 68 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 96b43936be92cda..0c552c9bf19260e 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -463,20 +463,70 @@ def catch_exception(): self.assertIsInstance(caught_exception, InjectedException) + @requires_subprocess() def test_limbo_cleanup(self): - # Issue 7481: Failure to start thread should cleanup the limbo map. - def fail_new_thread(*args, **kwargs): - raise threading.ThreadError() - _start_joinable_thread = threading._start_joinable_thread - threading._start_joinable_thread = fail_new_thread - try: - t = threading.Thread(target=lambda: None) - self.assertRaises(threading.ThreadError, t.start) - self.assertFalse( - t in threading._limbo, - "Failed to cleanup _limbo map on failure of Thread.start().") - finally: - threading._start_joinable_thread = _start_joinable_thread + # Issue 7481: Failure to start a thread must clean up the limbo map. + code = """if 1: + import builtins + import sys + import threading + + error = getattr(builtins, sys.argv[1])() + def audit(event, args): + if event == '_thread.start_joinable_thread': + raise error + + sys.addaudithook(audit) + thread = threading.Thread() + try: + thread.start() + except BaseException as caught: + assert caught is error + else: + raise AssertionError('Thread.start() did not raise') + + assert thread._os_thread_handle.ident == 0 + assert not thread._started.is_set() + assert thread not in threading._limbo + assert thread not in threading.enumerate() + """ + for exception in ('RuntimeError', 'KeyboardInterrupt', 'SystemExit'): + with self.subTest(exception=exception): + assert_python_ok('-c', code, exception) + + @requires_subprocess() + def test_start_interrupted_after_native_start(self): + code = """if 1: + import sys + import threading + from test.support import SHORT_TIMEOUT, threading_helper + + def interrupt(frame, event, arg): + if event == 'c_return' and arg is threading._start_joinable_thread: + raise KeyboardInterrupt + + completed = threading.Event() + thread = threading.Thread(target=completed.set) + # Hold the child before it signals successful startup. + with thread._started._cond: + sys.setprofile(interrupt) + try: + thread.start() + except KeyboardInterrupt: + pass + else: + raise AssertionError('Thread.start() did not raise') + finally: + sys.setprofile(None) + assert thread._os_thread_handle.ident + assert thread in threading._limbo + + assert thread._started.wait(SHORT_TIMEOUT) + threading_helper.join_thread(thread) + assert completed.is_set() + """ + _, _, stderr = assert_python_ok('-c', code) + self.assertEqual(stderr, b'') def test_finalize_running_thread(self): # Issue 1402: the PyGILState_Ensure / _Release functions may be called diff --git a/Lib/threading.py b/Lib/threading.py index 507d40a1baa275f..df96f8a0ab34d95 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -1141,9 +1141,11 @@ def start(self): # Start joinable thread _start_joinable_thread(self._bootstrap, handle=self._os_thread_handle, daemon=self.daemon) - except Exception: - with _active_limbo_lock: - del _limbo[self] + except BaseException: + # The native call can be interrupted after creating the OS thread. + if not self._os_thread_handle.ident: + with _active_limbo_lock: + del _limbo[self] raise self._started.wait() # Will set ident and native_id From 14224f9cd51c900ae444d014063cbf81264f551b Mon Sep 17 00:00:00 2001 From: Griffin Date: Wed, 28 Oct 2026 21:03:18 -0700 Subject: [PATCH 2/4] refactor: Fit thread test condition to PEP 8 --- Lib/test/test_threading.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index 0c552c9bf19260e..97132c5e9ce7b1b 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -502,7 +502,8 @@ def test_start_interrupted_after_native_start(self): from test.support import SHORT_TIMEOUT, threading_helper def interrupt(frame, event, arg): - if event == 'c_return' and arg is threading._start_joinable_thread: + if (event == 'c_return' + and arg is threading._start_joinable_thread): raise KeyboardInterrupt completed = threading.Event() From 7de11820ded6d976999172116d9d5ebd45fb840c Mon Sep 17 00:00:00 2001 From: Griffin Date: Wed, 28 Oct 2026 21:14:24 -0700 Subject: [PATCH 3/4] gh-160864: Note refused native thread startup cleanup --- .../next/Library/2026-10-28-21-13-15.gh-issue-160864.3924f7.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-28-21-13-15.gh-issue-160864.3924f7.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-28-21-13-15.gh-issue-160864.3924f7.rst b/Misc/NEWS.d/next/Library/2026-10-28-21-13-15.gh-issue-160864.3924f7.rst new file mode 100644 index 000000000000000..fd74134e02b16d3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-28-21-13-15.gh-issue-160864.3924f7.rst @@ -0,0 +1,2 @@ +Remove unstarted threads from the threading registry when an audit hook aborts +native thread creation with :exc:`KeyboardInterrupt` or :exc:`SystemExit`. From 785af1a8d64c71309f77e4278f5dea2da8a0f92b Mon Sep 17 00:00:00 2001 From: Griffin Date: Wed, 28 Oct 2026 21:45:40 -0700 Subject: [PATCH 4/4] gh-160864: Retry CI after known sampling flake ASan hit gh-160334 in the unchanged collapsed-stack profiler test. The approved fix is tracked in gh-160542. Retry the same source because this account cannot rerun upstream jobs.