Skip to content

Report Hermes fatal errors on the New Architecture - #59729

Merged
javache merged 2 commits into
react:mainfrom
kraenhansen:fix/hermes-fatal-handler-new-arch
Nov 10, 2026
Merged

javache merged 2 commits into
react:mainfrom
kraenhansen:fix/hermes-fatal-handler-new-arch

Conversation

@kraenhansen

@kraenhansen kraenhansen commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

Part of #59728. The rest of it depends on the glog follow-up described in #59729 (comment).

hermesFatalHandler, which reports the reason for a Hermes fatal error, is only installed by HermesExecutorHolder on the legacy path. On the New Architecture nothing sets a handler, so Hermes writes the reason to stderr, which doesn't reach logcat, and crashes with its deliberate null write. The result is a SIGSEGV with no reason.

This installs a handler from HermesInstance::createJSRuntime, once per process, on both platforms. It logs the reason through react_native_log_fatal, which aborts. It's kept separate from the handler in reactexecutor/OnLoad.cpp so that the legacy code can be removed on its own.

On Android, react_native_log_fatal uses glog, which only reaches logcat once reactnativejni has loaded. That's described, with a proposed follow-up, in #59729 (comment).

Why the handler doesn't call android_set_abort_message

Once glog's logcat sink is installed, the tombstone's Abort message: already carries the reason. The sink in first-party/fbgloginit/glog_init.cpp maps GLOG_FATAL to ANDROID_LOG_FATAL and writes it with __android_log_print. Since Android 7.0 (API 24, React Native's minSdk), liblog's __android_log_buf_write calls android_set_abort_message for every ANDROID_LOG_FATAL write (source). Neither this handler nor glog_init.cpp needs to call it. On the API 37 emulator, this revision's libreactnative.so and libhermestooling.so don't reference android_set_abort_message, and the tombstone still shows the reason. A missing abort message means the sink wasn't installed, which is the follow-up in the comment linked above, not a missing call.

Changelog:

[GENERAL] [FIXED] - Report the reason for Hermes fatal errors through the React Native logger on the New Architecture

Test Plan:

Used the reproducer from the issue, https://github.com/kraenhansen/rn-hermes-fatal-handler-repro, on an arm64 API 37 emulator. It's a stock 0.88.1 release app that exhausts a small-heap runtime 8 seconds after launch. The change was cherry-picked onto v0.88.1, ReactAndroid was built from source, and the resulting libhermestooling.so was swapped into the APK.

Before, with stock 0.88.1:

I HermesGC: OOM: [repro] reason = Max heap size was exceeded (...)
F libc    : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 4254 (Thread-2), pid 3125 (com.fatalrepro)
F DEBUG   : Cause: null pointer dereference

After, with a transform on the root view so that reactnativejni loads, in 3 of 3 runs:

I HermesGC: OOM: [repro] reason = Max heap size was exceeded (...)
F ReactNativeJNI: OOM: error_code(value = 1, category = vm_allocate_category, message = Max heap size was exceeded)
F libc    : Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 5985 (Thread-2), pid 5904 (com.fatalrepro)
F DEBUG   : Abort message: 'OOM: error_code(value = 1, category = vm_allocate_category, message = Max heap size was exceeded)'

With the stock template, 3 of 3 runs ended in SIGABRT from glog, with no ReactNativeJNI line and no Abort message: line. Nothing was run on iOS.

Note that fatal errors now end in SIGABRT rather than SIGSEGV, which may change how they group in crash reporters. An app that sets its own Hermes fatal handler before the runtime is created would be replaced, because Hermes has a single handler and no getter.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 31, 2026
@kraenhansen
kraenhansen marked this pull request as ready for review November 1, 2026 22:05
@facebook-github-tools facebook-github-tools Bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Nov 1, 2026

@javache javache left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's move this to the cross-platform HermesInstance::createJSRuntime path, and log using react_native_log_fatal

@kraenhansen
kraenhansen requested a review from javache November 7, 2026 21:28
Moves the handler from the Android-only JHermesInstance to the
cross-platform HermesInstance, and logs through react_native_log_fatal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kraenhansen
kraenhansen force-pushed the fix/hermes-fatal-handler-new-arch branch from 3b4bd0e to ce3d9c9 Compare November 8, 2026 01:29
@kraenhansen

kraenhansen commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor Author

TL;DR: This PR can merge as is. It doesn't make anything worse: Hermes fatal errors now report their reason through React Native's logger. On Android, though, that reason doesn't yet reliably reach logcat or the crash report. Testing turned up a problem with glog, which the logger uses on Android, and it affects more than Hermes. I've described it below with a proposed fix in #59861.

What this PR does

The Hermes fatal handler is installed once from HermesInstance::createJSRuntime and calls react_native_log_fatal. It's platform-agnostic, and recording why the process aborts is left to the code that aborts. On Android that's glog, which calls abort() at the end of LOG(FATAL):

#04 pc ... libreactnative.so (google::LogMessageFatal::~LogMessageFatal()+8)
#05 pc ... libreactnative.so (react_native_log_fatal+148)

I tested it with the reproducer from the issue on 0.88.1 (arm64 API 37 emulator). It triggers a Hermes fatal error by exhausting a second runtime with an 8 MB heap, so the reason to look for is Max heap size was exceeded.

Problem: glog is set up too late on the New Architecture

glog reaches logcat only after its logcat sink is installed, in the JNI_OnLoad of reactnativejni. Since #50437 moved NativeMap and NativeArray into reactnativejni_common, the New Architecture loads reactnativejni lazily. On 0.88.1 that's on the first transform prop or network inspector use, and on main also on the first touch event. Until then, glog writes to stderr. So a Hermes fatal error, or anything else logging through glog during startup, isn't guaranteed to reach logcat.

In the reproducer, 3 of 3 runs of the stock template aborted with no log line from glog. With a transform on the root view, 3 of 3 runs logged the reason:

F ReactNativeJNI: OOM: error_code(value = 1, category = vm_allocate_category, message = Max heap size was exceeded)
F libc    : Fatal signal 6 (SIGABRT), code -1 (SI_QUEUE) in tid 4521 (Thread-2), pid 4445 (com.fatalrepro)

Proposed fix: call gloginit::initialize() from rninstance's JNI_OnLoad. ReactInstance loads rninstance on the main thread before the runtime, Fabric and TurboModules are created, so glog would be set up before any of them can log. gloginit::initialize() only runs once per process, so the existing call in reactnativejni can stay for the legacy architecture. The WITH_GLOGINIT and WITH_XPLATINIT guards would need to carry over for internal builds.

Implemented in #59861. With it and this PR, 3 of 3 runs of the stock template log the reason as F ReactNativeJNI: OOM: ... Max heap size was exceeded, and the tombstone shows it as Abort message:. rninstance loads before hermesinstancejni, and the library load order is unchanged.

I also considered moving it from reactnativejni's JNI_OnLoad into reactnativejni_common's, only because it would keep glog setup in the library it came from, in case there's a reason it lives there. But reactnativejni_common loaded about 27 ms after hermesinstancejni in the reproducer, on a background thread. It would also have to be loaded earlier, from a class that doesn't use its native code.

Guidance

  • Is rninstance OK for setting up glog, or is there a reason it lives in reactnativejni?

@meta-codesync

meta-codesync Bot commented Nov 10, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D130916024.

@javache
javache merged commit 339e09f into react:main Nov 10, 2026
101 checks passed
meta-codesync Bot pushed a commit that referenced this pull request Nov 10, 2026
## Summary:

Part of #59728. Context in #59729 (comment) (problem 1).

glog's logcat sink is installed in the `JNI_OnLoad` of `reactnativejni`. Since #50437, the New Architecture loads that library lazily: on the first transform prop or network inspector use, and on `main` also on the first touch event. Until then, glog writes only to stderr, so warnings and fatal errors logged during startup never reach logcat. Since Android 7.0, `liblog` sets the tombstone's abort message from fatal log writes, so those fatal errors also crash with a bare `SIGABRT`.

This calls `gloginit::initialize()` from `rninstance`'s `JNI_OnLoad`. `ReactInstance` loads `rninstance` before the runtime, Fabric and TurboModules are created. `gloginit::initialize()` runs once per process, so the call in `reactnativejni` stays for the legacy architecture.

## Changelog:

[ANDROID] [FIXED] - Send glog output during startup to logcat on the New Architecture

## Test Plan:

Tested with [rn-hermes-fatal-handler-repro](https://github.com/kraenhansen/rn-hermes-fatal-handler-repro), which triggers a Hermes fatal error with the reason `Max heap size was exceeded`. I cherry-picked this commit onto 0.88.1, built `./gradlew :packages:react-native:ReactAndroid:assembleRelease`, swapped the arm64 libraries into the reproducer's stock-template APK, and ran it 3 times per build on an arm64 API 37 emulator. All runs were consistent.

| Build | Crash | glog warnings in logcat | Fatal reason in logcat | Abort message in tombstone |
| --- | --- | --- | --- | --- |
| 0.88.1 | `SIGSEGV` | No | No | No |
| 0.88.1 + this PR | `SIGSEGV` | Yes | No | No |
| 0.88.1 + #59729 | `SIGABRT` | No | No | No |
| 0.88.1 + #59729 + this PR | `SIGABRT` | Yes | Yes | Yes |

With this PR and #59729:

```text
W ReactNativeJNI: Codegen component descriptors were not configured from JNI_OnLoad
W ReactNativeJNI: ReactInstance: evaluateJavaScript() with JS bundle
F ReactNativeJNI: OOM: error_code(value = 1, category = vm_allocate_category, message = Max heap size was exceeded)
F DEBUG   : Abort message: 'OOM: error_code(value = 1, category = vm_allocate_category, message = Max heap size was exceeded)'
```

Nothing was logged twice, including with a transform on the root view, where `reactnativejni` also loads. I didn't run this on API levels below 37.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Differential Revision: D130915975

Pull Request resolved: #59861
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants