Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

- Check `captureReplay` return value in iOS bridge to avoid linking error events to uncaptured replays ([#6008](https://github.com/getsentry/sentry-react-native/pull/6008))
- Report the expected properties file path and any missing keys when using `flavorAware` on Android, instead of failing with an opaque `Illegal null value provided in this collection` error ([#6031](https://github.com/getsentry/sentry-react-native/pull/6031))
- Stop the Hermes sampling profiler on React instance teardown to prevent `pthread_kill` SIGABRT when the JS thread is torn down with profiling active ([#5441](https://github.com/getsentry/sentry-react-native/issues/5441))
Comment thread
antonis marked this conversation as resolved.
Outdated

### Dependencies

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,14 @@
*/
private int profilingTracesHz = 101;

private AndroidProfiler androidProfiler = null;
// Volatile because invalidate() (NativeModules/teardown thread) can null this concurrently
// with stopProfiling() (JS thread); we need cross-thread visibility of the null write.
private volatile AndroidProfiler androidProfiler = null;

// Written on the JS thread (start/stopProfiling via @ReactMethod isBlockingSynchronousMethod),
// read on the NativeModules/teardown thread (invalidate). volatile ensures cross-thread
// visibility so invalidate() cannot observe a stale `false` while profiling is active.
private volatile boolean isProfiling = false;

private boolean isProguardDebugMetaLoaded = false;
private @Nullable String proguardUuid = null;
Expand Down Expand Up @@ -789,10 +796,15 @@
}

try {
// Defensive reset: the Hermes sampling profiler is global with no state
// introspection, so flush any leaked registration from a prior run before
// enabling. See https://github.com/facebook/hermes/issues/1853.
HermesSamplingProfiler.disable();
HermesSamplingProfiler.enable();
if (androidProfiler != null) {
Comment thread
antonis marked this conversation as resolved.
Outdated
androidProfiler.start();
}
isProfiling = true;
Comment thread
antonis marked this conversation as resolved.
Outdated

result.putBoolean("started", true);
} catch (Throwable e) { // NOPMD - We don't want to crash in any case
Expand All @@ -812,6 +824,7 @@
end = androidProfiler.endAndCollect(false, null);
}
HermesSamplingProfiler.disable();
isProfiling = false;
Comment thread
antonis marked this conversation as resolved.
Outdated

output =
File.createTempFile(
Expand Down Expand Up @@ -852,6 +865,47 @@
return result;
}

/**
* Cleanup hook invoked from the module wrappers when the React instance is destroyed (e.g. Metro
* reload, orderly Activity teardown). Calls HermesSamplingProfiler.disable(), which synchronously
* joins the sampler's timer thread, so no pthread_kill can fire after the JS thread is torn down.
*
* <p>Correctness depends on RN invoking module invalidate() before joining the JS message queue
* thread. That is the documented contract for both old-arch CatalystInstance shutdown and
* new-arch TurboModule teardown.
*
* <p>See https://github.com/getsentry/sentry-react-native/issues/5441 and
* https://github.com/facebook/hermes/issues/1853.
*/
public void invalidate() {
if (!isProfiling) {
return;
}
// Flip the flag first to make a re-entrant invalidate() a no-op. Then snatch the
// AndroidProfiler reference and null the field so a concurrent stopProfiling() on the
// JS thread sees null and skips its own endAndCollect() on the same instance.
isProfiling = false;
final AndroidProfiler profiler = androidProfiler;
androidProfiler = null;

Check warning on line 889 in packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java

View check run for this annotation

@sentry/warden / warden: find-bugs

TOCTOU race condition can cause NullPointerException in stopProfiling

In `stopProfiling()`, the check `if (androidProfiler != null)` at line 823 and the access at line 824 are separate volatile reads. If `invalidate()` runs on the teardown thread between these reads and nulls `androidProfiler`, `stopProfiling()` will throw a NullPointerException. The `invalidate()` method correctly captures a local copy before nulling, but `stopProfiling()` does not.
Comment thread
antonis marked this conversation as resolved.
Outdated

try {
if (profiler != null) {
final AndroidProfiler.ProfileEndData end = profiler.endAndCollect(false, null);
// The profile is being discarded; delete the trace file to avoid leaking in cacheDir.
if (end != null && end.traceFile != null) {
try {
end.traceFile.delete();
} catch (Throwable ignored) { // NOPMD - File cleanup is best-effort.
}
}
}
HermesSamplingProfiler.disable();
logger.log(SentryLevel.INFO, "Stopped Hermes sampling profiler on React instance destroy.");
} catch (Throwable e) { // NOPMD - Guards the JNI boundary; does not catch native SIGABRT.
logger.log(SentryLevel.WARNING, "Failed to stop Hermes sampling profiler on teardown: " + e);
}
Comment thread
antonis marked this conversation as resolved.
}

private @Nullable String getProguardUuid() {
if (isProguardDebugMetaLoaded) {
return proguardUuid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,10 @@ public void enableShakeDetection() {
public void disableShakeDetection() {
this.impl.disableShakeDetection();
}

@Override
public void invalidate() {
this.impl.invalidate();
super.invalidate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,10 @@ public void enableShakeDetection() {
public void disableShakeDetection() {
this.impl.disableShakeDetection();
}

@Override
public void invalidate() {
this.impl.invalidate();
super.invalidate();
}
}
Loading