Skip to content

Commit d341a65

Browse files
authored
Avoid a potential race condition and use-after-free when calling Test.cancel(). (#1395)
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: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 56a6b79 commit d341a65

File tree

1 file changed

+11
-2
lines changed

1 file changed

+11
-2
lines changed

Sources/Testing/Test+Cancellation.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,19 @@ extension TestCancellable {
8787
/// the current task, test, or test case is cancelled, it records a
8888
/// corresponding cancellation event.
8989
func withCancellationHandling<R>(_ body: () async throws -> R) async rethrows -> R {
90+
let taskReference = _TaskReference()
9091
var currentTaskReferences = _currentTaskReferences
91-
currentTaskReferences[ObjectIdentifier(Self.self)] = _TaskReference()
92+
currentTaskReferences[ObjectIdentifier(Self.self)] = taskReference
9293
return try await $_currentTaskReferences.withValue(currentTaskReferences) {
93-
try await withTaskCancellationHandler {
94+
// Before returning, explicitly clear the stored task. This minimizes
95+
// the potential race condition that can occur if test code creates an
96+
// unstructured task and calls `Test.cancel()` in it after the test body
97+
// has finished.
98+
defer {
99+
_ = taskReference.takeUnsafeCurrentTask()
100+
}
101+
102+
return try await withTaskCancellationHandler {
94103
try await body()
95104
} onCancel: {
96105
// The current task was cancelled, so cancel the test case or test

0 commit comments

Comments
 (0)