Repository navigation
subprocess.run(timeout=...) swallows a signal handler's InterruptedError on macOS in Python 3.15 #160700
Description
Bug report
On macOS, a timed subprocess.run() does not propagate an exception raised by a Python signal handler while the child is still running. In Python 3.15.2 the handler below raises InterruptedError, but run() returns the child's exit status after the child finishes. Python 3.14.10 raises the exception immediately.
import os
import signal
import subprocess
import sys
import time
def handler(signum, frame):
signal.signal(signal.SIGTERM, signal.SIG_IGN)
raise InterruptedError(f"interrupted by signal {signum}")
signal.signal(signal.SIGTERM, handler)
started = time.monotonic()
child = "import os, time; os.kill(os.getppid(), 15); time.sleep(5)"
try:
result = subprocess.run([sys.executable, "-c", child], timeout=60)
print("returned", result.returncode, "after", round(time.monotonic() - started, 2), "seconds")
except BaseException as error:
print("raised", type(error).__name__, "after", round(time.monotonic() - started, 2), "seconds")On macOS 26.7 arm64 (Darwin 25.6.0 arm), repeated five times:
- Python 3.15.2 (
v3.15.2, commit5e58eaad7c58df58dfa8ea549c87996e01dd1077):returned 0 after 5.07–5.08 seconds(5/5). - Python 3.14.10 (commit
b703b2c5e704341d61e367b9b940b77ed11dded0):raised InterruptedError after 0.01–0.03 seconds(5/5). - CPython main at
c436e8d83597c6f8c1bc7e37812c98d46670c29a(Python 3.16.0a1+dev):returned 0 after 5.04–5.08 seconds(5/5).
The expected result is for the InterruptedError raised by the handler to escape the timed wait, as it does in 3.14.10. PEP 475 specifies that when a Python signal handler raises, the interrupted system-call wrapper must fail with that exception: https://peps.python.org/pep-0475/#proposal.
CPython versions tested on:
3.15.2, 3.14.10, and CPython main (built at c436e8d83597c6f8c1bc7e37812c98d46670c29a; Python 3.16.0a1+dev).
Operating systems tested on:
macOS 26.7 arm64.
Additional information
At v3.15.2, Lib/subprocess.py:2279-2282 catches OSError around kq.control() and returns False; Python documents InterruptedError as an OSError subclass, so this catch covers the handler exception and _wait() falls back to polling instead of propagating it. The same catch is present on CPython main at Lib/subprocess.py:2303-2306:
Lines 2303 to 2306 in c436e8d
| try: | |
| events = kq.control(changelist, 1, max(delay, 0)) | |
| except OSError: | |
| return False |
The release code is at
Lines 2279 to 2282 in 5e58eaa
| try: | |
| events = kq.control(changelist, 1, max(delay, 0)) | |
| except OSError: | |
| return False |
Lib/subprocess.py:2279-2282. The existing test_kqueue_control_error only exercises generic OSError fallback with EPERM at Lib/test/test_subprocess.py:4217-4231; it does not distinguish InterruptedError. The select.kqueue.control() documentation specifies that a signal-handler-raised exception is not retried at Doc/library/select.rst:535-538. Popen._internal_poll() has a separate broad OSError catch around waitpid(WNOHANG) at Lib/subprocess.py:2190; it should be audited for the same signal-handler exception issue.
Activity
pythongh-160700: Add NEWS entry