Skip to content

Conversation

@grynspan
Copy link
Contributor

@grynspan grynspan commented Nov 6, 2025

If a test creates an unstructured, non-detached task that continues running after the test has finished and eventually calls Test.cancel(), then it may be able to see a reference to the test's (or test case's) task after it has been destroyed by the Swift runtime.

This PR ensures that the infrastructure under Test.cancel() clears its reference to the test's task before returning. This then minimizes the risk of observing the task after it has been destroyed.

Further work at the Swift runtime level may be required to completely eliminate this race condition, but this change makes it sufficiently narrow that any example I can come up with is contrived.

Checklist:

  • Code and documentation should follow the style of the Style Guide.
  • If public symbols are renamed or modified, DocC references should be updated.

…t.cancel()`.

If a test creates an unstructured, non-detached task that continues running
after the test has finished and eventually calls `Test.cancel()`, then it may be
able to see a reference to the test's (or test case's) task after it has been
destroyed by the Swift runtime.

This PR ensures that the infrastructure under `Test.cancel()` clears its
reference to the test's task before returning. This then minimizes the risk of
observing the task after it has been destroyed.

Further work at the Swift runtime level may be required to completely eliminate
this race condition, but this change makes it sufficiently narrow that any
example I can come up with is contrived.
@grynspan grynspan added this to the Swift 6.x (main) milestone Nov 6, 2025
@grynspan grynspan self-assigned this Nov 6, 2025
@grynspan grynspan added bug 🪲 Something isn't working concurrency 🔀 Swift concurrency/sendability issues labels Nov 6, 2025
Copy link

@ktoso ktoso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this way at least you should not have references to a task which may have been destroyed;

I'm not sure about how the currentTaskReferences is used (doesn't that need some synchronization?), but the clearing here is definitely a good improvement.

@grynspan
Copy link
Contributor Author

grynspan commented Nov 6, 2025

The dictionary is a task-local, so no lock needed there. The elements are each a struct with a lock that guards the actual UnsafeCurrentTask instance. takeUnsafeCurrentTask() acquires the lock, swaps the task with nil, and returns it to the caller.

@grynspan
Copy link
Contributor Author

grynspan commented Nov 6, 2025

Me trying to explain the race condition:

pepe-silvia-v0-dvnwtwovkkke1 jpg

@grynspan grynspan merged commit d341a65 into main Nov 6, 2025
53 of 54 checks passed
@grynspan grynspan deleted the jgrynspan/test-cancel-race-condition branch November 6, 2025 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug 🪲 Something isn't working concurrency 🔀 Swift concurrency/sendability issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants