Repository navigation
Python 3.13: PyThreadState_SetAsyncExc might deliver the exception outside of the thread #139622
Copy link
Copy link
Open
Labels
3.13only security fixesonly security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
Bug description:
I have the following naive demo that uses PyThreadState_SetAsyncExc to implement a time-limited interruptible context manager:
import contextlib
import ctypes
import sys
import threading
import time
class Interruptible:
def __init__(self, seconds: float):
self.seconds = seconds
self.finished = False
def __enter__(self):
self._thread = threading.Thread(
target=self._interrupt_in,
args=(threading.get_ident(), self.seconds),
daemon=True,
)
self._thread.start()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.finished = True
def _interrupt_in(self, tid: int, seconds: float) -> None:
time.sleep(self.seconds)
print(f"Slept for {seconds}")
if self.finished:
print("Finished, not interrupting")
return
else:
print("Not finished, interrupting")
ctypes.pythonapi.PyThreadState_SetAsyncExc(
ctypes.c_long(tid), ctypes.py_object(TimeoutError)
)
def busy_loop():
s = time.monotonic()
while time.monotonic() - s < 0.5:
pass
print(f"Python version: {sys.version}")
for i in range(100):
try:
with Interruptible(seconds=0.1):
s = time.monotonic()
while time.monotonic() - s < 0.5:
pass
# NOTE: If we replace above block with the equivalent busy_loop(), it works.
# busy_loop()
except TimeoutError:
print(f"Iteration {i + 1}: ✓ TimeoutError caught")
else:
print(f"Iteration {i + 1}: ✗ TimeoutError NOT raised")
time.sleep(0.01) # Small delay between iterationsIn Python 3.13, this might fail with something like:
❯ python /tmp/demo.py
Python version: 3.13.7 (main, Sep 29 2025, 18:42:10) [GCC 11.4.0]
Slept for 0.1
Not finished, interrupting
Iteration 1: ✓ TimeoutError caught
Slept for 0.1
Not finished, interrupting
Iteration 2: ✓ TimeoutError caught
Slept for 0.1
Not finished, interrupting
Iteration 3: ✓ TimeoutError caught
Slept for 0.1
Not finished, interrupting
Iteration 4: ✓ TimeoutError caught
Slept for 0.1
Not finished, interrupting
Traceback (most recent call last):
File "/tmp/demo.py", line 50, in <module>
while time.monotonic() - s < 0.5:
^^^^^^^^^^^^^^^^^^^^^^^^^^
TimeoutError
Meaning the TimeoutError is raised but not caught by the try/except block. A few other observations:
- If I wrap the "work" inside the context manager to a wrapper function
busy_loop(), it's NOT reproducible. - I tested Python
3.11.13,3.12.11,3.13.7, and3.14.0rc3, it is only reproducible in Python3.13.7.
CPython versions tested on:
3.13, 3.12, 3.14, 3.11
Operating systems tested on:
Linux
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
3.13only security fixesonly security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-C-APItype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Projects
- StatusShow more project fieldsTodo
Activity
yilei commentedon Oct 5, 2025
One plausible related change in 3.13 is #115194
Eclips4 commentedon Oct 5, 2025
@gpshead FYI, this isn't related to asyncio, though name of this function could be confusing.
ZeroIntensity commentedon Oct 6, 2025
Bisecting shows that the regressing PR was #106141 and was fixed by #122934, so this appears to be an issue with the eval breaker rather than
PyThreadState_SetAsyncExc.gpshead commentedon Oct 6, 2025
I'm not sure that PR the bisect shows fixing it was intended to fix this? that might be a side effect of the particular reproducer code vs that change.
ZeroIntensity commentedon Oct 6, 2025
Yeah, definitely not an intentional fix. I think the problem is that the eval breaker is being checked somewhere it shouldn't (or not being checked somewhere it should), and changing when
whilestatements jump apparently fixed that.cjwatson commentedon Jan 27, 2026
I think I'm seeing this in Python 3.14.2 as well, in the test suite of https://github.com/glenfant/stopit. The error is visible in https://bugs.debian.org/1123317. This test looks something like this:
async_raisecallsPyThreadState_SetAsyncExcviactypes. The same approach suggested in this issue (replacing the body of thetryinvariable_duration_func_handling_excwith a call tovariable_duration_func(duration)) seems to work around it.Is it possible that something changed between 3.14.0rc3 and 3.14.2 that would have introduced this bug?
test: the real-runaway bounded_join test in the engine's shape — Pyth…