diff --git a/CHANGELOG.md b/CHANGELOG.md index a80a420c8..b63503929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Native/Linux: parse minidump-writer ELF build-id notes with `sentry__elf_find_note`. ([#2055](https://github.com/getsentry/sentry-native/pull/2055)) - Native: Read frame records at pointer width in the crash daemon's frame-pointer walk, so 32-bit targets no longer read two stack slots per pointer. ([#2052](https://github.com/getsentry/sentry-native/pull/2052)) +- Native: Report ARM32 registers for Linux crash events, and walk both r11-based ARM32 frame-record shapes (GCC's and clang's; Thumb r7 chains are not walked) in the crash daemon. ([#2053](https://github.com/getsentry/sentry-native/pull/2053)) - Prevent backend state races when `sentry_reinstall_backend` runs concurrently with scope observer callbacks. ([#2041](https://github.com/getsentry/sentry-native/pull/2041)) - `sentry_set_trace` omits `parent_span_id` when the caller does not provide one, instead of serializing it as `null`. ([#2047](https://github.com/getsentry/sentry-native/pull/2047)) - Native/Linux i386: write valid thread stack descriptors to minidumps when stack addresses use the upper half of the 32-bit address space. ([#2054](https://github.com/getsentry/sentry-native/pull/2054)) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 980de372b..6894b5f79 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -50,6 +50,136 @@ # if defined(SENTRY_PLATFORM_LINUX) # include "unwinder/sentry_unwinder.h" # endif +# if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +/** + * Read one logical line of `/proc//maps`. A pathname longer than the + * buffer is truncated, and the rest of that line is consumed rather than + * handed to the next `fgets` as if it were a mapping of its own. + */ +static bool +read_maps_line(FILE *f, char *line, size_t size) +{ + if (!fgets(line, size, f)) { + return false; + } + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') { + return true; + } + int c; + while ((c = fgetc(f)) != EOF && c != '\n') { } + return true; +} +# endif +# if defined(__arm__) +/** + * Every mapping of the crashed process, with its permission bits, recorded + * by `vma_capture` immediately after a thread's stack bytes have been copied + * out. Unlike the module inventory this includes anonymous and special + * mappings, so the ARM32 frame-pointer walk can answer "is this a return + * address" (executable, not writable) and "is this a saved frame pointer" + * (writable, above the current frame) without guessing. It is read after + * the stack copy on purpose: only the crashing thread is stopped while the + * daemon works, so the freshest view of the mappings is the one taken next + * to the bytes it will be asked about. `g_vma_complete` is true only when + * the whole file was read and recorded. + * ARM32 only: nothing else consumes it, so nothing else pays for it. + */ +typedef struct { + uint64_t start; + uint64_t end; + bool write; + bool exec; +} vma_range_t; +static vma_range_t *g_vmas = NULL; +static size_t g_vma_count = 0; +static size_t g_vma_capacity = 0; +static bool g_vma_alloc_failed = false; +static bool g_vma_complete = false; + +static bool +vma_record(uint64_t start, uint64_t end, const char *perms) +{ + if (g_vma_alloc_failed) { + return false; + } + if (g_vma_count == g_vma_capacity) { + size_t capacity = g_vma_capacity ? g_vma_capacity * 2 : 256; + if (capacity < g_vma_capacity + || capacity > SIZE_MAX / sizeof(vma_range_t)) { + g_vma_alloc_failed = true; + return false; + } + vma_range_t *grown = sentry_malloc(capacity * sizeof(vma_range_t)); + if (!grown) { + g_vma_alloc_failed = true; + return false; + } + if (g_vmas) { + memcpy(grown, g_vmas, g_vma_count * sizeof(vma_range_t)); + sentry_free(g_vmas); + } + g_vmas = grown; + g_vma_capacity = capacity; + } + g_vmas[g_vma_count].start = start; + g_vmas[g_vma_count].end = end; + g_vmas[g_vma_count].write = perms[1] == 'w'; + g_vmas[g_vma_count].exec = perms[2] == 'x'; + g_vma_count++; + return true; +} + +static const vma_range_t * +vma_containing(uint64_t addr) +{ + for (size_t i = 0; i < g_vma_count; i++) { + if (addr >= g_vmas[i].start && addr < g_vmas[i].end) { + return &g_vmas[i]; + } + } + return NULL; +} + +/** + * Read `/proc//maps` into the VMA table, replacing whatever it held. + * Called right after a stack copy, so the walk judges the copied bytes + * against the mappings as they were at (nearly) the same moment. + */ +static void +vma_capture(pid_t pid) +{ + g_vma_count = 0; + g_vma_complete = false; + g_vma_alloc_failed = false; + + char maps_path[64]; + snprintf(maps_path, sizeof(maps_path), "/proc/%d/maps", pid); + FILE *f = fopen(maps_path, "r"); + if (!f) { + SENTRY_WARNF("Failed to open %s for mapping capture", maps_path); + return; + } + char line[1024]; + bool ok = true; + while (read_maps_line(f, line, sizeof(line))) { + unsigned long long start, end; + char perms[5]; + if (sscanf(line, "%llx-%llx %4s", &start, &end, perms) != 3) { + ok = false; // a line this parser cannot read is a mapping it + // does not know about + continue; + } + if (!vma_record(start, end, perms)) { + ok = false; + } + } + g_vma_complete = ok && !ferror(f); + fclose(f); + SENTRY_DEBUGF("Captured %zu mappings (%s) from %s", g_vma_count, + g_vma_complete ? "complete" : "INCOMPLETE", maps_path); +} +# endif #elif defined(SENTRY_PLATFORM_WINDOWS) # include # include @@ -420,6 +550,41 @@ build_registers_from_ctx(const sentry_crash_context_t *ctx, size_t thread_idx) registers, "sp", sentry__value_new_addr(uctx->uc_mcontext.sp)); sentry_value_set_by_key( registers, "pc", sentry__value_new_addr(uctx->uc_mcontext.pc)); +# elif defined(__arm__) + sentry_value_set_by_key( + registers, "r0", sentry__value_new_addr(uctx->uc_mcontext.arm_r0)); + sentry_value_set_by_key( + registers, "r1", sentry__value_new_addr(uctx->uc_mcontext.arm_r1)); + sentry_value_set_by_key( + registers, "r2", sentry__value_new_addr(uctx->uc_mcontext.arm_r2)); + sentry_value_set_by_key( + registers, "r3", sentry__value_new_addr(uctx->uc_mcontext.arm_r3)); + sentry_value_set_by_key( + registers, "r4", sentry__value_new_addr(uctx->uc_mcontext.arm_r4)); + sentry_value_set_by_key( + registers, "r5", sentry__value_new_addr(uctx->uc_mcontext.arm_r5)); + sentry_value_set_by_key( + registers, "r6", sentry__value_new_addr(uctx->uc_mcontext.arm_r6)); + sentry_value_set_by_key( + registers, "r7", sentry__value_new_addr(uctx->uc_mcontext.arm_r7)); + sentry_value_set_by_key( + registers, "r8", sentry__value_new_addr(uctx->uc_mcontext.arm_r8)); + sentry_value_set_by_key( + registers, "r9", sentry__value_new_addr(uctx->uc_mcontext.arm_r9)); + sentry_value_set_by_key( + registers, "r10", sentry__value_new_addr(uctx->uc_mcontext.arm_r10)); + sentry_value_set_by_key( + registers, "fp", sentry__value_new_addr(uctx->uc_mcontext.arm_fp)); + sentry_value_set_by_key( + registers, "ip", sentry__value_new_addr(uctx->uc_mcontext.arm_ip)); + sentry_value_set_by_key( + registers, "sp", sentry__value_new_addr(uctx->uc_mcontext.arm_sp)); + sentry_value_set_by_key( + registers, "lr", sentry__value_new_addr(uctx->uc_mcontext.arm_lr)); + sentry_value_set_by_key( + registers, "pc", sentry__value_new_addr(uctx->uc_mcontext.arm_pc)); + sentry_value_set_by_key( + registers, "cpsr", sentry__value_new_addr(uctx->uc_mcontext.arm_cpsr)); # endif #elif defined(SENTRY_PLATFORM_MACOS) @@ -606,6 +771,41 @@ read_stack_value(const uint8_t *stack_buf, uint64_t stack_start, return true; } +#if defined(__arm__) +/** + * Does a (saved fp, return address) pair look like a frame record? + * + * Judged against the crashed process's mappings. A return address must be in + * an executable mapping that is not writable: that is what separates code + * from a stack (writable, and on some systems executable too) and from the + * heap. A non-zero saved fp must be above the current frame and in a + * writable mapping: stacks are writable, code is not, so the other compiler's + * record shape - whose "saved fp" is really a return address - is rejected + * even when code sits numerically below the stack. The walk reads frame + * records out of the one captured window, so a chain is followed only while + * it stays inside that window; a signal stack or a split stack that links to + * a lower or non-contiguous segment ends the walk. Not walked either: JIT + * code in writable mappings. + */ +static bool +arm_frame_record_plausible( + uint64_t saved_fp, uint64_t return_addr, uint64_t current_fp) +{ + const vma_range_t *code = vma_containing(return_addr); + if (!code || !code->exec || code->write) { + return false; + } + if (saved_fp == 0) { + return true; + } + if (saved_fp <= current_fp) { + return false; + } + const vma_range_t *stack = vma_containing(saved_fp); + return stack && stack->write; +} +#endif + #if defined(SENTRY_PLATFORM_MACOS) && defined(__aarch64__) # define SENTRY__STRIP_PAC(addr) ((addr) & 0x00007FFFFFFFFFFFULL) #else @@ -865,6 +1065,9 @@ build_stacktrace_for_thread( stack_size = (uint64_t)bytes_read; SENTRY_DEBUGF( "Read %zd bytes of stack from process %d", bytes_read, pid); +# if defined(__arm__) + vma_capture(pid); +# endif } } } @@ -1105,6 +1308,16 @@ build_stacktrace_for_thread( if (stack_buf && fp != 0 && frame_count < MAX_STACK_FRAMES) { uint64_t current_fp = fp; int walk_count = 0; +#if defined(__arm__) + // Fail closed: without a complete picture of the mappings neither + // frame-record shape can be told from the other, and guessing would + // emit fabricated frames rather than a shorter, honest stack. + if (!g_vma_complete) { + SENTRY_TRACE( + "Mappings incomplete; skipping the ARM32 frame-pointer walk"); + walk_count = MAX_STACK_FRAMES; + } +#endif // Check if FP is within captured stack range uint64_t stack_end = stack_start + stack_size; @@ -1118,6 +1331,44 @@ build_stacktrace_for_thread( uint64_t saved_fp = 0; uint64_t return_addr = 0; +#if defined(__arm__) + // Two frame-record shapes coexist in one ARM32 process. GCC's + // `push {.., fp, lr}; add fp, sp, #N` leaves fp on the LR slot + // ([fp-4] = saved fp, [fp] = return address), while clang/LLVM's + // leaves it on the saved-fp slot ([fp] = saved fp, [fp+4] = return + // address). Read both records and keep the one shaped like a + // frame. + { + uint64_t fp_llvm = 0, ret_llvm = 0, fp_gcc = 0, ret_gcc = 0; + bool ok_llvm = read_stack_value(stack_buf, stack_start, + stack_size, current_fp, &fp_llvm) + && read_stack_value(stack_buf, stack_start, stack_size, + current_fp + sizeof(uintptr_t), &ret_llvm); + bool ok_gcc + = read_stack_value(stack_buf, stack_start, stack_size, + current_fp - sizeof(uintptr_t), &fp_gcc) + && read_stack_value(stack_buf, stack_start, stack_size, + current_fp, &ret_gcc); + if (ok_llvm + && arm_frame_record_plausible( + fp_llvm, ret_llvm, current_fp)) { + saved_fp = fp_llvm; + return_addr = ret_llvm; + } else if (ok_gcc + && arm_frame_record_plausible( + fp_gcc, ret_gcc, current_fp)) { + saved_fp = fp_gcc; + return_addr = ret_gcc; + } else { + SENTRY_TRACEF("No frame record at FP=0x%llx (stack: 0x%llx " + "- 0x%llx)", + (unsigned long long)current_fp, + (unsigned long long)stack_start, + (unsigned long long)stack_end); + break; + } + } +#else // Read saved frame pointer and return address // Frame layout: [FP] = saved FP, [FP + pointer size] = return addr if (!read_stack_value(stack_buf, stack_start, stack_size, @@ -1135,6 +1386,7 @@ build_stacktrace_for_thread( (unsigned long long)(current_fp + sizeof(uintptr_t))); break; } +#endif saved_fp = SENTRY__STRIP_PAC(saved_fp); return_addr = SENTRY__STRIP_PAC(return_addr); @@ -1719,8 +1971,7 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) char line[1024]; ctx->module_count = 0; - while (fgets(line, sizeof(line), f) - && ctx->module_count < SENTRY_CRASH_MAX_MODULES) { + while (read_maps_line(f, line, sizeof(line))) { // Parse line: "start-end perms offset dev inode pathname" unsigned long long start, end, offset; @@ -1734,6 +1985,10 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) continue; } + if (ctx->module_count >= SENTRY_CRASH_MAX_MODULES) { + break; + } + // Must have a valid pathname (not [stack], [heap], etc.) if (pathname_offset <= 0 || line[pathname_offset] == '\0' || line[pathname_offset] == '[' || line[pathname_offset] == '\n') {