[iOS][Fabric] 0.86.2 Release builds crash on cold launch with heap corruption when mounting an Expo native view; framework-swap bisection pins it to the prebuilt core binary
Nobody has claimed this yet.
- Dominant language
- C++
- Stars
- 127k
- Forks
- 25.3k
- Avg merge
- 1d 23h
- Merged PRs (30d)
- 4
Description
Description
A minimal app that mounts a single native view from expo-symbols crashes on most cold launches under React Native 0.86.2, and never under 0.86.0. The reproduction repository is at https://github.com/SpiGAndromeda/reproducer-rn-0862-symbolview-launch-crash, created from the reproducer-react-native template. Each test below launches the installed app ten times on an iOS simulator, terminating it between launches and waiting 6 seconds per launch; a launch counts as crashed when the process is no longer running after the wait:
| Configuration | Crashed launches |
|---|---|
| Template reproducer (HEAD), 0.86.2 prebuilt core | 10/10 |
Template reproducer at HEAD~1, react-native 0.86.0 |
0/10 |
| Expo-prebuild reproducer, 0.86.2 prebuilt core | 9/10 |
Same built binary, only React.framework swapped to 0.86.0-release |
0/10 |
| Production app the trigger was isolated from, 0.86.2 | 7/10 to 10/10 |
Production app with only React.framework swapped to 0.86.0-release |
0/10 |
The two Expo-prebuild reproducer runs used the same built .app, so the only difference between 9/10 and 0/10 is the core framework binary. Because React.framework and hermesvm.framework are separate dylibs in the bundle, they can be swapped in a built app (plus ad-hoc re-signing) without recompiling anything. Swapping only the Hermes VM (250829098.0.16 back to 250829098.0.14) changes nothing, so the Hermes V1 bump is ruled out. The defect therefore lives in the react-native-artifacts 0.86.2 reactnative-core binary.
I verified by UUID (dwarfdump --uuid against the Maven tarballs cached in Pods/*-artifacts/) that each app carried the release flavor of React.framework and hermesvm.framework, so this is not the debug/release swap issue from #57803.
The trigger was isolated from a production app by removing one launch ingredient at a time: the full app crashes, the app without a single mounted SymbolView (the SF Symbols view from expo-symbols) does not, and a trivial app containing nothing but a <View>, a <Text> and that one SymbolView crashes again. The production app's remaining native modules were then removed without changing the outcome, so the reproducer links only expo and expo-symbols.
Three configuration facts, each load-bearing for the reproduction:
- The crash requires Expo's precompiled module binaries: with
EXPO_USE_PRECOMPILED_MODULES=1in the Podfile (theexpo prebuilddefault, but not the CocoaPods default), ExpoModulesCore and its sibling pods arrive as prebuilt dynamic xcframeworks and the app crashes 10/10; with the same pods compiled from source and linked statically, the same pin set on the same 0.86.2 core is clean 0/10. Whether the discriminator is the dynamic linkage or the shipped binaries is not determined. A reproduction attempt from a bare@react-native-community/cli initapp without this flag concludes the bug does not exist. - The trigger depends on the
expo-modules-coreversion, with an exact boundary: at expo-modules-core 57.0.7 (together with the ExpoModulesWorklets it ships and versions in lockstep) the app crashes 10/10; raising only expo-modules-core to 57.0.8 or 57.0.10, everything else pinned unchanged, drops that to 0/10. This masks the crash rather than provably fixing it: the framework swap above localises the defect to the 0.86.2reactnative-corebinary, and a consumer-side change that stops triggering the defect is consistent with the defect remaining. The floated SDK 57 patch set current today (expo 57.0.12, expo-symbols 57.0.2, expo-modules-core 57.0.10) is accordingly clean over twenty launches. - The repository pins the full expo module set and commits its
package-lock.json, because~ranges across expo native modules resolve to ABI-mismatched sibling sets that abort at dyld before any React code runs. That failure inflates naive crash counts and has nothing to do with this bug.
The crashes are heap corruption, not a single bad dereference. The crash reports show the same two variants across the reproducers and the production app: EXC_BREAKPOINT raised inside libsystem_malloc (xzone malloc detecting corrupted heap state) during allocations on the JavaScript thread (under RawPropsParser::at and under CoreText font-name resolution through a corrupted CFDictionary), and EXC_BAD_ACCESS on the main thread in -[RCTComponentViewFactory createComponentViewWithComponentHandle:] during a mounting transaction.
Narrowing the source delta: v0.86.0...v0.86.2 contains four functional commits (the 0.86.2 release notes omit what shipped in the unpublished 0.86.1). Two are Android-only (821045a24f, cdfba520fa). The Hermes V1 bump (95538111bf) is ruled out by the Hermes-swap result above. That leaves the Yoga display: contents fix (36f69eff0d) and 0b941bc51a, "Add guards around nativeProps usage to prevent race conditions" (#52646), which reworks ShadowNode::clone and UIManager. Which of the two corrupts the heap is not established. The reproducer contains no setNativeProps call and no display: 'contents' node, so I can offer the binary-level localisation and the trigger, not the commit. 0.87.0 carries both commits with no follow-up fixes to them.
Environment: iOS 26.5 simulator on macOS 26.5.2, Xcode 26.6, Hermes, New Architecture (bridgeless).
Steps to reproduce
- Clone https://github.com/SpiGAndromeda/reproducer-rn-0862-symbolview-launch-crash and follow its README:
npm ci,cd ios && pod install, then a Release simulator build withxcodebuild. The pinned dependency versions are part of the reproduction; do not update them. - Run
tools/verify-flavors.shon the built app (UUID check), thentools/census.shfor the ten-launch series: 10/10 crash during the first mount.tools/crash-signature.shreads the exit signals from the simulator log (SIGTRAP/SIGSEGV are the crash; SIGABRT would indicate a broken dependency set instead). git checkout HEAD~1is the identical app on react-native 0.86.0; built the same way, it launches clean 0/10.git diff HEAD~1 HEADis the complete regression delta: the react-native pin, its lockstep@react-native/*pins, and the regenerated lockfiles.- The framework-swap counter-experiment (replacing only
Frameworks/React.frameworkinside an already-built app) and the expo-modules-core masking measurements are in the supplementary repository: https://github.com/it-bens/rn-0862-ios-fabric-launch-crash (tools/swap-core.sh). - If crashes stop producing
.ipsreports, that is ReportCrash throttling repeated identical crashes; both READMEs document counting abnormal exits from the simulator log instead.
React Native Version
0.86.2
Affected Platforms
Runtime - iOS
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
System:
OS: macOS 26.5.2
CPU: (12) arm64 Apple M4 Pro
Memory: 3.72 GB / 48.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 26.5.0
path: /Users/martinbens/.nvm/versions/node/v26.5.0/bin/node
Yarn: Not Found
npm:
version: 11.17.0
path: /Users/martinbens/.nvm/versions/node/v26.5.0/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.17.0
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 25.5
- iOS 26.5
- macOS 26.5
- tvOS 26.5
- visionOS 26.5
- watchOS 26.5
Android SDK:
API Levels:
- "35"
- "36"
Build Tools:
- 35.0.0
- 36.0.0
System Images:
- android-35 | Google APIs ARM 64 v8a
Android NDK: Not Found
IDEs:
Android Studio: Not Found
Xcode:
version: 26.6/17F113
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.20
path: /usr/bin/javac
Ruby:
version: 4.0.6
path: /opt/homebrew/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 19.2.8
wanted: 19.2.8
react-native:
installed: 0.86.2
wanted: 0.86.2
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: Not found
newArchEnabled: Not found
iOS:
hermesEnabled: Not found
newArchEnabled: Not found
Reproducer dependency pins: expo 57.0.8, expo-symbols 57.0.1, and npm overrides holding expo-modules-core 57.0.7, expo-file-system 57.0.1, expo-asset 57.0.7, expo-font 57.0.1, expo-keep-awake 57.0.1, expo-constants 57.0.10. The committed package-lock.json is authoritative.
Stacktrace or Logs
Two representative faulting threads (full `.ips` files available on request,
from both the reproducer and the production app). Heap-corruption abort on the
JavaScript thread:
Exception: EXC_BREAKPOINT
Thread: com.facebook.react.runtime.JavaScript
libsystem_malloc.dylib _xzm_xzone_malloc_freelist_outlined
libsystem_malloc.dylib _xzm_xzone_malloc_tiny
libc++abi.dylib operator new(unsigned long)
React <deduplicated_symbol>
React std::__1::vector<facebook::react::RawPropsKey, ...>::push_back(...)
React facebook::react::RawPropsParser::at(facebook::react::RawProps const&, facebook::react::RawPropsKey const&) const
Null-region dereference on the main thread during a mounting transaction:
Exception: EXC_BAD_ACCESS, KERN_INVALID_ADDRESS at 0x0000000000000018
Thread: com.apple.main-thread
React -[RCTComponentViewFactory createComponentViewWithComponentHandle:]
React -[RCTComponentViewRegistry _dequeueComponentViewWithComponentHandle:]
React -[RCTComponentViewRegistry dequeueComponentViewWithComponentHandle:tag:]
React RCTPerformMountInstructions(...)
React facebook::react::TelemetryController::pullTransaction(...)
React -[RCTMountingManager performTransaction:]
Loaded-image UUIDs in the crashing processes match the release tarballs: `React`
= 791C3298-6723-3FE6-B25E-4C6BD2F09175 (reactnative-core-0.86.2-release, arm64
simulator slice), `hermesvm` = 75872184-F738-3E5C-B47A-FD4BCE09605A
(hermes-ios-250829098.0.16-release). The clean runs carry `React` =
61086F4A-30F5-3EA0-A72A-2C94C71C7AAC (reactnative-core-0.86.0-release).
MANDATORY Reproducer
https://github.com/SpiGAndromeda/reproducer-rn-0862-symbolview-launch-crash
Created from the reproducer-react-native template (generated-from metadata intact; the template's pre-merge CI is green on the reproducing state), with the Expo modules integrated manually and App.tsx mounting one <SymbolView name="gearshape" /> next to a <Text>. On top of the two template commits sit exactly two more: a working baseline at react-native 0.86.0 (0/10 crashed, flavor-verified), and HEAD, which moves only the react-native pin to 0.86.2 with its lockstep @react-native/* pins and lockfiles (10/10 crashed; exit signals 7 SIGTRAP + 3 SIGSEGV, zero SIGABRT). It ships tools/verify-flavors.sh, tools/census.sh and tools/crash-signature.sh, and commits the package-lock.json that holds the expo module set at the versions the crash requires.
Supplementary repository with the dylib-swap tooling and the expo-modules-core masking table: https://github.com/it-bens/rn-0862-ios-fabric-launch-crash.
Screenshots and Videos
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the mandatory reproducer README, its pinned package-lock.json, and tools/verify-flavors.sh, tools/census.sh, and tools/crash-signature.sh; compare the 0.86.2 HEAD with HEAD~1 on repeated Release simulator launches. Use the supplementary repository and tools/swap-core.sh to confirm the framework boundary, then investigate the two remaining candidate commits. Done means identifying the corrupting regression and demonstrating that the reproducer no longer crashes while the 0.86.0 baseline remains clean.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp, ios, react-native
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100