https://github.com/python/cpython/commit/59cba59b153b2980930999d20ccf97e44fb6edf5
commit: 59cba59b153b2980930999d20ccf97e44fb6edf5
branch: main
author: Duprat <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-09-11T18:06:33+05:30
summary:

gh-108549: fix asyncio.Task cancellation swallowing SystemExit and 
KeyboardInterrupt (#156309)

files:
A Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst
M Lib/asyncio/tasks.py
M Lib/test/test_asyncio/test_tasks.py
M Modules/_asynciomodule.c

diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
index f432cf0afa895a2..8f4dce831c5ccce 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -268,7 +268,9 @@ def __step(self, exc=None):
             raise exceptions.InvalidStateError(
                 f'__step(): already done: {self!r}, {exc!r}')
         if self._must_cancel:
-            if not isinstance(exc, exceptions.CancelledError):
+            # gh-108549: do not swallow SystemExit and KeyboardInterrupt.
+            if not isinstance(exc, (exceptions.CancelledError,
+                                    SystemExit, KeyboardInterrupt)):
                 exc = self._make_cancelled_error()
             self._must_cancel = False
         self._fut_waiter = None
diff --git a/Lib/test/test_asyncio/test_tasks.py 
b/Lib/test/test_asyncio/test_tasks.py
index 9c111da8c27f162..fa3eaef0ac03375 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -1891,6 +1891,30 @@ async def notmuch():
             self.loop.run_until_complete(task),
             'ko')
 
+    def test_step_dont_swallow_systemexit_or_keyboardinterrupt(self):
+        # see gh-108549: do not swallow SystemExit and KeyboardInterrupt
+        # in Task.__step when the current task must be cancelled.
+        async def sub_task(exc):
+            raise exc
+
+        async def current_task(exc):
+            try:
+                await asyncio.create_task(sub_task(exc))
+            except exc:
+                pass
+            except BaseException as e:
+                self.fail(f'{exc} is expected, instead of {type(e)}')
+            return "ok"
+
+        for exc in (SystemExit, KeyboardInterrupt):
+            with self.subTest(exc):
+                t = self.new_task(self.loop, current_task(exc))
+                self.assertRaises(exc, self.loop.run_until_complete, t)
+                t.cancel()
+                test_utils.run_briefly(self.loop)
+                self.assertTrue(not t.cancelled())
+                self.assertEqual(t.result(), "ok")
+
     def test_step_result_future(self):
         # If coroutine returns future, task waits on this future.
 
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst 
b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst
new file mode 100644
index 000000000000000..c6b7605f1b0eb56
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst
@@ -0,0 +1,3 @@
+Fix :class:`asyncio.Task`, when the task has a pending cancellation,
+replace **exc** exception with a :exc:`asyncio.CancelledError` unless it is
+:exc:`SystemExit` or :exc:`KeyboardInterrupt`, which must propagate unchanged.
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index a380f8ac72b32f4..8c90b0b1517ae4e 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -3051,8 +3051,13 @@ task_step_impl(asyncio_state *state, TaskObj *task, 
PyObject *exc)
     if (task->task_must_cancel) {
         assert(exc != Py_None);
 
-        if (!exc || !PyErr_GivenExceptionMatches(exc, 
state->asyncio_CancelledError)) {
-            /* exc was not a CancelledError */
+        /* Replace exc with a CancelledError unless it already is one, or
+        it is SystemExit/KeyboardInterrupt, which must propagate
+        unchanged (gh-108549). */
+        if (!exc ||
+            (!PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError) 
&&
+            !PyErr_GivenExceptionMatches(exc, PyExc_KeyboardInterrupt) &&
+            !PyErr_GivenExceptionMatches(exc, PyExc_SystemExit))) {
             exc = create_cancelled_error(state, (FutureObj*)task);
 
             if (!exc) {

_______________________________________________
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