apple / apple/swift-async-algorithms

AsyncShareSequence causes data races

Open
#392 3 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.