apple / apple/swift-async-algorithms
AsyncShareSequence causes data races
- Dominant language
- Swift
- Stars
- 3.7k
- Forks
- 226
- Avg merge
- 10d 3h
- Merged PRs (30d)
- 1
Description
This comment struck me as false: https://github.com/apple/swift-async-algorithms/blob/6c050d5ef8e1aa6342528460db614e9770d7f804/Sources/AsyncAlgorithms/AsyncShareSequence.swift#L71-L84 and indeed, I can cause data races due to the non-Sendable upstream iterator being sent:
```swift
import AsyncAlgorithms
class MyCoolAsyncSequence: AsyncSequence {
typealias Element = String
struct AsyncIterator: AsyncIteratorProtocol {
weak let parent: MyCoolAsyncSequence?
func next(isolation actor: isolated (any Actor)?) async -> String? {
guard let parent else { return nil }
if let element = parent.buffer.popLast() {
return element
} else {
let (stream, continuation) = AsyncStream.makeStream(of: String.self)
parent.pending.append(continuation)
var iterator = stream.makeAsyncIterator()
return await iterator.next(isolation: actor)
}
}
}
var buffer = [String]()
var pending = [AsyncStream.Continuation]()
func makeAsyncIterator() -> AsyncIterator {
AsyncIterator(parent: self)
}
func send(_ value: String) {
if let continuation = pending.popLast() {
continuation.yield(value)
continuation.finish()
} else {
buffer.append(value)
}
}
}
@main
struct AsyncShareSequenceIsUnsafe {
static func main() async {
let seq = MyCoolAsyncSequence()
let shared = seq.share()
await withDiscardingTaskGroup { group in
for task in 0..<1 {
group.addTask {
for await element in shared {
print(task, ":", element)
}
}
}
for i in 0... {
seq.buffer.append("Defeat small string optimizations \(i)")
}
}
}
}
```
```
swift run
Thread 2 crashed:
0 0x00000001a51ce590 _swift_release_dealloc + 32 in libswiftCore.dylib
1 0x00000001a51cf114 bool swift::RefCounts>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 152 in libswiftCore.dylib
2 closure #1 in closure #1 in static AsyncShareSequenceIsUnsafe.main() + 348 in AsyncShareSequenceIsUnsafe
```
(alternatively, some runs will deadlock. This is another consequence of the same bug; the lack of synchronization between `buffer` and `pending` means that concurrent execution sometimes leaves both arrays full)
Contributor guide
Research direction
Read Sources/AsyncAlgorithms/AsyncShareSequence.swift, especially the comment and implementation around lines 71-84. Reproduce the issue with the Swift example in the report, then inspect how the upstream iterator, buffer, and pending continuations are accessed concurrently. Done means concurrent sharing no longer produces data races or leaves both arrays full and deadlocked.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- swift
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100