https://github.com/python/cpython/commit/e8d685f2e4b0fe0850a25259c4dbdb3938f94635
commit: e8d685f2e4b0fe0850a25259c4dbdb3938f94635
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-22T11:21:28+05:30
summary:

[3.15] gh-158287: Fix blocked asyncio event loop on macOS when child process is 
stopped (GH-158962) (#160087)

gh-158287: Fix blocked asyncio event loop on macOS when child process is 
stopped (GH-158962)
(cherry picked from commit 231311c6f9f2bc78a73a68a1ca4466821fe39755)

Co-authored-by: Johnny Wilson Dougherty <[email protected]>

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 67415bf44730e6..ca13a43c6ab9b8 100644
--- a/Lib/asyncio/unix_events.py
+++ b/Lib/asyncio/unix_events.py
@@ -948,7 +948,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 aab92db872d6a9..041748734573b6 100644
--- a/Lib/test/test_asyncio/test_subprocess.py
+++ b/Lib/test/test_asyncio/test_subprocess.py
@@ -2,6 +2,7 @@
 import signal
 import sys
 import textwrap
+import threading
 import unittest
 import warnings
 from unittest import mock
@@ -1053,6 +1054,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]

Reply via email to