Native Crash Symbolication
Parses a Firebase Crashlytics native crash log, automatically downloads the matching
debug libmega.so from Artifactory (cached by SDK version), resolves each PC address
using llvm-addr2line, and outputs an annotated stack trace with root cause analysis.
Usage
/analyze-native-crash --version "16.7(261600931)" # Commit optional — looked up automatically
/analyze-native-crash --version "16.4(261070708)(f33dd33fe8)" --log <file> # Read crash log from a file
/analyze-native-crash --so <path> --log <file> # Use a local libmega.so directly
/analyze-native-crash --version "16.7(261600931)" --ndk <path> # Specify NDK root explicitly
/analyze-native-crash --version "16.7(261600931)" --proxy socks5://127.0.0.1:1080 # Specify proxy
Arguments
| Argument | Description | Example |
|---|---|---|
--version <ver> |
App version string from Crashlytics. The commit hash is optional — if omitted it is resolved automatically from the build number via Artifactory. | --version "16.7(261600931)" or --version "16.4(261070708)(f33dd33fe8)" |
--log <file> |
Path to a crash log text file | --log ~/Downloads/crash.txt |
--so <path> |
Path to a local debug libmega.so (skips auto-download) |
--so ~/Downloads/libmega.so |
--ndk <path> |
NDK root directory | --ndk ~/Library/Android/sdk/ndk/27.1.12297006 |
--proxy <url> |
Proxy for Artifactory downloads | --proxy socks5://127.0.0.1:1080 |
Either --version or --so is required. If both are provided, --so takes precedence.
If --proxy is not provided, try downloading without proxy. If the download fails, inform the user and suggest specifying --proxy.
Execution Steps
Step 1 — Collect the Crash Log
If --log <file> was provided: Read the file using the Read tool.
Otherwise: Use the crash log text pasted by the user in the current conversation message. If no crash log text is visible in the conversation, ask the user to paste it.
Step 2 — Locate llvm-addr2line
If --ndk was provided, use it directly:
<ndk>/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line
Otherwise, resolve via shell in this priority order:
$NDK_ROOT$ANDROID_NDK_ROOT$ANDROID_NDK- Auto-discover:
ls -d ~/Library/Android/sdk/ndk/* | sort -rV | head -1
Sandbox note: Do NOT redirect to
/dev/null(or any path outside the project root) — the Bash sandbox blocks output redirects whose target is outside the project directory and the command fails withLayer 1: Redirect to path outside project directory — /dev/null. To silence stderr, fold it into stdout with2>&1, or just let it print.
NDK="${NDK_ROOT:-${ANDROID_NDK_ROOT:-${ANDROID_NDK:-$(ls -d ~/Library/Android/sdk/ndk/* 2>&1 | sort -rV | head -1)}}}"
ADDR2LINE="$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line"
echo "NDK=$NDK"
echo "ADDR2LINE=$ADDR2LINE"
ls "$ADDR2LINE" && echo "OK" || echo "ERROR: not found"
If not found, halt and tell the user to install the NDK or set NDK_ROOT.
Step 3 — Resolve libmega.so
If --so <path> was provided, use that path directly and skip to Step 4.
Otherwise, auto-download using --version:
Proxy setup (do this first). Do NOT hard-code any proxy host/port — other users may
not need a proxy at all, or may use a different one. Build a PROXY_OPT variable from the
--proxy argument and reuse it in every curl call below. If --proxy was not provided,
PROXY_OPT stays an empty array and curl connects directly:
# Set PROXY only if the user passed --proxy <url>; otherwise leave it unset/empty.
PROXY="<value of --proxy, or empty if not provided>"
# Build PROXY_OPT as an array: empty when no proxy, otherwise (--proxy <url>).
PROXY_OPT=()
[ -n "$PROXY" ] && PROXY_OPT=(--proxy "$PROXY")
If a direct (no-proxy) request fails, tell the user the download failed and suggest
re-running with --proxy <url> (e.g. --proxy socks5://127.0.0.1:1080).
3a — Parse the version string
From --version extract:
MAJOR=16.7(required)BUILD=261600931(required)COMMIT=fa9705cdf5(optional — may be absent, e.g.--version "16.7(261600931)")
3b — Resolve the commit hash (only if not provided)
The commit is not required from the user. The symbol file is ultimately keyed by SDK
version, not commit — the commit is only used to locate buildinfo.txt. If COMMIT is
missing, look it up from the Artifactory release directory using the build number, which is
unique. The folder name has the form {BUILD}_{COMMIT}:
# Only run this if COMMIT was not given on the command line.
COMMIT=$(curl "${PROXY_OPT[@]}" -s "https://artifactory.developers.mega.co.nz/artifactory/api/storage/android-mega/release/v${MAJOR}/" \
| tr ',' '\n' | grep -o "${BUILD}_[0-9a-f]*" | head -1 | cut -d_ -f2)
echo "Resolved COMMIT=$COMMIT"
If this returns nothing (build too old / purged, or the listing needs a proxy), tell the
user and ask them to supply the commit in --version, or pass a local --so instead.
3c — Download buildinfo.txt
BUILDINFO_URL="https://artifactory.developers.mega.co.nz/artifactory/android-mega/release/v${MAJOR}/${BUILD}_${COMMIT}/buildinfo.txt"
curl "${PROXY_OPT[@]}" -sf "$BUILDINFO_URL"
Example URL for version 16.4(261070708)(f33dd33fe8):
https://artifactory.developers.mega.co.nz/artifactory/android-mega/release/v16.4/261070708_f33dd33fe8/buildinfo.txt
The response looks like:
Version: v16.4(261070708)(f33dd33fe8)
Upload Time: Fri Apr 17 19:26:35 NZST 2026
Android: branch(release/v16.4) - commit(f33dd33fe8d94e929ee78a63227a4088866ceec3)
SDK version: 20260417.015208-rel
Extract the SDK version field (e.g. 20260417.015208-rel).
3d — Check local cache
Symbol files are cached by SDK version under the project root:
<project_root>/symbols/<sdk_version>/libmega.so
Check if the file already exists. SDK_VER is not a fixed value — set it to the
SDK version you extracted from buildinfo.txt in 3c (it looks like 20260417.015208-rel):
SDK_VER="<SDK version from buildinfo.txt, e.g. 20260417.015208-rel>"
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
SO_PATH="${PROJECT_ROOT}/symbols/${SDK_VER}/libmega.so"
[ -f "$SO_PATH" ] && ls -lh "$SO_PATH" && echo "Cache hit" || echo "Need to download"
If the file exists, use it directly and skip 3e.
3e — Download libmega.so
The symbol file is stored inside a zip archive in Artifactory. Use the ! syntax to
extract the specific file directly.
First, fetch the REAL size with a HEAD request and report the actual bytes to the user — never assume or hard-code a size, it changes from build to build:
# Reuse the same SDK_VER from 3c/3d — the real value from buildinfo.txt,
# e.g. 20260417.015208-rel. Do not hard-code the example.
SDK_VER="<SDK version from buildinfo.txt, e.g. 20260417.015208-rel>"
SO_URL="https://artifactory.developers.mega.co.nz/artifactory/android-mega/cicd/native-symbol/${SDK_VER}.zip!/arm64-v8a/libmega.so"
BYTES=$(curl "${PROXY_OPT[@]}" -sI "$SO_URL" | awk 'tolower($1) == "content-length:" { gsub(/\r/, "", $2); print $2 }')
echo "Symbol file size: ${BYTES} bytes (~$(( BYTES / 1024 / 1024 )) MB)"
Report the actual size to the user (e.g. The symbol file is 340 MB. Downloading now.).
Confirm before downloading only if the size seems unexpectedly large (well over ~500MB).
Then download. Write the output by redirecting to a path inside the project
(> "$SO_PATH") rather than curl -o, which the execution sandbox blocks:
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
SO_DIR="${PROJECT_ROOT}/symbols/${SDK_VER}"
SO_PATH="${SO_DIR}/libmega.so"
mkdir -p "$SO_DIR"
curl "${PROXY_OPT[@]}" -s "$SO_URL" > "$SO_PATH"
ls -lh "$SO_PATH"
All Artifactory curl calls use the same "${PROXY_OPT[@]}" built in the proxy-setup step
above — pass a proxy only when the user supplied --proxy, otherwise connect directly.
Step 4 — Parse the Crash Log
Read the crash log text (from Step 1) and identify:
- Signal line — the first line, e.g.
Crashed: Thread: SIGSEGV 0x0000000000000008 - Stack frames — lines matching
#NN pc 0xADDR LIBRARY (BuildId: ...):- Extract frame number, hex address, and library name from each line
- Split into
libmega_frames(library ==libmega.so) andother_frames
- BuildId — extract from the first
libmega.soframe, e.g.5642dae2e6736fa132edbe42ad00e99aa9528978
Do this parsing yourself by reading the text — no script needed.
Step 5 — Symbolicate libmega.so Frames
Run a single llvm-addr2line call with all libmega.so addresses at once:
<ADDR2LINE> -f -C -e <SO_PATH> <addr1> <addr2> <addr3> ...
Example:
"$ADDR2LINE" \
-f -C -e "$SO_PATH" \
0x2415144 0x1021848 0x1020614 0x11cd758 0x11ca794 0x109e1e4 0xe98170 0xe98054
The output is pairs of lines per address: function_name\nsource_file:line.
Map each pair back to its frame in order.
A result of ?? / ??:0 means the address is unresolved (stripped symbol table).
Step 6 — Output Annotated Stack Trace
Print the fully annotated stack trace. For each frame:
- SIGSEGV near 0x0 (e.g. 0x8, 0x10, 0x18): near-null offset dereference — accessing a member of a null object. Mark the signal line accordingly.
- SIGSEGV at exactly 0x0: true null pointer dereference.
- Resolved
libmega.soframe: show function name and source location on the next line - Unresolved
libmega.soframe: show address with(no symbol) - Other library frame (libc, etc.): mark as
(external — not symbolicated)
Output format:
Native Crash Symbolication Report
==================================
Signal : SIGSEGV 0x0000000000000008 ← near-null offset (member of null object)
BuildId : 5642dae2e6736fa132edbe42ad00e99aa9528978
SDK Ver : 20260417.015208-rel
Symbolicated Stack Trace:
--------------------------
#00 pc 0x8bea4 libc.so (external — not symbolicated)
#01 pc 0x2415144 libmega.so std::__ndk1::recursive_mutex::lock()
(no source — inlined/system)
#02 pc 0x1021848 libmega.so std::lock_guard<recursive_mutex>::lock_guard(...)
__mutex/lock_guard.h:35
#03 pc 0x1020614 libmega.so LibwebsocketsClient::wsCallback(...)
MEGAchat/src/net/libwebsocketsIO.cpp:535
#04 pc 0x11cd758 libmega.so lws_client_connect_3_connect
libwebsockets/connect3.c:728
...
#09 pc 0x109e1e4 libmega.so mega::LibuvWaiter::wait()
MEGAchat/src/waiter/libuvWaiter.cpp:58
#10 pc 0xe98170 libmega.so megachat::MegaChatApiImpl::loop()
MEGAchat/src/megachatapi_impl.cpp:166
#11 pc 0xe98054 libmega.so megachat::MegaChatApiImpl::threadEntryPoint(void*)
MEGAchat/src/megachatapi_impl.cpp:153
#12 pc 0x8a714 libc.so (external — not symbolicated)
#13 pc 0x7b3b4 libc.so (external — not symbolicated)
Summary
-------
Total frames : 14
libmega frames : 11 (10 resolved, 1 unresolved)
Other frames : 3
Root Cause Analysis
-------------------
<Inspect the top resolved frames and explain the likely root cause.>
<SIGSEGV at small non-zero offset (0x8, 0x10…): accessing member of null object — identify which object and member.>
<SIGSEGV at 0x0: true null pointer dereference — name the function where it occurs.>
<If the crash is deep in libuv/libc, trace upward to find the MEGA code responsible.>
Step 7 — Root Cause & Next Steps
Based on the symbolicated trace:
- SIGSEGV at small non-zero offset (0x8, 0x10, 0x18…) → member access on a null object. The offset corresponds to a field in the object (e.g., mutex at offset 8). Look for use-after-free or missing null check.
- SIGSEGV at exactly 0x0 → null pointer dereference. Identify which pointer in the nearest resolved frame could be null.
- Crash in libuv (
uv__work_done,uv__run_timers, etc.) → the libuv event loop thread crashed. Check what async work or timer callback was executing. - Crash in
megachatapi_impl.cpp→ the MegaChat event loop thread. Look at what operationloop()was executing at the time. - BuildId check: compare the BuildId from the crash log against the
libmega.soyou downloaded. If they differ, symbolication may be wrong — ensure the SDK version matches.
Notes
- If
--proxywas provided, pass it to everycurlcall via--proxy <url>. Otherwise try without proxy; if the download fails, tell the user to specify--proxy. - Symbol files are cached at
<project_root>/symbols/<sdk_version>/libmega.so— reused across sessions. libmega.sofrom Artifactory is ~350MB unstripped with DWARF symbols. A stripped.soreturns??.llvm-addr2lineaccepts multiple addresses in a single invocation — always batch them.- On macOS the toolchain path is
darwin-x86_64; on Linux it islinux-x86_64. libc.soand other system libraries cannot be symbolicated without matching device system symbols.