Files
swift-mirror/test/Concurrency/Runtime/async_taskgroup_cancellation_race.swift
Mike Ash 5be22fa7cc [Concurrency] Fix races/overflows in TaskGroup implementation.
statusCompletePendingReadyWaiting(), offer(), and poll() did a one-off compare_exchange_strong which could fail if the group was concurrently cancelled. Put these into loops so that they are retried when needed.

DiscardingTaskGroup creation passed the group result type as the task result type. waitAll() would then use the group result type when collecting task results. Since the task result type is always Void in this case, this would overflow the result buffer if the group result type was larger. This often works as it writes into the free space of the task allocator, but can crash if it happens to be at the end of a page or the group result type is particularly large.

rdar://151663730
2025-05-28 20:58:33 -04:00

60 lines
1.6 KiB
Swift

// RUN: %target-run-simple-swift
// REQUIRES: executable_test
// REQUIRES: concurrency
// REQUIRES: libdispatch
// REQUIRES: concurrency_runtime
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: back_deployment_runtime
// UNSUPPORTED: back_deploy_concurrency
// UNSUPPORTED: freestanding
func unorderedResults<R>(
_ fns: [@Sendable () async -> R]) -> (Task<(), Never>, AsyncStream<R>) {
var capturedContinuation: AsyncStream<R>.Continuation? = nil
let stream = AsyncStream<R> { continuation in
capturedContinuation = continuation
}
guard let capturedContinuation = capturedContinuation else {
fatalError("failed to capture continuation")
}
let task = Task.detached {
await withTaskGroup(of: Void.self) { group in
for fn in fns {
group.addTask {
let _ = capturedContinuation.yield(await fn())
}
}
await group.waitForAll()
}
capturedContinuation.finish()
}
let result = (task, stream)
return result
}
var fns: [@Sendable () async -> String] = [
{
try? await Task.sleep(nanoseconds: .random(in: 0..<50000))
return "hello"
}
]
fns.append(fns[0])
fns.append(fns[0])
// This is a race that will crash or trigger an assertion failure if there's an
// issue. If we get to the end then we pass.
for _ in 0..<1000 {
let (t, s) = unorderedResults(fns)
for try await x in s {
_ = x
if Bool.random() { t.cancel() }
}
}