diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index bdfd03b1e58f62..8593d19f25f9f0 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -22,6 +22,7 @@ import textwrap import traceback import warnings +import ctypes from unittest import mock from test import lock_tests @@ -412,6 +413,39 @@ def run(self): t.join() # else the thread is still running, and we have no way to kill it + @cpython_only + @unittest.skipIf(not hasattr(signal, "pthread_kill"), "requires pthread_kill (Unix only)") + def test_PyThreadState_SetAsyncExc_interrupts_sleep(self): + set_async_exc = ctypes.pythonapi.PyThreadState_SetAsyncExc + set_async_exc.argtypes = (ctypes.c_ulong, ctypes.py_object) + + class AsyncExc(Exception): + pass + + exception = ctypes.py_object(AsyncExc) + signal.signal(signal.SIGUSR1, lambda *_: None) + worker_started = threading.Event() + worker_finished = threading.Event() + + def worker(): + tid = threading.get_ident() + worker_started.set() + try: + time.sleep(10) # blocked in EINTR retry path + except AsyncExc: + worker_finished.set() + + t = threading.Thread(target=worker) + t.start() + worker_started.wait() + time.sleep(0.2) + result = set_async_exc(t.ident, exception) + self.assertEqual(result, 1) + signal.pthread_kill(t.ident, signal.SIGUSR1) + worker_finished.wait(timeout=3) + self.assertTrue(worker_finished.is_set()) + t.join(timeout=3) + def test_limbo_cleanup(self): # Issue 7481: Failure to start thread should cleanup the limbo map. def fail_new_thread(*args, **kwargs): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-23-45-41.gh-issue-144748.Y3ZHWs.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-23-45-41.gh-issue-144748.Y3ZHWs.rst new file mode 100644 index 00000000000000..59567527521b48 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-23-45-41.gh-issue-144748.Y3ZHWs.rst @@ -0,0 +1,2 @@ +Fix PyThreadState_SetAsyncExc not interrupting threads blocked in EINTR +retry paths such as time.sleep(). diff --git a/Python/ceval_gil.c b/Python/ceval_gil.c index 88cc66e97f3424..e4ecf66843d7ea 100644 --- a/Python/ceval_gil.c +++ b/Python/ceval_gil.c @@ -1050,6 +1050,15 @@ _PyEval_MakePendingCalls(PyThreadState *tstate) return res; } + /* Check for async exceptions (PyThreadState_SetAsyncExc) */ + if (tstate->async_exc != NULL) { + PyObject *exc = tstate->async_exc; + tstate->async_exc = NULL; + PyErr_SetNone(exc); + Py_DECREF(exc); + return -1; + } + return 0; }