iOS 26.5 + Fabric: RCTGetBorderImage allocates thousands of CG raster surfaces, OOMing Hermes during JS bundle eval
还没有人认领这个 Issue。
- 主要语言
- C++
- 星标
- 127k
- 派生
- 25.3k
- 平均合并
- 1 天 23 小时
- 30 天内合并 PR
- 4
描述
Summary
After upgrading a test device from iOS 26.4.1 to iOS 26.5 (23F77), our React Native 0.85.3 app crashes at launch with LLVM ERROR: OOM in Hermes' GC. The faulting thread is the Hermes JS runtime, but the proximate cause is the iOS main thread: Fabric's first mounting transaction calls RCTGetBorderImage thousands of times while the JS bundle is still being evaluated, producing ~18,902 CG raster regions / 6.9 GB of CG raster data before bundle eval finishes. Total process VM reaches ~8.8 GB, then Hermes' next OldGen allocation aborts the process (signal 6).
The same Release build on iOS 26.4.1 boots and runs normally; on iOS 26.5 the same binary aborts before any onboarding view renders. We worked around the bug by rewriting the offending background component in @shopify/react-native-skia (Skia draws to a single GPU surface and never calls RCTGetBorderImage); the production app now boots cleanly on iOS 26.5. The underlying RN + iOS interaction is filed here so other teams hitting it can find this thread and so RN can consider a defensive cache cap in RCTBorderDrawing.
Reproducer
https://github.com/Bodholdt-Speed-Testing/ios26-5-fabric-oom-repro
Important caveat: the reproducer scaffold isolates the structural pattern we believe is the trigger (5+ <Animated.View> children with borderRadius + borderWidth + JS-driver animated transform: [{ scale: interpolate(...) }] + animated opacity) but does not crash standalone on the affected device. We ran two cycles (5+30 views, then 15+60 views + 20 cards + 3 hidden Modals + header chrome). Both booted cleanly on iPhone 17 Pro Max running iOS 26.5 (23F77).
The crashing production app renders the same pattern but inside a much larger mount context: @shopify/react-native-skia initialized, react-native-reanimated@4.3.1 runtime active, a 4.5 MiB Hermes JS bundle evaluating, AsyncStorage + Keychain native calls at startup, multiple authentication/onboarding screens already mounted. In that surrounding VM pressure, the bug reliably reproduces 100% of the time.
We have not yet been able to compress that combined pressure into a minimal pure-RN reproducer. We're filing now with the scaffold + full diagnosis rather than blocking on extracting a smaller repro — happy to extract a stripped fork of the production app if it would help upstream diagnosis.
React Native version
System:
OS: macOS 26.5 (25F71)
Packages:
react-native: 0.85.3
react: 19.2.3
react-native-reanimated: 4.3.1
@shopify/react-native-skia: 2.6.2
Pods:
hermes-engine: 250829098.0.10
React-Fabric: bb0baa33d91839631d315800eb23e9aaa4338a44
React-FabricComponents: c504d0b0e2f3054b2ba19af839f175cb361153c0
React-FabricImage: 91eaea1cc58d25ae2596a9277bcfe028f92374c3
New Architecture: enabled (default for 0.85.3; Bridgeless + Fabric)
Device: iPhone 17 Pro Max
Device iOS: 26.5 (23F77) ← regression starts here
Last-known-good iOS: 26.4.1
Xcode: 26.x
Steps to reproduce (against the production app)
- Build the production app in Release configuration on an iPhone 17 Pro Max running iOS 26.5 (23F77).
- Install and launch.
- App aborts during JS bundle evaluation. Crash is 100% repeatable across uninstall + reinstall + reboot, fresh
pod install, and clean DerivedData.
The same binary on iOS 26.4.1 boots and runs normally end-to-end. The same source on an iOS 26.5 simulator does not crash (only the Release-on-device path reproduces).
Console output at crash
W ... ReactInstance: evaluateJavaScript() with JS bundle
LLVM ERROR: OOM: error_code(value = 12, ...message = Cannot allocate memory)
App terminated due to signal 6.
The LLVM ERROR is Hermes' embedded llvh::report_fatal_error printer (not the LLVM JIT) — there is no JavaScript exception. A global ErrorUtils.setGlobalHandler cannot catch this; we tried, and Hermes does not get the chance to install handlers before the OOM.
Crash report — key threads
Faulting thread (Hermes JS runtime):
abort
hermesvm::hermesFatalErrorHandler
llvh::report_fatal_error
GCBase::oom
HadesGC::OldGen::allocSlow
HadesGC::youngGenCollection
Interpreter::interpretFunction
TimerManager::attachGlobals (lambda)
Main thread (UI / Fabric mounting):
argb32_mark_constmask
RIPLayerBltShape
ripc_Render
ripc_DrawPath
CG::DisplayList::executeEntries
-[UIGraphicsImageRenderer imageWithActions:]
RCTGetBorderImage
RCTAddContourEffectToLayer
-[RCTViewComponentView finalizeUpdates:]
-[RCTMountingManager performTransaction:]
So Fabric's first mounting transaction is allocating CG image surfaces via RCTGetBorderImage → UIGraphicsImageRenderer for our initial view tree, the resulting raster blows the process VM budget, and Hermes — still busy evaluating the JS bundle on a separate thread — fails its next OldGen allocation and aborts.
vmSummary line at crash
CG raster data 6.9 GB 18,902 regions
(Out of ~8.8 GB total VM at the moment of abort.) 18,902 raster regions is roughly 200–500× what we would expect for this screen's view tree.
Render-only bisect on the production app
Each cycle is a full Release build + uninstall + reinstall + Trust + console launch on the same iPhone 17 Pro Max running iOS 26.5. We progressively stripped the failing screen (SpeedTestScreen):
| Cycle | App state | Result |
|---|---|---|
| 1 | 3 always-mounted modals stripped, body intact | crashes (std::bad_alloc — signature changes from LLVM OOM) |
| 2 | + full ScrollView body + post-ScrollView overlays stripped | crashes (LLVM ERROR: OOM) |
| 3 | + theme background + header chrome stripped (only Animated.View wrapper) |
boots cleanly |
| 4 | Theme background re-enabled, header still stripped | crashes (std::bad_alloc) |
| 5 | Same as cycle 4 but outer Animated.View swapped for plain <View> |
crashes (std::bad_alloc) |
Cycle 5 specifically rules out the outer animated wrapper as the trigger. The trigger is the background subtree itself — 5 rings + 30 streaks, each <Animated.View> with all of:
borderRadius(5 rings:90; 30 streaks:width/2)borderWidth(rings:2.5; streaks:1)- Animated
transform: [{ scale: ring.scale.interpolate([0,1], [0.05, 3.0]) }](rings;useNativeDriver: false) - Animated
opacity(rings:useNativeDriver: false; streaks:useNativeDriver: true)
Workarounds tried
- Clean rebuild + wipe DerivedData + fresh
pod install: no change. - Reboot of device: no change.
- Revert all in-flight defensive try/catch wraps: no change. Confirms this is not a JS error our code is throwing.
RCT_NEW_ARCH_ENABLED=0(try Legacy Renderer): blocked —react-native-reanimated@4.3.1's podspec abortspod installwithassert_new_architecture_enabled. Reanimated 4 hard-requires the New Architecture, and our app's animation layer depends on it. We cannot trial the Legacy Renderer as a diagnostic without ripping out Reanimated 4.- A global
ErrorUtils.setGlobalHandler: prevented bundle eval from completing on Hermes Release builds (separate bug, off-topic here). Removed. - Skia rewrite — works. We rewrote the offending background in
@shopify/react-native-skia(single<Canvas>root,<Circle style="stroke">for rings,<RoundedRect>for streaks). Skia draws to one GPU surface and never callsRCTGetBorderImage. The production app now boots cleanly on iOS 26.5. This is a real workaround, but it's app-specific — RN itself remains exposed for any app rendering the trigger pattern on iOS 26.5.
Related code-area work
facebook/react-native#56915 — "Fix percentage-based border radius" — landed 2026-05-22, post-0.85.3, touches the same isUniform path in RCTBorderDrawing. No memory mention in that PR, but it is the closest recent change we can find in the relevant code area.
Asks
- Is this a known regression upstream on iOS 26.5? Public searches across GitHub issues, Apple Developer Forums, Stack Overflow, Reddit, and MacRumors turned up nothing matching this stack — happy to be pointed at a duplicate.
- Would a defensive cache cap in
RCTBorderDrawing(e.g., LRU bound onRCTGetBorderImage's cache) be considered as a temporary mitigation while Apple addresses the underlying CG raster regression? - Are there any styling patterns known to inflate
RCTGetBorderImageraster region count on iOS 26.5 (e.g., percentageborderRadius, gradient borders, dashed borders, specificborderRadius/borderWidthcombinations)?
We are happy to provide the full .ips file privately, run further on-device diagnostics, test a patched Hermes / Fabric build, or extract a more minimal reproducer from the production app on request. The reproduction is 100% reliable on our side.
cc @react-native-bot Platform: iOS
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
从链接的 reproducer 以及报告中描述的 RCTBorderDrawing/RCTGetBorderImage 路径开始。以报告中的 vmSummary 和 mounting stack 作为检查点,比较 iOS 26.4.1 和 26.5 上生产环境中的触发模式。完成标准是得到可靠的最小复现,或确认存在针对过度 raster 分配的 upstream 缓解方案。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- ios, react-native
- 领域
- mobile, performance
- Issue 类型
- 缺陷
- 难度
- 5/5
- 预计耗时
- 一周以上
- 活跃度
- 活跃
- 描述清晰度
- 需要澄清
- 新手友好度
- 35/100