Environment
- @rnmapbox/maps: 10.2.10 (also verified present in 10.3.2 sources)
- MapboxMaps iOS: 11.16.6
- React Native: 0.81 (Expo 54), New Architecture (Fabric) enabled
- iOS 26.5, iPhone14,3, real device, debug + confirmed race also fatal in release-class conditions
Summary
MovePointShapeAnimator (and ChangeLineOffsetsShapeAnimator) mutate the MapboxMaps style from a background thread under the New Architecture, corrupting MapboxMaps' internal StyleSourceManager.WorkItemPerGeoJSONSourceTracker Swift Dictionary and crashing the app (FATAL). MapboxMaps' style/source API is main-thread-only (mapbox/mapbox-maps-ios#1322); every other RNMBX module already hops to main (RNMBXViewResolver / addUIBlock), but the shape-animator TurboModules do not: methodQueue { return RCTGetUIManagerQueue(); } is ignored for TurboModule invocations, so moveTo runs on a background dispatch queue and synchronously calls ShapeAnimatorCommon.refresh() → shapeUpdated → Style.updateGeoJSONSource off-main — racing (a) the animator's own main-thread CADisplayLink refresh and (b) main-thread Fabric shape.didset updates.
Evidence — two fully-symbolicated on-device crash reports, same dictionary, opposite threads
Crash 1 — MAIN thread (Fabric prop update side of the race):
5 libswiftCore Dictionary._Variant.removeValue(forKey:)
6 MapboxMaps StyleSourceManager.WorkItemPerGeoJSONSourceTracker.cancelAll(for:)
7 MapboxMaps StyleSourceManager.updateGeoJSONSource(withId:data:dataId:)
...
21 RNMBX RNMBXShapeSource.shape.didset
24 RNMBX -[RNMBXShapeSourceComponentView updateProps:oldProps:]
25 React RCTPerformMountInstructions (main thread)
→ NSInvalidArgumentException: -[NSIndexPath objectForKey:]: unrecognized selector sent to instance 0x8000000000000000 (poisoned pointer out of the corrupted Dictionary).
Crash 2 — BACKGROUND TurboModule queue (animator side of the race):
5 libswiftCore Dictionary._Variant.setValue(_:forKey:)
6 libswiftCore Dictionary.subscript.setter
7 MapboxMaps StyleSourceManager.WorkItemPerGeoJSONSourceTracker.add(_:for:)
8 MapboxMaps StyleSourceManager.updateGeoJSONSource(withId:data:dataId:)
...
16 RNMBX RNMBXShapeSource.doUpdate(_:)
19 RNMBX ShapeAnimatorCommon.refresh()
23 RNMBX MovePointShapeAnimator.moveTo(coordinate:durationSec:)
26 RNMBX -[RNMBXMovePointShapeAnimatorModule moveTo:coordinate:duration:resolve:reject:]
31 React facebook::react::ObjCTurboModule::performMethodInvocation
...on _dispatch_workloop_worker_thread (background)
→ EXC_BAD_ACCESS KERN_INVALID_ADDRESS (possible pointer authentication failure).
Reproduction context: navigation app animating a point ShapeSource via __experimental.MovePointShapeAnimator.moveTo(...) per GPS tick (~6-10Hz) while other declarative ShapeSources update. Intermittent (seconds→minutes to crash); frequency scales with update volume.
Fix that works for us (patch-package, happy to PR)
ShapeAnimatorCommon.refresh() — main-thread guard (deepest chokepoint, covers all animators):
guard Thread.isMainThread else {
DispatchQueue.main.async { [weak self] in self?.refresh() }
return
}
RNMBXMovePointShapeAnimatorModule.swift static moveTo — wrap body in DispatchQueue.main.async (with resolve: @escaping RCTPromiseResolveBlock), so the animator's AnimatableElement state writes can't race the 60fps main-thread displayLink reads (Swift exclusivity traps this in debug).
ShapeAnimatorManager.register — same main-thread guard (generate registers from the background queue while get(shape:) reads on main).
Ordering stays correct (main queue FIFO), latency cost ≤1 runloop turn per moveTo, no ABI change. Related upstream reports of the same crash class: mapbox/mapbox-maps-ios#2282, mapbox/mapbox-maps-ios#2195.
Environment
Summary
MovePointShapeAnimator(andChangeLineOffsetsShapeAnimator) mutate the MapboxMaps style from a background thread under the New Architecture, corrupting MapboxMaps' internalStyleSourceManager.WorkItemPerGeoJSONSourceTrackerSwiftDictionaryand crashing the app (FATAL). MapboxMaps' style/source API is main-thread-only (mapbox/mapbox-maps-ios#1322); every other RNMBX module already hops to main (RNMBXViewResolver/addUIBlock), but the shape-animator TurboModules do not:methodQueue { return RCTGetUIManagerQueue(); }is ignored for TurboModule invocations, somoveToruns on a background dispatch queue and synchronously callsShapeAnimatorCommon.refresh()→shapeUpdated→Style.updateGeoJSONSourceoff-main — racing (a) the animator's own main-threadCADisplayLinkrefresh and (b) main-thread Fabricshape.didsetupdates.Evidence — two fully-symbolicated on-device crash reports, same dictionary, opposite threads
Crash 1 — MAIN thread (Fabric prop update side of the race):
→
NSInvalidArgumentException: -[NSIndexPath objectForKey:]: unrecognized selector sent to instance 0x8000000000000000(poisoned pointer out of the corrupted Dictionary).Crash 2 — BACKGROUND TurboModule queue (animator side of the race):
→
EXC_BAD_ACCESS KERN_INVALID_ADDRESS (possible pointer authentication failure).Reproduction context: navigation app animating a point ShapeSource via
__experimental.MovePointShapeAnimator.moveTo(...)per GPS tick (~6-10Hz) while other declarative ShapeSources update. Intermittent (seconds→minutes to crash); frequency scales with update volume.Fix that works for us (patch-package, happy to PR)
ShapeAnimatorCommon.refresh()— main-thread guard (deepest chokepoint, covers all animators):RNMBXMovePointShapeAnimatorModule.swiftstaticmoveTo— wrap body inDispatchQueue.main.async(withresolve: @escaping RCTPromiseResolveBlock), so the animator'sAnimatableElementstate writes can't race the 60fps main-thread displayLink reads (Swift exclusivity traps this in debug).ShapeAnimatorManager.register— same main-thread guard (generateregisters from the background queue whileget(shape:)reads on main).Ordering stays correct (main queue FIFO), latency cost ≤1 runloop turn per moveTo, no ABI change. Related upstream reports of the same crash class: mapbox/mapbox-maps-ios#2282, mapbox/mapbox-maps-ios#2195.