Dissecting Signal 11 (SIGSEGV) Crashes in Android NDK Binaries
A step-by-step forensic guide to decoding tombstone files, interpreting fault addresses, and identifying memory corruption in native Android shared libraries.
Understanding Fatal Native Signals
In mobile client engineering, few failure modes are as difficult to diagnose as native signal terminations. Unlike managed exceptions in Kotlin or Java which provide structured stack traces, a SIGSEGV (Segmentation Fault, Signal 11) occurs when a process attempts to access an unallocated or protected memory address.
When this occurs, the Linux kernel terminates the process immediately, dumping register state to the Android logcat or a system tombstone file.
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/raven/raven:13/TP1A.220624.014/8819323:user/release-keys'
Revision: 'MP1.0'
ABI: 'arm64'
Timestamp: 2026-07-14 14:32:09.112349021+0700
Process: com.example.clientapp (pid: 18492, tid: 18530)
Cmdline: com.example.clientapp
pid: 18492, tid: 18530, name: RenderThread >>> com.example.clientapp <<<
uid: 10243
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000018
x0 0000000000000000 x1 0000007b34208a10 x2 0000000000000040 x3 0000007b34208a50
x4 0000000000000008 x5 0000007b34208a90 x6 0000000000000000 x7 0000007b34208ad0
Decoding SEGV_MAPERR vs SEGV_ACCERR
The code field in the signal header provides the first critical clue:
SEGV_MAPERR(Address not mapped to object): The process attempted to read or write to an address that does not exist in its virtual memory space. A classic example is a null-pointer dereference with a struct offset (e.g.fault addr 0x0000000000000018, indicating access to a field at offset 24 bytes from a NULL pointer).SEGV_ACCERR(Invalid permissions for mapped object): The address exists in memory, but the process lacks permissions (e.g. attempting to write to a read-only.rodatasegment, or executing data on a non-executable heap page).
Unstripping Native Shared Libraries with ndk-stack
In production release builds, .so files are stripped of symbol tables (.symtab and .strtab) to reduce APK download size. When the crash occurs, stack traces display only hexadecimal program counter offsets:
#00 pc 000000000004a8bc /data/app/.../libnative_engine.so
#01 pc 000000000003b120 /data/app/.../libnative_engine.so
#02 pc 0000000000089f44 /apex/com.android.runtime/lib64/bionic/libc.so
To resolve these offsets into file names and line numbers:
# Locate the unstripped binary produced during the release build:
# path: app/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a/libnative_engine.so
$ ndk-stack -sym /path/to/unstripped/arm64-v8a/ -dump tombstone.txt
This transforms the opaque offset into concrete code lines:
#00 0x000000000004a8bc /workspace/cpp/graphics_context.cpp:142
GraphicsContext::BindTexture(TextureHandle* handle)
#01 0x000000000003b120 /workspace/cpp/render_pipeline.cpp:89
RenderPipeline::DrawFrame()
Common Native Root Causes & Defensive Patterns
- Stale JNI Global References: Forgetting to promote local JNI references when passing objects across asynchronous native threads.
- NEON SIMD Vector Alignment: Using 128-bit load instructions (
vld1q_f32) against unaligned byte buffers on ARM64 hardware. - Use-After-Free in Callback Queues: Accessing C++ object instances after their corresponding Java
finalize()or Kotlinclose()method has deallocated the native pointer.
Summary
Resolving native crashes requires establishing a disciplined symbol archive pipeline during CI/CD builds and systematically inspecting register fault addresses. When automated tools fail to unstrip native stack frames, manual forensic triage uncovers the memory boundary violations beneath the surface.
Need Diagnostic Assistance on a Similar Issue?
Think Harbor Base specializes in isolating native stack corruption, high-impact ANRs, and memory regression. Inquire about an engineering audit for your application.