fix(firestore,ios): guard the shared transactions map against concurrent access - #9209
Open
Matador829 wants to merge 1 commit into
Open
fix(firestore,ios): guard the shared transactions map against concurrent access#9209Matador829 wants to merge 1 commit into
Matador829 wants to merge 1 commit into
Conversation
…ent access `transactions` is a file-scope static NSMutableDictionary shared by every in-flight Firestore transaction, but every @synchronized in this file locks the per-transaction `transactionState` object instead of the container. Two transactions beginning at once therefore hold two different locks while mutating the same dictionary from different threads, corrupting its internal hash table. The next lookup dereferences a null bucket pointer and the app dies with EXC_BAD_ACCESS in mdict_index_for_key. Add a dedicated `transactionsLock` covering every access to the container and nothing else: the insert in transactionBegin, the remove in the completion block, invalidate, and the three lookup methods. The lookups previously opened with `@synchronized(transactions[key])`, which is `@synchronized(nil)` -- a silent no-op -- whenever the key is absent, and which read the shared dictionary outside any lock in order to acquire that lock. They now resolve the state under the container lock, release it, and only then lock the state. Lock ordering is state -> container everywhere, and the lookups never hold the container lock while acquiring a state lock, so there is no reverse-order hold-and-wait. The container lock is never held across the semaphore wait or the event dispatch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GhxoXDyQvDTCxak1GSmVpr
|
|
Collaborator
|
Oh hey 👋 - believe it or not this problem is endemic throughout the repository, there are approximately 11 spots where this same pattern happens, including in functions. I've got a PR where I'm working through all of them including this one
Sorry this bit you! |
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.
Description
transactionsinRNFBFirestoreTransactionModuleis a file-scope staticNSMutableDictionaryshared by every in-flight Firestore transaction, but every
@synchronizedin the file locks theper-transaction
transactionStateobject — never the container. Two transactions beginning atonce therefore hold two different locks while mutating the same dictionary from different
threads. That corrupts its internal hash table, and the next lookup dereferences a null bucket
pointer.
This surfaced as a production crash:
EXC_BAD_ACCESS (SIGSEGV), null deref, ~2.4s after launch oniOS 26.6 / iPhone 15 Pro, from an app whose first-run migration fires several
runTransaction()calls in a loop.
Frame 3 is the container insert. The crash log also shows the concurrency directly rather than by
inference — four threads were inside
transactionBegin's block simultaneously:dispatch_asyncevent emitdispatch_semaphore_waitdispatch_semaphore_waitA second, quieter bug in the same file, fixed here too: the three lookup methods open with
When the key is absent that expression is
nil, and@synchronized(nil)is a silent no-op — thelock protects nothing. It also reads the shared dictionary outside any lock in order to acquire the
lock.
The change
A dedicated
transactionsLocknow guards every access to the container and nothing else: the insertin
transactionBegin, the remove in the completion block,invalidate, and the three lookups. Thelookups resolve the state under the container lock, release it, and only then lock the state.
acquiring a state lock, so there is no reverse-order hold-and-wait and no new deadlock.
Related issues
I could not find an existing issue for this (#8715 is a different
transactionBegincrash — niltransactionId). Happy to file one to link against if you'd prefer that for changelog/triage.Release Summary
Fix an iOS crash (
EXC_BAD_ACCESS) when two or more Firestore transactions run concurrently.Checklist
AndroidiOSOther(macOS, web)e2etests added or updated inpackages/**/e2ejesttests added or updated inpackages/**/__tests__Test Plan
Being a data race in native locking, this resists a deterministic regression test, and I want to be
precise about what I did and did not verify:
fix to
RNFBFirestoreTransactionModule.mon25.1.0viapatch-packagein the app that producedthe crash log above. That patched file compiles:
clang -fsyntax-only -fobjc-arc -fmodulesagainst the app's installed Pods headers passes, and the same harness fails on a deliberately
typo'd copy, so the check isn't vacuous.
and unit-tested it: 6 transactions fired synchronously in a burst go from 6 concurrent to 1, and
the test fails without the gate.
clang-format --style=Google -n -Werroris clean on the changed file(and flags a deliberately mangled copy).
.mmhas not been compiled. It importsRNFBFirestoreTurboModules.h, which is codegen output I can't resolve without building themonorepo, so I have not compiled this file locally. I originally expected CI to cover it, but all
the substantive workflows on this PR (
Code Quality Checks,Testing,Testing E2E iOS, …) aresitting at
action_requiredpending maintainer approval, so nothing has compiled it yet —I'd rather say that plainly than let the checklist imply otherwise. What I can say is that the
edits here are the same transformation, line for line, as the
.mversion I did compile, andthey touch only locking — no TurboModule scaffolding. Approving the workflows should settle it;
happy to fix anything they surface.
log as evidence of the failure, not a red/green run.
Note on Android
ReactNativeFirebaseFirestoreTransactionModule.javakeeps its handlers in a plainSparseArray,which is likewise not thread-safe, with no visible synchronization around
put/get/delete/clear. That looks like an analogous issue, though the failure mode differs (JVM data corruption ora lost/duplicated handler rather than a segfault). I've deliberately left it out of scope — I have
no crash evidence for it and didn't want to widen an unverifiable diff. Happy to follow up if you'd
like it addressed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GhxoXDyQvDTCxak1GSmVpr