Skip to content

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

@BodholdtSpeedTesting

Description

@BodholdtSpeedTesting

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Needs: Author FeedbackNeeds: ReproThis issue could be improved with a clear list of steps to reproduce the issue.

    Type

    No type
    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