Skip to content
5 changes: 4 additions & 1 deletion Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,10 @@ 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
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,28 @@ 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)}')

for exc in (SystemExit, KeyboardInterrupt):
Comment thread
YvesDup marked this conversation as resolved.
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())

def test_step_result_future(self):
# If coroutine returns future, task waits on this future.

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 7 additions & 2 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading