Skip to content

Commit d67e2da

Browse files
authored
Merge pull request #231 from winnerspiros/copilot/fix-apk-crash-on-start-another-one
Symbolicate JIT frames in Android native crash handler
2 parents 528cd41 + 96be656 commit d67e2da

3 files changed

Lines changed: 376 additions & 10 deletions

File tree

osu.Android/Native/crash_handler.cpp

Lines changed: 344 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@
2323
// 1. Signal info (signal/code/fault address/tid/thread name/uptime).
2424
// 2. Full register state.
2525
// 3. The faulting thread's backtrace, walked from the saved ucontext via
26-
// the AArch64 frame-pointer chain. This is the diagnostic that
27-
// actually matters — it shows where the crash happened. We do NOT
28-
// use `_Unwind_Backtrace` for this because that walks the *handler*
29-
// thread's stack (which terminates at the kernel signal trampoline
30-
// in vdso, giving "crashHandler → libsigchain → vdso" — useless).
31-
// 4. /proc/self/maps so any addresses dladdr couldn't symbolicate
32-
// (internal-namespace / stripped-.dynsym frames) can still be matched
33-
// to a library and offset post-mortem.
26+
// the AArch64 frame-pointer chain. Each frame is symbolicated by
27+
// trying, in order:
28+
// a. dladdr — resolves PCs that fall inside a loaded ELF (.so).
29+
// b. The Mono `--jitmap` perfmap (`<TMPDIR>/perf-<pid>.map`), which
30+
// names managed JIT methods. Enable by setting Mono env vars
31+
// (see `resolveViaPerfmap` comment below).
32+
// c. /proc/self/maps — labels the containing region (e.g. JIT trampoline
33+
// or stripped .so) with offset, so addresses without a symbol still
34+
// produce actionable output instead of "<unresolved>".
35+
// 4. /proc/self/maps so any addresses still without a symbol after (3c)
36+
// can be cross-checked manually against the full process memory layout.
3437
// 5. The secondary `_Unwind_Backtrace` output for completeness.
3538
//
3639
// Async-signal safety:
@@ -69,6 +72,7 @@
6972
#include <dlfcn.h>
7073
#include <unwind.h>
7174
#include <ucontext.h>
75+
#include <sys/mman.h>
7276
#include <android/log.h>
7377

7478
#include "crash_handler.h"
@@ -148,6 +152,331 @@ static void logcatWrite(const char* msg) {
148152
__android_log_write(ANDROID_LOG_ERROR, CRASH_LOG_TAG, msg);
149153
}
150154

155+
// ----------------------------------------------------------------------------
156+
// Mapping- and JIT-perfmap-based fallback symbolicators.
157+
//
158+
// Why this exists:
159+
// `dladdr` only resolves PCs that fall inside a loaded ELF (.so) image.
160+
// It cannot resolve:
161+
// a. PCs in Mono's JIT/trampoline regions (anonymous `rwxp` mappings) —
162+
// these are where every `<unresolved>` frame in our existing crash logs
163+
// sits when the crash is in managed C# code or a Mono trampoline.
164+
// b. PCs in stripped libraries with no .dynsym entries, where dladdr at
165+
// best returns the library path with no symbol.
166+
//
167+
// We add two fallbacks below, tried in order whenever dladdr fails:
168+
// 1. `resolveViaPerfmap` — search a Mono `--jitmap` file (if present) for
169+
// the managed method that owns this PC. Mono with `--jitmap` (enabled
170+
// via `MONO_ENV_OPTIONS=--jitmap`) writes one line per JIT method to
171+
// `<TMPDIR>/perf-<pid>.map` in the format
172+
// <hex_start_addr> <hex_size> <method_name>
173+
// which we parse line-by-line.
174+
// 2. `resolveViaProcMaps` — find the `/proc/self/maps` entry containing
175+
// the PC and emit `[perms start-end +offset] /path/to/lib` (or a
176+
// `[Mono JIT/trampoline (anon rwxp)]` tag for anonymous executable
177+
// mappings). This always works and turns "<unresolved>" lines into
178+
// actionable output even when no perfmap is present.
179+
//
180+
// To enable the perfmap output (step 1) for a build:
181+
// - Add an `AndroidEnvironment` text file to the project containing:
182+
// MONO_ENV_OPTIONS=--jitmap
183+
// TMPDIR=/storage/emulated/0/Android/data/<pkg>/files
184+
// so Mono writes the perfmap into the same dir as `native_crash.log`.
185+
// `crash_handler.cpp` searches that dir, plus `/tmp` and `/data/local/tmp`,
186+
// plus the directory containing `g_logPath`.
187+
//
188+
// Async-signal safety:
189+
// - All file I/O uses open/read/close (signal-safe).
190+
// - We do NOT use malloc. The perfmap is mmap'd once on first use into a
191+
// static slot; subsequent frame lookups scan that buffer in-place.
192+
// - The /proc/self/maps lookup uses a fixed-size stack buffer and
193+
// re-opens the file once per crash (it is small — typically <1 MB).
194+
// ----------------------------------------------------------------------------
195+
196+
// Lazily-mapped Mono perfmap. Set on first call to resolveViaPerfmap during
197+
// a crash; never unmapped (we're about to die anyway).
198+
static const char* g_perfmapData = nullptr;
199+
static size_t g_perfmapSize = 0;
200+
static volatile sig_atomic_t g_perfmapTried = 0;
201+
202+
static int hexVal(char c) {
203+
if (c >= '0' && c <= '9') return c - '0';
204+
if (c >= 'a' && c <= 'f') return 10 + (c - 'a');
205+
if (c >= 'A' && c <= 'F') return 10 + (c - 'A');
206+
return -1;
207+
}
208+
209+
// Parse "<hex>" up to a non-hex char. Advances *p past the parsed digits.
210+
// Returns the parsed value (0 if no digits parsed).
211+
static uint64_t parseHexAt(const char* s, size_t len, size_t* p) {
212+
uint64_t v = 0;
213+
while (*p < len) {
214+
int d = hexVal(s[*p]);
215+
if (d < 0) break;
216+
v = (v << 4) | (uint64_t)d;
217+
++*p;
218+
}
219+
return v;
220+
}
221+
222+
// Append a NUL-terminated literal to a fixed buffer; advances *bp.
223+
static void appendLit(char* buf, int cap, int* bp, const char* s) {
224+
while (*s && *bp < cap - 1) buf[(*bp)++] = *s++;
225+
}
226+
227+
// Append a length-bounded string (may contain '\n' which we stop at).
228+
static void appendBounded(char* buf, int cap, int* bp, const char* s, int slen) {
229+
for (int i = 0; i < slen && *bp < cap - 1; ++i) {
230+
char c = s[i];
231+
if (c == '\n' || c == '\r') break;
232+
buf[(*bp)++] = c;
233+
}
234+
}
235+
236+
// Try to mmap the Mono perfmap once. Returns true if g_perfmapData is set.
237+
// Searches several plausible locations because Mono's `--jitmap` always
238+
// writes to `<TMPDIR>/perf-<pid>.map` and TMPDIR varies by configuration.
239+
static bool ensurePerfmapLoaded() {
240+
if (g_perfmapTried) return g_perfmapData != nullptr;
241+
g_perfmapTried = 1;
242+
243+
// Build "perf-<pid>.map" once.
244+
char nameBuf[64];
245+
int np = 0;
246+
appendLit(nameBuf, sizeof(nameBuf), &np, "perf-");
247+
{
248+
char tmp[16]; int tp = 0;
249+
long long n = (long long)getpid();
250+
if (n == 0) tmp[tp++] = '0';
251+
while (n > 0 && tp < 15) { tmp[tp++] = (char)('0' + (n % 10)); n /= 10; }
252+
while (tp > 0 && np < (int)sizeof(nameBuf) - 1) nameBuf[np++] = tmp[--tp];
253+
}
254+
appendLit(nameBuf, sizeof(nameBuf), &np, ".map");
255+
nameBuf[np] = '\0';
256+
257+
// Candidate directories, in priority order. The dir containing g_logPath
258+
// is checked first so a build that sets `TMPDIR=<external-files-dir>`
259+
// (the recommended config) finds its perfmap immediately.
260+
const char* tmpEnv = getenv("TMPDIR");
261+
char logDir[kMaxLogPathLen] = {};
262+
if (g_logPath[0] != '\0') {
263+
size_t len = 0;
264+
while (g_logPath[len] != '\0' && len < sizeof(logDir) - 1) {
265+
logDir[len] = g_logPath[len];
266+
++len;
267+
}
268+
// Strip trailing filename component.
269+
while (len > 0 && logDir[len - 1] != '/') { logDir[--len] = '\0'; }
270+
if (len > 1 && logDir[len - 1] == '/') logDir[len - 1] = '\0';
271+
}
272+
273+
const char* dirs[4] = {
274+
logDir[0] ? logDir : nullptr,
275+
tmpEnv,
276+
"/tmp",
277+
"/data/local/tmp",
278+
};
279+
280+
for (int i = 0; i < 4; ++i) {
281+
if (!dirs[i] || dirs[i][0] == '\0') continue;
282+
char path[kMaxLogPathLen + 64];
283+
int p = 0;
284+
for (int k = 0; dirs[i][k] && p < (int)sizeof(path) - 1; ++k) path[p++] = dirs[i][k];
285+
if (p > 0 && path[p - 1] != '/' && p < (int)sizeof(path) - 1) path[p++] = '/';
286+
for (int k = 0; nameBuf[k] && p < (int)sizeof(path) - 1; ++k) path[p++] = nameBuf[k];
287+
path[p] = '\0';
288+
289+
int fd = open(path, O_RDONLY | O_CLOEXEC);
290+
if (fd < 0) continue;
291+
struct stat st;
292+
if (fstat(fd, &st) != 0 || st.st_size <= 0) { close(fd); continue; }
293+
// Cap the mapped size at 64 MB to bound scan time. A perfmap larger
294+
// than that for a single .NET process would be extraordinary.
295+
size_t sz = (size_t)st.st_size;
296+
if (sz > 64u * 1024u * 1024u) sz = 64u * 1024u * 1024u;
297+
void* m = mmap(nullptr, sz, PROT_READ, MAP_PRIVATE, fd, 0);
298+
close(fd);
299+
if (m == MAP_FAILED) continue;
300+
g_perfmapData = static_cast<const char*>(m);
301+
g_perfmapSize = sz;
302+
return true;
303+
}
304+
return false;
305+
}
306+
307+
// Linearly scan the perfmap for the entry containing pc. On hit, writes
308+
// " [JIT] <method_name>+0xOFF" to fd and returns true.
309+
static bool resolveViaPerfmap(int fd, uintptr_t pc) {
310+
if (!ensurePerfmapLoaded()) return false;
311+
const char* d = g_perfmapData;
312+
size_t n = g_perfmapSize;
313+
size_t i = 0;
314+
while (i < n) {
315+
// Each line: "<hex_start> <hex_size> <name>\n"
316+
size_t lineStart = i;
317+
size_t p = i;
318+
uint64_t start = parseHexAt(d, n, &p);
319+
// skip space
320+
while (p < n && d[p] == ' ') ++p;
321+
uint64_t size = parseHexAt(d, n, &p);
322+
while (p < n && d[p] == ' ') ++p;
323+
size_t nameStart = p;
324+
while (p < n && d[p] != '\n') ++p;
325+
size_t nameLen = p - nameStart;
326+
if (size != 0 && pc >= start && pc < start + size) {
327+
char buf[320];
328+
int bp = 0;
329+
appendLit(buf, sizeof(buf), &bp, " [JIT] ");
330+
appendBounded(buf, sizeof(buf), &bp, d + nameStart, (int)nameLen);
331+
appendLit(buf, sizeof(buf), &bp, "+0x");
332+
// hex offset
333+
uint64_t off = pc - start;
334+
char hb[17];
335+
static const char hd[] = "0123456789abcdef";
336+
int hp = 0;
337+
if (off == 0) hb[hp++] = '0';
338+
char rev[17]; int rp = 0;
339+
while (off > 0) { rev[rp++] = hd[off & 0xf]; off >>= 4; }
340+
while (rp > 0) hb[hp++] = rev[--rp];
341+
for (int k = 0; k < hp && bp < (int)sizeof(buf) - 1; ++k) buf[bp++] = hb[k];
342+
buf[bp] = '\0';
343+
ssize_t wn = write(fd, buf, bp);
344+
(void)wn;
345+
return true;
346+
}
347+
// advance past newline
348+
if (p < n && d[p] == '\n') ++p;
349+
// safety: if a line is malformed and we didn't advance, force progress
350+
if (p == lineStart) ++p;
351+
i = p;
352+
}
353+
return false;
354+
}
355+
356+
// Scan /proc/self/maps for the entry containing pc. On hit writes
357+
// " [perms start-end +0xOFF] <path-or-tag>" to fd and returns true.
358+
// Reads the file fresh each call (it's small and signal-safe to do so).
359+
static bool resolveViaProcMaps(int fd, uintptr_t pc) {
360+
int mfd = open("/proc/self/maps", O_RDONLY | O_CLOEXEC);
361+
if (mfd < 0) return false;
362+
363+
// We accumulate a single map line into `line` (max 512 chars; lines on
364+
// Android maps are well below this in practice — long ones are paths to
365+
// /data/app/.../base.apk plus offset, ~280 chars).
366+
char line[512];
367+
int lp = 0;
368+
char chunk[4096];
369+
bool hit = false;
370+
371+
for (;;) {
372+
ssize_t r = read(mfd, chunk, sizeof(chunk));
373+
if (r <= 0) break;
374+
for (ssize_t ci = 0; ci < r; ++ci) {
375+
char c = chunk[ci];
376+
if (c == '\n') {
377+
line[lp] = '\0';
378+
// Parse "start-end perms offset dev inode <path>"
379+
size_t p = 0; size_t lineLen = (size_t)lp;
380+
uint64_t s = parseHexAt(line, lineLen, &p);
381+
if (p < lineLen && line[p] == '-') {
382+
++p;
383+
uint64_t e = parseHexAt(line, lineLen, &p);
384+
if (pc >= s && pc < e) {
385+
// Skip space, capture perms (4 chars).
386+
while (p < lineLen && line[p] == ' ') ++p;
387+
char perms[5] = {};
388+
for (int k = 0; k < 4 && p < lineLen; ++k, ++p) perms[k] = line[p];
389+
// Skip 3 fields (offset dev inode) to reach path.
390+
for (int field = 0; field < 3; ++field) {
391+
while (p < lineLen && line[p] == ' ') ++p;
392+
while (p < lineLen && line[p] != ' ') ++p;
393+
}
394+
while (p < lineLen && line[p] == ' ') ++p;
395+
const char* path = (p < lineLen) ? &line[p] : "";
396+
397+
char outBuf[640];
398+
int bp = 0;
399+
appendLit(outBuf, sizeof(outBuf), &bp, " [");
400+
for (int k = 0; k < 4 && perms[k] && bp < (int)sizeof(outBuf) - 1; ++k)
401+
outBuf[bp++] = perms[k];
402+
appendLit(outBuf, sizeof(outBuf), &bp, " 0x");
403+
// start hex
404+
{
405+
uint64_t v = s;
406+
char hb[17]; int hp = 0;
407+
static const char hd[] = "0123456789abcdef";
408+
if (v == 0) hb[hp++] = '0';
409+
char rev[17]; int rp = 0;
410+
while (v > 0) { rev[rp++] = hd[v & 0xf]; v >>= 4; }
411+
while (rp > 0) hb[hp++] = rev[--rp];
412+
for (int k = 0; k < hp && bp < (int)sizeof(outBuf) - 1; ++k)
413+
outBuf[bp++] = hb[k];
414+
}
415+
appendLit(outBuf, sizeof(outBuf), &bp, "-0x");
416+
{
417+
uint64_t v = e;
418+
char hb[17]; int hp = 0;
419+
static const char hd[] = "0123456789abcdef";
420+
if (v == 0) hb[hp++] = '0';
421+
char rev[17]; int rp = 0;
422+
while (v > 0) { rev[rp++] = hd[v & 0xf]; v >>= 4; }
423+
while (rp > 0) hb[hp++] = rev[--rp];
424+
for (int k = 0; k < hp && bp < (int)sizeof(outBuf) - 1; ++k)
425+
outBuf[bp++] = hb[k];
426+
}
427+
appendLit(outBuf, sizeof(outBuf), &bp, " +0x");
428+
{
429+
uint64_t v = pc - s;
430+
char hb[17]; int hp = 0;
431+
static const char hd[] = "0123456789abcdef";
432+
if (v == 0) hb[hp++] = '0';
433+
char rev[17]; int rp = 0;
434+
while (v > 0) { rev[rp++] = hd[v & 0xf]; v >>= 4; }
435+
while (rp > 0) hb[hp++] = rev[--rp];
436+
for (int k = 0; k < hp && bp < (int)sizeof(outBuf) - 1; ++k)
437+
outBuf[bp++] = hb[k];
438+
}
439+
outBuf[bp < (int)sizeof(outBuf) - 1 ? bp++ : bp] = ']';
440+
if (path[0] != '\0') {
441+
appendLit(outBuf, sizeof(outBuf), &bp, " ");
442+
for (int k = 0; path[k] && bp < (int)sizeof(outBuf) - 1; ++k)
443+
outBuf[bp++] = path[k];
444+
} else if (perms[2] == 'x') {
445+
// Anonymous executable mapping: classic Mono JIT/trampoline region.
446+
appendLit(outBuf, sizeof(outBuf), &bp,
447+
" [Mono JIT/trampoline (anon rwxp)]");
448+
} else {
449+
appendLit(outBuf, sizeof(outBuf), &bp, " [anon]");
450+
}
451+
outBuf[bp] = '\0';
452+
ssize_t wn = write(fd, outBuf, bp);
453+
(void)wn;
454+
hit = true;
455+
}
456+
}
457+
lp = 0;
458+
if (hit) break;
459+
} else if (lp < (int)sizeof(line) - 1) {
460+
line[lp++] = c;
461+
} else {
462+
// overflow: drop until newline
463+
}
464+
}
465+
if (hit) break;
466+
}
467+
close(mfd);
468+
return hit;
469+
}
470+
471+
// Convenience: try perfmap then /proc/self/maps. Writes nothing (and returns
472+
// false) if neither resolves.
473+
static bool resolveUnknownPc(int fd, uintptr_t pc) {
474+
if (pc == 0) return false;
475+
if (resolveViaPerfmap(fd, pc)) return true;
476+
return resolveViaProcMaps(fd, pc);
477+
}
478+
479+
151480
// ----------------------------------------------------------------------------
152481
// Backtrace via libgcc/compiler-rt _Unwind_Backtrace.
153482
// ----------------------------------------------------------------------------
@@ -191,7 +520,7 @@ static _Unwind_Reason_Code unwindCallback(struct _Unwind_Context* ctx, void* arg
191520
writeHex64(st->fd, (uint64_t)off, 1);
192521
writeStr(st->fd, ")");
193522
}
194-
} else {
523+
} else if (!resolveUnknownPc(st->fd, pc)) {
195524
writeStr(st->fd, " <unresolved>");
196525
}
197526
writeStr(st->fd, "\n");
@@ -312,7 +641,12 @@ static void writeFrame(int fd, int frameNo, uintptr_t pc, const char* tagWhenUnr
312641
} else if (tagWhenUnresolved) {
313642
writeStr(fd, " ");
314643
writeStr(fd, tagWhenUnresolved);
315-
} else {
644+
// Even when we have a synthetic tag (e.g. "<NULL function pointer call>"
645+
// or "<LR (return address of NULL call)>"), still try to attach a
646+
// perfmap/maps annotation so we know which JIT region or library the
647+
// PC sits in.
648+
resolveUnknownPc(fd, pc);
649+
} else if (!resolveUnknownPc(fd, pc)) {
316650
writeStr(fd, " <unresolved>");
317651
}
318652
writeStr(fd, "\n");

0 commit comments

Comments
 (0)