Conversation
…est race CI was hitting a documented race in `demangledNode(for:in:)` where the cache-miss branch wrote to `demangledNodeBySymbol` without synchronization. When sibling test suites ran in parallel, concurrent writers corrupted the Dictionary's internal layout, surfacing as SIGSEGV in the hash lookup or as downstream `.invalidContextDescriptor` when subsequently reading trashed bytes as context-descriptor flags. - SymbolIndexStore: decorate `demangledNodeBySymbol` with `@Mutex` so the generated `_modify` accessor keeps `dict[key] = value` atomic. - GenericSpecializationTests: fold the API tests into the same suite and mark it `.serialized`. - macOS.yml: pass `--no-parallel` to both `swift test` invocations and drop the merged-away `GenericSpecializerAPITests` filter alternative.
There was a problem hiding this comment.
Code Review
This pull request introduces thread safety to the demangledNodeBySymbol dictionary using a @Mutex property wrapper and refactors the test suite by merging GenericSpecializerAPITests into GenericSpecializationTests with serialized execution. The review feedback highlights potential performance bottlenecks when using @Mutex with value types like Dictionary due to copy-on-write behavior and suggests ensuring atomic updates to prevent redundant operations.
| @Mutex | ||
| private(set) var demangledNodeBySymbol: [Symbol: Node] = [:] |
There was a problem hiding this comment.
While @Mutex correctly synchronizes access to demangledNodeBySymbol to prevent data races, using a property wrapper on a Dictionary (a value type) can have performance implications. In Swift, any mutation to a property-wrapped value type (like the subscript assignment in setDemangledNode) typically involves a read-modify-write cycle that copies the entire collection if it's not uniquely referenced. For a large symbol index, this could become a bottleneck. Additionally, the check-then-set pattern in demangledNode(for:in:) is not atomic across the mutex boundary, which might lead to redundant demangling operations. If the @Mutex implementation provides a withLock method on its projected value, using it would allow for more efficient, atomic updates.
There was a problem hiding this comment.
Pull request overview
This PR addresses a documented CI-only data race in SymbolIndexStore.demangledNode(for:in:) by adding synchronization around the shared demangle cache and reducing test parallelism to prevent cross-suite corruption during swift test.
Changes:
- Add
@Mutexprotection toSymbolIndexStore.Storage.demangledNodeBySymbol. - Fold the former
GenericSpecializerAPITestsintoGenericSpecializationTestsand serialize the suite. - Disable test parallelism in macOS CI (
swift test --no-parallel) and update test filters accordingly.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
Tests/SwiftInterfaceTests/GenericSpecializationTests.swift |
Merges API tests into the existing suite and marks it serialized to avoid concurrency-related flakiness. |
Sources/MachOSymbols/SymbolIndexStore.swift |
Synchronizes the shared demangled-node cache to prevent concurrent dictionary mutation/read races. |
.github/workflows/macOS.yml |
Disables parallel test execution and removes the merged-away suite from the filter list. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…lization _readRelativeDescriptors now uses RelativeIndirectablePointer<Descriptor, Pointer<Descriptor>> instead of RelativeDirectPointer<Descriptor>. Some Swift section entries are indirect on CI binaries; treating them as direct pointers reads the wrong offset and is the likely root cause of the release-mode invalidContextDescriptor failure (the prior SymbolIndexStore race fix did not clear release CI). Adds retroactive LayoutProtocol conformances for RelativeIndirectPointer, RelativeIndirectablePointer, and RelativeIndirectablePointerIntPair so that AnyLocatableLayoutWrapper accepts them. Also reverts --no-parallel on both test steps and @suite(.serialized) on GenericSpecializationTests to check whether the pointer-type change is sufficient on its own. @Mutex on demangledNodeBySymbol is kept as independent concurrency hardening.
Replace the Indirectable-as-everything workaround from ab0d033 with ABI-accurate wrappers for each Swift section entry type, matching swift/include/swift/ABI/Metadata.h: - TypeMetadataRecord (Metadata.h:2720) — struct around RelativeDirectPointerIntPair<ContextDescriptorWrapper, TypeReferenceKind>. Resolution branches on the 2-bit TypeReferenceKind like TargetTypeMetadataRecord::getContextDescriptor: direct → RelativeDirectPointer, indirect → RelativeIndirectPointer, ObjC kinds → nil (mirroring the runtime nullptr fallback at Metadata.h:2753). - ProtocolRecord (Metadata.h:2766) — struct around RelativeIndirectablePointerIntPair<ProtocolDescriptor?, Bit, Pointer<…>>. Optional Pointee matches nullable=true at MetadataRef.h:109; the reserved low-bit tag is kept as Bit (runtime reads only the pointer at MetadataLookup.cpp:821). - __swift5_proto — left on RelativeDirectPointer<Descriptor> on both ends to match the naked typealias at Metadata.h:3054. Infra: - Add RelativeDirectPointerIntPair / TargetRelativeDirectPointerIntPair mirroring RelativePointer.h:575-619 (low 2 bits = int, no indirect bit — distinct from the existing indirectable variant). - MachOFile and MachOImage readers now throw strictly (no try?), symmetric across ends. - Drop retroactive LayoutProtocol conformances that are no longer reached.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @Mutex | ||
| private(set) var demangledNodeBySymbol: [Symbol: Node] = [:] | ||
|
|
There was a problem hiding this comment.
@Mutex on demangledNodeBySymbol alone may not eliminate the race if call sites mutate/read the Dictionary via subscripting (which can go through an in-place _modify access). To guarantee the lock is always held (as SharedCache does for storageByIdentifier), access this property via the wrapper’s withLockUnchecked (e.g. _demangledNodeBySymbol.withLockUnchecked { ... }) for both reads in demangledNode(for:in:) and writes in setDemangledNode(_:,for:) / setDemangledNodeBySymbol(_:).
| swift test \ | ||
| -c debug \ | ||
| --build-path .build-test-debug \ | ||
| --filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|GenericSpecializerAPITests|MultiPayloadEnumTests|MetadataReaderDemanglingTests)(/|$)' | ||
| --filter '\.(SymbolTestsCoreDumpSnapshotTests|SymbolTestsCoreInterfaceSnapshotTests|SymbolTestsCoreCoverageInvariantTests|STCoreE2ETests|STCoreTests|GenericSpecializationTests|MultiPayloadEnumTests|MetadataReaderDemanglingTests)(/|$)' | ||
|
|
There was a problem hiding this comment.
PR description says CI should pass --no-parallel to swift test, but these invocations still run with default parallelism. If disabling parallelism is required to avoid cross-suite races, add --no-parallel here (or update the PR description if that’s no longer the approach).
| } | ||
| } | ||
|
|
||
| // MARK: - GenericSpecializer API Tests | ||
|
|
||
| @Suite | ||
| struct GenericSpecializerAPITests { | ||
|
|
||
| @Test func makeRequest() async throws { | ||
| let machO = MachOImage.current() |
There was a problem hiding this comment.
PR description mentions marking the merged GenericSpecialization test suite as .serialized, but this file still has no @Suite(.serialized) annotation on the suite type. If serialization is still intended (to avoid intra-suite parallelism), add the attribute; otherwise update the PR description to match the implementation.
Summary
1. Data race fix —
SymbolIndexStore(commite61952b)Guard
Storage.demangledNodeBySymbolwith@Mutex— closes the race documented inKNOWN_ISSUES.mdunder "SymbolIndexStore.demangledNode(for:in:)data race under parallel tests". Sibling test suites had been corrupting the unsynchronizedDictionary, surfacing non-deterministically as SIGSEGV in the hash lookup, or later as.invalidContextDescriptorwhen the trashed bytes were reinterpreted as context-descriptor flags.With the mutex in place,
--no-paralleland@Suite(.serialized)are no longer needed — reverted in67a2824.2. Swift section reader ABI alignment (commit
67a2824)Replace the generic
_readRelativeDescriptorsapproach with ABI-accurate wrappers for each section entry type, mirroringswift/include/swift/ABI/Metadata.h:TypeMetadataRecord(Metadata.h:2720) — struct overRelativeDirectPointerIntPair<ContextDescriptorWrapper, TypeReferenceKind>. Resolution branches on the 2-bitTypeReferenceKindexactly likeTargetTypeMetadataRecord::getContextDescriptordoes:DirectTypeDescriptor→RelativeDirectPointerIndirectTypeDescriptor→RelativeIndirectPointerDirectObjCClassName/IndirectObjCClass→nil(mirrors the runtimenullptrfallback atMetadata.h:2753, which notes these kinds "are just never used in these lists")ProtocolRecord(Metadata.h:2766) — struct overRelativeIndirectablePointerIntPair<ProtocolDescriptor?, Bit, Pointer<…>>.Optionalpointee matchesnullable=trueatMetadataRef.h:109; the reserved low-bit tag is exposed viaBit(runtime ignores it — seeMetadataLookup.cpp:821only callinggetPointer()).__swift5_proto— left onRelativeDirectPointer<ProtocolConformanceDescriptor>on both ends to match the nakedusingtypealias atMetadata.h:3054(no IntPair, no indirect).Infrastructure
RelativeDirectPointerIntPair/TargetRelativeDirectPointerIntPairmirroringswift/include/swift/Basic/RelativePointer.h:575-619(low 2 bits = int value, no indirect bit — distinct from the existingRelativeIndirectablePointerIntPair).MachOFile.SwiftandMachOImage.Swiftreaders now throw strictly (notry?), symmetric across ends.LayoutProtocolconformances that are no longer reached.Test plan
swift buildacross the full packageSymbolIndexStore.demangledNode(for:in:)race out of "deferred" inKNOWN_ISSUES.md