Skip to content

TombstoneMerged native crashes: merge discards the NDK crashing thread; managed frames stay obfuscated #5770

Description

@runningcode

Update — control experiment reframes this issue (read first)

The original framing ("native crashes lose symbolication and ProGuard deobfuscation") was too strong. A control experiment shows native symbolication and the uploaded symbols/sources were never broken:

  • I changed the sample's Native Crash to a genuine null-pointer deref inside a named C function (trigger_null_deref() in native-sample.cpp) instead of raise(SIGSEGV).
  • On main (no fix) the crash symbolicates fine: trigger_null_deref (native-sample.cpp:13) incl. the source line.
  • With the fix it also symbolicates. Same issue, same frame.

So the original "unsymbolicated/obfuscated native crash" was an artifact of the sample's raise(SIGSEGV)-from-JNI crash — a managed-origin crash whose tombstone stack is the ART/OAT managed chain with no app-native frame — not a symbolication or symbol-source failure.

What the merge actually does (genuine native crash, both TombstoneMerged, both symbolicate)

⇒ For a real native crash the current PR is neutral-to-worse (loses call-chain context), not the clear win originally claimed. Recommend holding PR #5771 and reframing toward the deobfuscation gap below.

The real readability gap

Managed frames in a TombstoneMerged native event are not ProGuard-deobfuscated, because the event platform == "native". This affects:

  1. Managed-origin crashes (e.g. raise() from JNI): the whole crashing stack is obfuscated ART/OAT frames — unreadable.
  2. Genuine native crashes: the native crash frame resolves, but the surrounding managed call chain (d3.v.ag6.f.a) stays obfuscated.

If those java-classifiable frames were deobfuscated (they carry the correct ProGuard image, e.g. c44ea147-…), the mixed stack would be the best of both worlds.

Mechanism (still accurate)

sentry-android-core/.../TombstoneIntegration.java, mergeNativeCrashes (~L287–320) overwrites the Native SDK event's exceptions/threads/debugMeta with the tombstone's. This is the "discards the NDK crashing thread" behavior — real, but per the control experiment it is not what makes native crashes unreadable.

Evidence

Genuine null-deref (trigger_null_deref), Pixel 10 / Android 17, TombstoneMerged:

  • main event 906e4e40…: Most-relevant frame trigger_null_deref (native-sample.cpp:13); full stack = obfuscated managed chain → trigger_null_deref.
  • fix event 99a2e924…: <unknown><unknown>trigger_null_deref (native-sample.cpp:13).
  • raise()-from-JNI (the original repro): main → obfuscated managed stack; fix → degenerate native stack (raise + garbage). Neither useful — a synthetic-crash artifact.

Options

  1. Deobfuscate managed frames in the merged native event (preferred) — makes main's mixed stack fully readable; strictly better than either current behavior.
  2. Keep the NDK crashing thread (current PR fix(android): Preserve NDK stack on tombstone merge (JAVA-645) [on hold] #5771) — only helps the synthetic raise()-from-JNI case, and drops context for real native crashes.

Repro

Sample (sentry-samples-android), release + SAGP (-PuseSagp, uploadNativeSymbols=true, includeProguardMapping=true), io.sentry.tombstone.enable=true. Compare a raise(SIGSEGV) crash vs a real in-.so null-deref.

Not a server bug

sentry and symbolicator behave correctly (verified from raw event JSON).

Metadata

Metadata

Assignees

Fields

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