-
Notifications
You must be signed in to change notification settings - Fork 149
Description
Description
The shared resource (locked_iteratorCreated) is protected by a lock in line 386 (try checkIfCanCreateIterator()) but not protected after that – until it is protect again in line 395 (via makeAsyncIterator()).
This can lead to incosistent state where the flow continues past 386 (locked_iteratorCreated == false) and the intentional crash in makeAsyncIterator() is triggered when another thread sets locked_iteratorCreated to true in the meantime.
With enough threads calling e.g. HTTPBody.ByteChunk(collecting:, upTo:) on the same HTTPBody object, it is pretty easy to produce a crash.
In collect(upTo:) locked_iteratorCreated needs to be protected until after the async sequence was created to ensure consistency (avoiding deadlocks, of course).
My I also suggest instead of the intentional crash in HTTPBody.makeAsyncIterator() (line 346) to return an async throwing sequence which (re-)throws the error thrown from tryToMarkIteratorCreated()?
I think that would honor both the intention that a HTTPBody with IterationBehavior.single may be iterated only once and at the same time the fact that HTTPBody conforms to AsyncSequence.
The same applies to MultipartBody.makeAsyncIterator()
Reproduction
My test case in Test_HTTPBody.swift crashes reliably on my M1 Pro:
func testThreadSafety() async throws {
for testIteration in 0..<100 {
print("Starting test iteration \(testIteration)")
let sut = HTTPBody([HTTPBody.ByteChunk("")], length: .unknown, iterationBehavior: .single)
let collectSuccess = expectation(description: "collectSuccess")
let collectFailure = expectation(description: "collectFailure")
let taskCount = 100
collectFailure.expectedFulfillmentCount = taskCount - 1
for _ in 0..<taskCount {
Task(priority: .high) {
try? await Task.sleep(nanoseconds: 10_000_000) // makes collision a bit more likely from experience
do {
_ = try await HTTPBody.ByteChunk(collecting: sut, upTo: 99)
collectSuccess.fulfill()
} catch {
collectFailure.fulfill()
}
}
}
await fulfillment(of: [collectSuccess, collectFailure], timeout: 10, enforceOrder: false)
}
}
Package version(s)
main branch of swift-openapi-runtime:
.
├── swift-http-types<https://github.com/apple/[email protected]>
└── swift-docc-plugin<https://github.com/apple/[email protected]>
└── swift-docc-symbolkit<https://github.com/apple/[email protected]>
Expected behavior
No crash.
Environment
swift-driver version: 1.87.3 Apple Swift version 5.9.2 (swiftlang-5.9.2.2.56 clang-1500.1.0.2.5)
Target: arm64-apple-macosx14.0
Additional information
I could provide a pull request with a fix (have to find a bit of time for that).