Skip to content

Commit 59cba59

Browse files
authored
gh-108549: fix asyncio.Task cancellation swallowing SystemExit and KeyboardInterrupt (#156309)
1 parent fb2f0bb commit 59cba59

4 files changed

Lines changed: 37 additions & 3 deletions

File tree

Lib/asyncio/tasks.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,9 @@ def __step(self, exc=None):
268268
raise exceptions.InvalidStateError(
269269
f'__step(): already done: {self!r}, {exc!r}')
270270
if self._must_cancel:
271-
if not isinstance(exc, exceptions.CancelledError):
271+
# gh-108549: do not swallow SystemExit and KeyboardInterrupt.
272+
if not isinstance(exc, (exceptions.CancelledError,
273+
SystemExit, KeyboardInterrupt)):
272274
exc = self._make_cancelled_error()
273275
self._must_cancel = False
274276
self._fut_waiter = None

Lib/test/test_asyncio/test_tasks.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,30 @@ async def notmuch():
18911891
self.loop.run_until_complete(task),
18921892
'ko')
18931893

1894+
def test_step_dont_swallow_systemexit_or_keyboardinterrupt(self):
1895+
# see gh-108549: do not swallow SystemExit and KeyboardInterrupt
1896+
# in Task.__step when the current task must be cancelled.
1897+
async def sub_task(exc):
1898+
raise exc
1899+
1900+
async def current_task(exc):
1901+
try:
1902+
await asyncio.create_task(sub_task(exc))
1903+
except exc:
1904+
pass
1905+
except BaseException as e:
1906+
self.fail(f'{exc} is expected, instead of {type(e)}')
1907+
return "ok"
1908+
1909+
for exc in (SystemExit, KeyboardInterrupt):
1910+
with self.subTest(exc):
1911+
t = self.new_task(self.loop, current_task(exc))
1912+
self.assertRaises(exc, self.loop.run_until_complete, t)
1913+
t.cancel()
1914+
test_utils.run_briefly(self.loop)
1915+
self.assertTrue(not t.cancelled())
1916+
self.assertEqual(t.result(), "ok")
1917+
18941918
def test_step_result_future(self):
18951919
# If coroutine returns future, task waits on this future.
18961920

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :class:`asyncio.Task`, when the task has a pending cancellation,
2+
replace **exc** exception with a :exc:`asyncio.CancelledError` unless it is
3+
:exc:`SystemExit` or :exc:`KeyboardInterrupt`, which must propagate unchanged.

Modules/_asynciomodule.c

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3051,8 +3051,13 @@ task_step_impl(asyncio_state *state, TaskObj *task, PyObject *exc)
30513051
if (task->task_must_cancel) {
30523052
assert(exc != Py_None);
30533053

3054-
if (!exc || !PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError)) {
3055-
/* exc was not a CancelledError */
3054+
/* Replace exc with a CancelledError unless it already is one, or
3055+
it is SystemExit/KeyboardInterrupt, which must propagate
3056+
unchanged (gh-108549). */
3057+
if (!exc ||
3058+
(!PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError) &&
3059+
!PyErr_GivenExceptionMatches(exc, PyExc_KeyboardInterrupt) &&
3060+
!PyErr_GivenExceptionMatches(exc, PyExc_SystemExit))) {
30563061
exc = create_cancelled_error(state, (FutureObj*)task);
30573062

30583063
if (!exc) {

0 commit comments

Comments
 (0)