Fix Thread Sanitizer SEGV in certificate generation - #17
Open
darioalessandro wants to merge 1 commit into
Open
Conversation
`IdentityCertificate.makeCertificate` used `Certificate.SerialNumber()`, swift-certificates' convenience initialiser. That funnels into its `@inlinable` generic `RandomNumberGenerator.bytes(count:)`, and the specialisation the compiler emits for it is miscompiled under Thread Sanitizer: the instrumentation ends up treating a freshly generated random value as a memory address, so the process takes a SEGV on a garbage pointer inside `__tsan::MemoryAccess`. The report blames `Certificate.SerialNumber.init`, which is misleading — that initialiser is fine on its own, and so is CryptoKit signing on its own. On macOS it surfaces first as `Swift access race ... Location is global 'value witness table for PeerIdentity.SigningKey'`; on the iOS Simulator it is a hard process abort, which made a TSan-enabled suite impossible to run in a host app (Remote Shutter's 619-test suite could not complete). Generate the 20 random bytes directly and use the explicit `init(bytes:)` overload. Semantically identical — 20 random bytes, ASN.1-normalised by that initialiser — while staying out of the miscompiled specialisation. Ruled out along the way: it is not a data race in this code. Serialising certificate construction behind a lock did not fix it (and made macOS abort too); `SerialNumber()` alone, CryptoKit signing alone, and `Certificate.PublicKey`/`PrivateKey` wrapping alone all pass under TSan. Only the full signing path faults. Tests: `swift test --sanitize=thread` goes from 7 warnings + abort to 76 tests clean. Adds a concurrent-generation regression test that reproduces the crash when the fix is reverted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The issue
IdentityCertificate.makeCertificatebuilt its serial number withCertificate.SerialNumber()— swift-certificates' convenience initialiser. That funnels into its@inlinablegenericRandomNumberGenerator.bytes(count:), and the specialisation the compiler emits is miscompiled under Thread Sanitizer: the instrumentation ends up treating a freshly generated random value as a memory address, so the process SEGVs on a garbage pointer inside__tsan::MemoryAccess.x[1] = 0x4155255554020000is RNG output being used as a pointer.Impact: on macOS this first shows up as
Swift access race ... Location is global 'value witness table for PeerIdentity.SigningKey'(7 warnings, tests still pass). On the iOS Simulator it is a hard process abort, which makes a TSan-enabled suite impossible to run in a host app — Remote Shutter's 619-test suite could not complete, so that project's "runs clean under TSan" guarantee had silently lapsed.The fix
Generate the 20 random bytes directly and use the explicit
init(bytes:)overload. Semantically identical — 20 random bytes, ASN.1-normalised by that initialiser — while staying out of the miscompiled specialisation.What this is not
The stack looks like a data race, and it isn't one in this code:
Certificate.SerialNumber()alone passes under TSan.Certificate.PublicKey/Certificate.PrivateKeywrapping alone passes.Certificate.init(...)signing path faults.The blamed frame is an inlined temporary, not where the fault is. Hoisting the argument-list temporaries into locals also made the crash disappear — that confirmed codegen sensitivity, but it is a symptom patch, so it isn't what shipped here.
Root cause is upstream (Swift compiler + TSan instrumentation of that
@inlinablegeneric); this change routes around it.Verification
swift test --sanitize=threadAdds two tests: serial numbers are correctly sized and distinct, and an 8-way concurrent generation test. Reverting the one-line fix makes the suite report the SEGV/race again, so the guard is real.
🤖 Generated with Claude Code