Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
259 changes: 257 additions & 2 deletions src/backends/native/sentry_crash_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pid>/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/<pid>/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 <dbghelp.h>
# include <fcntl.h>
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
}
Comment thread
cursor[bot] marked this conversation as resolved.
#endif

#if defined(SENTRY_PLATFORM_MACOS) && defined(__aarch64__)
# define SENTRY__STRIP_PAC(addr) ((addr) & 0x00007FFFFFFFFFFFULL)
#else
Expand Down Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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);

Expand Down Expand Up @@ -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;
Expand All @@ -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') {
Expand Down
Loading