Skip to content

Fixed cancel scope crash without a current task on the asyncio backend - #1163

Open
inoue22 wants to merge 3 commits into
agronholm:masterfrom
inoue22:cancel-scope
Open

Fixed cancel scope crash without a current task on the asyncio backend#1163
inoue22 wants to merge 3 commits into
agronholm:masterfrom
inoue22:cancel-scope

Conversation

@inoue22

@inoue22 inoue22 commented Jun 4, 2026

Copy link
Copy Markdown

NOTE Erasing or replacing the contents of this template will result in your pull
request being summarily closed without consideration!

Changes

On the asyncio backend, entering a cancel scope while asyncio.current_task() returns None raised an obscure TypeError: cannot create weak reference to 'NoneType' object. CancelScope.__enter__ used the host task as a key in a WeakKeyDictionary (_task_states[host_task]) without a None check.

This is reached through CancelScope(shield=True) inside cancel_shielded_checkpoint(), which primitives such as CapacityLimiter and Lock rely on, so using any of those while current_task() is None crashes instead of behaving gracefully.

This is inconsistent with the rest of the backend: checkpoint_if_cancelled() and current_effective_deadline() already treat current_task() is None as a benign no-op.

This PR makes the asyncio backend consistent:

  • cancel_shielded_checkpoint() falls back to a plain checkpoint (await sleep(0)) when current_task() is None — with no current task there is nothing to shield from cancellation, mirroring checkpoint_if_cancelled().
  • CancelScope.__enter__ now raises a clear RuntimeError("A cancel scope can only be entered from within a task") instead of the weakref TypeError. The CancelScope docstring documents the new :raises RuntimeError:.

The defect is independent of the Python version (reproduced identically on 3.12, 3.13 and 3.14).

Checklist

If this is a user-facing code change, like a bugfix or a new feature, please ensure that
you've fulfilled the following conditions (where applicable):

  • You've added tests (in tests/) which would fail without your patch
  • You've updated the documentation (in docs/), in case of behavior changes or new features (N/A)
  • You've added a new changelog entry (in docs/versionhistory.rst).

If this is a trivial change, like a typo fix or a code reformatting, then you can ignore
these instructions.

Updating the changelog

If there are no entries after the last release, use **UNRELEASED** as the version.
If, say, your patch fixes issue #123, the entry should look like this:

- Fix big bad boo-boo in task groups
  (`#123 <https://github.com/agronholm/anyio/issues/123>`_; PR by @yourgithubaccount)

If there's no issue linked, just link to your pull request instead by updating the
changelog after you've created the PR.

@inoue22
inoue22 marked this pull request as ready for review June 4, 2026 07:21
@agronholm

Copy link
Copy Markdown
Owner

Just under what circumstances does it make any sense to call cancel_shielded_checkpoint() outside a task?

@inoue22

inoue22 commented Jun 5, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look, and for maintaining AnyIO.

It isn't user code calling it outside a task. anyio's own primitives (locks, semaphores, capacity limiters) call cancel_shielded_checkpoint(), which crashes when current_task() is None; for example CapacityLimiter.acquire(), the default thread limiter in to_thread.run_sync(). Each already pairs it with checkpoint_if_cancelled(), which self-guards that state, so guarding cancel_shielded_checkpoint() against it too is consistent and covers them all rather than special-casing one caller.

On its own, the RuntimeError change would not fix it, since acquire() would still raise and fail the call. The no-op lets acquire() complete; the RuntimeError just makes CancelScope.__enter__ raise A cancel scope can only be entered from within a task instead of TypeError: cannot create weak reference to 'NoneType' object.

@agronholm

Copy link
Copy Markdown
Owner

Let me rephrase: What sensible code would trigger this error?

@agronholm

Copy link
Copy Markdown
Owner

Or, is this only about confusing error messages when said APIs are misused?

@agronholm

Copy link
Copy Markdown
Owner

OK, so I would suggest changing the tests to use callbacks event_loop.call_soon() rather than monkey patching. That would make the tests more realistic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants