Skip to content

Potential Crash: HTTPBody.collect(upTo:) is not thread safe #502

Closed
apple/swift-openapi-runtime
#95
@LarsPetersHH

Description

@LarsPetersHH

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/swift-http-types@1.0.2>
└── swift-docc-plugin<https://github.com/apple/swift-docc-plugin@1.3.0>
    └── swift-docc-symbolkit<https://github.com/apple/swift-docc-symbolkit@1.0.0>

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).

Metadata

Metadata

Assignees

Labels

area/generatorAffects: plugin, CLI, config file.kind/bugFeature doesn't work as expected.

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions