https://github.com/python/cpython/commit/231311c6f9f2bc78a73a68a1ca4466821fe39755
commit: 231311c6f9f2bc78a73a68a1ca4466821fe39755
branch: main
author: Johnny Wilson Dougherty <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-21T18:24:24Z
summary:
gh-158287: Fix blocked asyncio event loop on macOS when child process is
stopped (#158962)
files:
A Misc/NEWS.d/next/Library/2026-10-07-17-30-00.gh-issue-158287.xY9zQp.rst
M Lib/asyncio/unix_events.py
M Lib/test/test_asyncio/test_subprocess.py
diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py
index 3a66cee93da4f5..4b41bc7869de1d 100644
--- a/Lib/asyncio/unix_events.py
+++ b/Lib/asyncio/unix_events.py
@@ -968,7 +968,15 @@ def _do_waitpid(self, loop, expected_pid, callback, args):
# This makes the reaping of the child and notification of the
return code
# atomic with respect to the event loop thread.
try:
- os.waitid(os.P_PID, expected_pid, os.WEXITED | os.WNOWAIT)
+ while True:
+ res = os.waitid(os.P_PID, expected_pid, os.WEXITED |
os.WNOWAIT)
+ # On macOS, waitid() with WEXITED incorrectly returns
stopped
+ # child processes (si_code == CLD_STOPPED). Consume the
stop
+ # notification so we wait for actual process termination.
+ if res.si_code == os.CLD_STOPPED:
+ os.waitid(os.P_PID, expected_pid, os.WSTOPPED |
os.WNOHANG)
+ continue
+ break
except ChildProcessError:
# The child process is already reaped
pass
diff --git a/Lib/test/test_asyncio/test_subprocess.py
b/Lib/test/test_asyncio/test_subprocess.py
index f9b4fe53acb134..6e85bdd57c9085 100644
--- a/Lib/test/test_asyncio/test_subprocess.py
+++ b/Lib/test/test_asyncio/test_subprocess.py
@@ -3,6 +3,7 @@
import signal
import sys
import textwrap
+import threading
import unittest
import warnings
from unittest import mock
@@ -1162,6 +1163,63 @@ async def run():
self.loop.run_until_complete(run())
+ @unittest.skipUnless(hasattr(os, 'waitid'), 'needs os.waitid()')
+ @unittest.skipUnless(hasattr(signal, 'SIGSTOP'), 'needs SIGSTOP')
+ def test_stopped_child_does_not_block_event_loop(self):
+ # gh-158287: on macOS, waitid() with WEXITED incorrectly reports
+ # stopped children (si_code == CLD_STOPPED). The child watcher
+ # must not mistake this for process termination and attempt to reap
+ # the child with waitpid(), which blocks the event loop thread.
+ proc = None
+ timed_out = False
+
+ def watchdog():
+ nonlocal timed_out
+ timed_out = True
+ # If the event loop gets stuck in waitpid() because of this
bug,
+ # killing the child process unblocks waitpid() so the test
fails
+ # instead of hanging indefinitely.
+ if proc is not None:
+ if hasattr(signal, 'SIGCONT'):
+ try:
+ os.kill(proc.pid, signal.SIGCONT)
+ except ProcessLookupError:
+ pass
+ try:
+ os.kill(proc.pid, signal.SIGKILL)
+ except ProcessLookupError:
+ pass
+
+ timer = threading.Timer(support.SHORT_TIMEOUT, watchdog)
+ timer.start()
+ try:
+ async def run():
+ nonlocal proc
+ proc = await
asyncio.create_subprocess_exec(*PROGRAM_BLOCKED)
+ try:
+ proc.send_signal(signal.SIGSTOP)
+ # Verify event loop remains responsive while child is
stopped
+ for _ in range(3):
+ await asyncio.sleep(0.05)
+ self.assertFalse(timed_out, "event loop was blocked by
stopped child")
+ self.assertIsNone(proc.returncode)
+ finally:
+ if hasattr(signal, 'SIGCONT'):
+ try:
+ proc.send_signal(signal.SIGCONT)
+ except ProcessLookupError:
+ pass
+ try:
+ proc.kill()
+ except ProcessLookupError:
+ pass
+ await proc.wait()
+
+ self.loop.run_until_complete(run())
+ finally:
+ timer.cancel()
+ timer.join()
+
@unittest.skipUnless(
unix_events.can_use_pidfd(),
"operating system does not support pidfds",
diff --git
a/Misc/NEWS.d/next/Library/2026-10-07-17-30-00.gh-issue-158287.xY9zQp.rst
b/Misc/NEWS.d/next/Library/2026-10-07-17-30-00.gh-issue-158287.xY9zQp.rst
new file mode 100644
index 00000000000000..d7432faa0b7f92
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-07-17-30-00.gh-issue-158287.xY9zQp.rst
@@ -0,0 +1 @@
+Fix a bug in :mod:`asyncio` where stopping a child process (such as with
``SIGSTOP``) on macOS caused the event loop to block indefinitely.
_______________________________________________
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]