Fix thread-safety crash in Matcher under parallel test execution#138
Closed
fortmarek wants to merge 2 commits intoKolos65:mainfrom
Closed
Fix thread-safety crash in Matcher under parallel test execution#138fortmarek wants to merge 2 commits intoKolos65:mainfrom
fortmarek wants to merge 2 commits intoKolos65:mainfrom
Conversation
The `Matcher` singleton's `matchers` array is accessed without synchronization. Under Swift Testing's parallel execution, concurrent `register()` writes and `comparator()` reads from multiple test `init()` methods cause data races that crash the test process. Observed crash sites: - `Matcher.register<A>(_:)` — concurrent appends to the array - `closure Kolos65#1 in Parameter.eraseToGenericValue()` — concurrent reads via `Matcher.comparator(for:)` while another thread appends Add an NSLock to synchronize all matchers array access. The lock is applied at the leaf level (append and snapshot-read) to avoid re-entrancy in the Sequence comparator path which calls `comparator(by:)` → `comparator(for: T.Element.self)` → `comparator(by:)`.
2 tasks
Replace manual NSLock usage with the existing LockedValue wrapper to make the synchronization more robust and harder to regress. LockedValue already uses NSRecursiveLock which handles the re-entrant comparator lookup path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3516883 to
045a9ed
Compare
Contributor
Author
|
Note on |
Kolos65
approved these changes
Feb 12, 2026
Owner
|
Merging in #139 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
NSLockto synchronize concurrent access to theMatchersingleton'smatchersarrayProblem
When using Swift Testing (which runs test
init()methods in parallel), theMatchersingleton crashes due to unsynchronized concurrent access to itsmatchers: [MatcherType]array.We observed two distinct crash sites in CI across hundreds of test runs in the tuist/tuist repository:
Crash 1:
Matcher.register<A>(_:)Multiple test structs call
Matcher.register()in theirinit()method. Under parallel execution, concurrentappend()calls to the sharedmatchersarray cause a data race:The crash manifests as a process-level crash (line_number: 0, path: "") that kills all tests in the xctest worker process. All tests assigned to that process report as failed with 0.000 second duration.
Crash 2:
closure #1 in Parameter.eraseToGenericValue()For mocked generic methods like
readValue<Value>(), the macro type-erases parameters viaeraseToGenericValue(). The erasure closure captures the concrete type and later callsMatcher.comparator(for: Value.self). Since the generic context has no Equatable constraint, this resolves at compile time to the non-Equatable overload, which reads thematchersarray.Full crash chain from CI:
The crash is
fatalError(noComparatorMessage)—Matcher.comparator(for: Value.self)returnsnilbecause thematchersarray is in a corrupted state from a concurrent write on another thread.Root cause
The
matchersarray is a plain[MatcherType]with no synchronization:Concurrent reads and writes to a Swift Array are undefined behavior and cause crashes.
Fix
Add an
NSLockto synchronize allmatchersarray accesses. The lock is applied at the leaf level — directly aroundappend()calls and as a snapshot-copy before iteration — to avoid re-entrancy in the Sequence comparator path (comparator(by:)→comparator(for: T.Element.self)→comparator(by:)).Test plan
🤖 Generated with Claude Code