Skip to content

[cloud_firestore]: iOS SIGABRT heap corruption — _transactions NSMutableDictionary mutated concurrently by parallel runTransaction calls #18417

Description

@josephysicist1

Is there an existing issue for this?

  • I have searched the existing issues.

Which plugins are affected?

Other

Which platforms are affected?

iOS

Description

Running many FirebaseFirestore.runTransaction calls concurrently on iOS crashes the app with SIGABRT (malloc heap-corruption abort, ___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED).

Root cause (from the symbolicated crash + plugin source):

Every native transaction attempt invokes the started: / ended: listeners from Firestore's transaction worker queue. Those listeners mutate the plugin's shared _transactions NSMutableDictionary with no synchronization:

NSMutableDictionary is not thread-safe. When enough transactions run in parallel (contention retries re-enter started:), two threads mutate the dictionary at the same time; a rehash then trips malloc's heap-consistency check and the process aborts.

In our crash report the crashed thread is inside the started: insert (frames: RunUpdateBlockFLTTransactionStreamHandler.m:55-[__NSDictionaryM setObject:forKeyedSubscript:]mdict_rehashdabort) while the main thread is simultaneously servicing another transaction's event sink (FLTTransactionStreamHandler.m:74, the DEADLINE_EXCEEDED path) — i.e. at least two transaction attempts were interacting with the shared state at the same moment.

Same crash signature as #16899 (closed as stale for lack of a repro) and likely related to #9527. The unsynchronized code is still present on current main (links above) — we hit it on cloud_firestore 5.6.12.

Suggested fix: guard _transactions with a serial dispatch queue / os_unfair_lock / @synchronized, or confine all mutations to a single queue.

Reproducing the issue

Fire ~100+ transactions concurrently (don't await them individually). Targeting the same document makes it reproduce faster because contention retries amplify the parallelism:

final db = FirebaseFirestore.instance;
final ref = db.collection('stress').doc('same-doc');
for (var i = 0; i < 150; i++) {
  // Intentionally not awaited — all transactions run in parallel.
  db.runTransaction((tx) async {
    final snap = await tx.get(ref);
    tx.set(ref, {'n': ((snap.data()?['n'] as int?) ?? 0) + 1});
  });
}

On a physical device this crashes within seconds (we hit it repeatedly on TestFlight builds). Our production trigger: a quiz app recording one transaction per answered question in a fire-and-forget loop at quiz completion (~120 questions → ~240 in-flight transactions including retries).

Firebase Core version

3.15.2

Flutter Version

3.44.1 (stable)

Relevant Log Output

Incident Identifier: D39359FB-877D-4C5F-9807-F25DD0BDD88C
Distributor ID:      com.apple.TestFlight
Hardware Model:      iPhone16,1
Process:             Runner [39185]
Path:                /private/var/containers/Bundle/Application/BFB49DAE-C3B4-4A27-A9EF-F50BD3F72BC9/Runner.app/Runner
Identifier:          com.atacmd.atacMdMobile
Version:             1.1.3 (14)
AppStoreTools:       17F106
AppVariant:          1:iPhone16,1:26
Beta:                YES
Code Type:           ARM-64 (Native)
Role:                Foreground
Parent Process:      launchd [1]
Coalition:           com.atacmd.atacMdMobile [9289]

Date/Time:           2026-07-02 14:25:53.4758 +0300
Launch Time:         2026-07-02 14:15:10.5106 +0300
OS Version:          iPhone OS 26.5 (23F77)
Release Type:        User
Baseband Version:    3.50.08
Report Version:      104

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Termination Reason: SIGNAL 6 Abort trap: 6
Terminating Process: Runner [39185]

Triggered by Thread:  35



Thread 35 name:
Thread 35 Crashed:
0   libsystem_kernel.dylib        	0x000000024b0d21d0 __pthread_kill + 8 (:-1)
1   libsystem_pthread.dylib       	0x00000001fb14b7dc pthread_kill + 268 (pthread.c:1721)
2   libsystem_c.dylib             	0x00000001a7b05c98 abort + 148 (abort.c:122)
3   libsystem_malloc.dylib        	0x00000001a76b4758 malloc_vreport + 892 (malloc_printf.c:251)
4   libsystem_malloc.dylib        	0x00000001a76b43d0 malloc_report + 64 (malloc_printf.c:290)
5   libsystem_malloc.dylib        	0x00000001a76a80bc ___BUG_IN_CLIENT_OF_LIBMALLOC_POINTER_BEING_FREED_WAS_NOT_ALLOCATED + 76 (malloc_common.c:179)
6   CoreFoundation                	0x000000019bf7559c mdict_rehashd + 288 (NSDictionaryM_Common.h:98)
7   CoreFoundation                	0x000000019bf6f1f4 -[__NSDictionaryM setObject:forKeyedSubscript:] + 784 (NSDictionaryM.m:202)
8   Runner                        	0x0000000100fd203c __63-[FLTTransactionStreamHandler onListenWithArguments:eventSink:]_block_invoke + 112 (FLTTransactionStreamHandler.m:55)
9   FirebaseFirestoreInternal     	0x000000010170f258 invocation function for block in -[FIRFirestore runTransactionWithOptions:block:dispatchQueue:completion:]::TransactionResult::RunUpdateBlock(std::__1::shared_ptr<firebase::firestore::core::Transac... + 180 (FIRFirestore.mm:329)
10  libdispatch.dylib             	0x00000001d63739a8 _dispatch_call_block_and_release + 32 (init.c:1597)
11  libdispatch.dylib             	0x00000001d638d1e4 _dispatch_client_callout + 16 (client_callout.mm:85)
12  libdispatch.dylib             	0x00000001d6378148 _dispatch_continuation_pop + 596 (queue.c:345)
13  libdispatch.dylib             	0x00000001d63777c4 _dispatch_async_redirect_invoke + 580 (queue.c:870)
14  libdispatch.dylib             	0x00000001d6385900 _dispatch_root_queue_drain + 360 (queue.c:7473)
15  libdispatch.dylib             	0x00000001d6386098 _dispatch_worker_thread2 + 184 (queue.c:7551)
16  libsystem_pthread.dylib       	0x00000001fb145374 _pthread_wqthread + 232 (pthread.c:2709)
17  libsystem_pthread.dylib       	0x00000001fb1448c0 start_wqthread + 8 (:-1)

Thread 36 name:
Thread 36:
0   libsystem_kernel.dylib        	0x000000024b0c7c68 semaphore_timedwait_trap + 8 (:-1)

Thread 0 (main, servicing another transaction event at the same time):
Thread 0:
0   libobjc.A.dylib               	0x0000000198b09260 objc_allocWithZone + 0 (NSObject.mm:2148)
1   Foundation                    	0x00000001991ebf1c +[NSMutableData(NSMutableData) dataWithCapacity:] + 24 (NSData.m:2060)
2   Flutter                       	0x00000001052a72ec -[FlutterStandardMethodCodec encodeSuccessEnvelope:] + 56 (FlutterStandardCodec.mm:90)
3   Flutter                       	0x00000001052a2c58 invocation function for block in SetStreamHandlerMessageHandlerOnChannel(NSObject<FlutterStreamHandler>*, NSString*, NSObject<FlutterBinaryMessenger>*, NSObject<FlutterMethodCodec>*, NSObject<Flutt... + 200 (FlutterChannels.mm:400)
4   Runner                        	0x0000000100fd2720 __63-[FLTTransactionStreamHandler onListenWithArguments:eventSink:]_block_invoke.5 + 220 (FLTTransactionStreamHandler.m:74)
5   libdispatch.dylib             	0x00000001d63739a8 _dispatch_call_block_and_release + 32 (init.c:1597)
6   libdispatch.dylib             	0x00000001d638d1e4 _dispatch_client_callout + 16 (client_callout.mm:85)
7   libdispatch.dylib             	0x00000001d63aae10 _dispatch_main_queue_drain.cold.6 + 832 (queue.c:8252)
8   libdispatch.dylib             	0x00000001d6382964 _dispatch_main_queue_drain + 176 (queue.c:8233)
9   libdispatch.dylib             	0x00000001d63828a4 _dispatch_main_queue_callback_4CF + 44 (queue.c:8412)
10  CoreFoundation                	0x000000019bff3030 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16 (CFRunLoop.c:1820)
11  CoreFoundation                	0x000000019bf80604 __CFRunLoopRun + 1944 (CFRunLoop.c:3177)
12  CoreFoundation                	0x000000019bf7f54c _CFRunLoopRunSpecificWithOptions + 532 (CFRunLoop.c:3462)

Flutter dependencies

Relevant packages
cloud_firestore: 5.6.12
firebase_core: 3.15.2
firebase_auth: (also in app, not involved in the crash)

Additional context and comments

  • cloud_firestore: 5.6.12
  • Device: iPhone16,1, iOS 26.5 (23F77), TestFlight (release) build
  • Crash is not reproducible in debug as easily; release + real device + parallel transactions is the reliable combination.
  • Workaround that eliminates the crash for us: serializing the transaction calls (await each one) so at most 1–2 are in flight.

Note: filed via CLI because issue forms don't allow pre-filling all fields; sections mirror the bug-report template.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions