react / react/react-native

iOS 26.5 + Fabric: RCTGetBorderImage allocates thousands of CG raster surfaces, OOMing Hermes during JS bundle eval

未关闭
#56,980 6 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

Needs: Author Feedback Needs: Repro
主要语言
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)

  1. Build the production app in Release configuration on an iPhone 17 Pro Max running iOS 26.5 (23F77).
  2. Install and launch.
  3. 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 RCTGetBorderImageUIGraphicsImageRenderer 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): blockedreact-native-reanimated@4.3.1's podspec aborts pod install with assert_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 calls RCTGetBorderImage. 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

  1. 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.
  2. Would a defensive cache cap in RCTBorderDrawing (e.g., LRU bound on RCTGetBorderImage's cache) be considered as a temporary mitigation while Apple addresses the underlying CG raster regression?
  3. Are there any styling patterns known to inflate RCTGetBorderImage raster region count on iOS 26.5 (e.g., percentage borderRadius, gradient borders, dashed borders, specific borderRadius/borderWidth combinations)?

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

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 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

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。