From 976905b548c75da22181a4678641d321ccef0c9e Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Mon, 27 Apr 2026 11:05:18 +0200 Subject: [PATCH 1/7] fix(native): Fix smaller findings for Linux minidump writer --- CHANGELOG.md | 7 + .../native/minidump/sentry_minidump_format.h | 3 +- .../native/minidump/sentry_minidump_linux.c | 196 ++++++++++++------ 3 files changed, 139 insertions(+), 67 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3a4ff25e..bcecd7a16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ - Metrics are enabled by default. This behavior first appeared in `0.13.5` and is now documented as part of the `0.14.0` behavior. Applications that do not want to send metrics must explicitly opt out with `sentry_options_set_enable_metrics(options, false)`. ([#1609](https://github.com/getsentry/sentry-native/pull/1609)) - Structured logs are enabled by default. This behavior first appeared in `0.13.9` and is now documented as part of the `0.14.0` behavior. Applications that do not want to capture structured logs must explicitly opt out with `sentry_options_set_enable_logs(options, false)`. ([#1673](https://github.com/getsentry/sentry-native/pull/1673)) +**Fixes**: + +- Native/Linux: correct `MD_LINUX_MAPS` stream type (was tagged as `MD_LINUX_AUXV`). +- Native/Linux: drop non-ELF mappings (e.g. `/dev/shm/*`, `(deleted)` files) from the minidump module list. +- Native/Linux: merge non-contiguous mappings of the same shared library into a single module, and use the offset==0 mapping as `base_of_image`. Fixes duplicate `ld-linux` entries that confused some debuggers (notably Windows LLDB) reading Linux ARM64 minidumps. +- Native/Linux: log when `uname()` is blocked (sandbox/seccomp) and fall back to `/proc/sys/kernel/osrelease` for the OS version. + ## 0.13.9 **Features**: diff --git a/src/backends/native/minidump/sentry_minidump_format.h b/src/backends/native/minidump/sentry_minidump_format.h index 90bed5eb8..7f63d5cae 100644 --- a/src/backends/native/minidump/sentry_minidump_format.h +++ b/src/backends/native/minidump/sentry_minidump_format.h @@ -36,7 +36,8 @@ typedef enum { MINIDUMP_STREAM_THREAD_NAMES = 24, // ThreadNamesStream (Crashpad/Windows) MINIDUMP_STREAM_LINUX_CPU_INFO = 0x47670003, MINIDUMP_STREAM_LINUX_PROC_STATUS = 0x47670004, - MINIDUMP_STREAM_LINUX_MAPS = 0x47670008, + MINIDUMP_STREAM_LINUX_AUXV = 0x47670008, + MINIDUMP_STREAM_LINUX_MAPS = 0x47670009, } minidump_stream_type_t; // CPU types (MINIDUMP_PROCESSOR_ARCHITECTURE) diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index a30e19e24..7d9e17923 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -19,7 +19,6 @@ # include # include -# include "../../../modulefinder/sentry_modulefinder_linux.h" # include "sentry_alloc.h" # include "sentry_logger.h" # include "sentry_minidump_common.h" @@ -545,7 +544,9 @@ write_system_info_stream(minidump_writer_t *writer, minidump_directory_t *dir) // Populate OS version from uname(), matching Crashpad behavior struct utsname uts; char csd_version[512] = ""; + bool have_uname = false; if (uname(&uts) == 0) { + have_uname = true; int major = 0, minor = 0, patch = 0; sscanf(uts.release, "%d.%d.%d", &major, &minor, &patch); sysinfo.major_version = (uint32_t)major; @@ -554,6 +555,37 @@ write_system_info_stream(minidump_writer_t *writer, minidump_directory_t *dir) snprintf(csd_version, sizeof(csd_version), "%s %s %s %s", uts.sysname, uts.release, uts.version, uts.machine); + } else { + SENTRY_WARNF("uname() failed: %s — falling back to " + "/proc/sys/kernel/osrelease", + strerror(errno)); + } + + // Fallback when uname succeeds but release didn't parse, or uname is + // blocked entirely (sandboxed/seccomp environments often return "(none)" + // or disable the syscall). + if (sysinfo.major_version == 0 && sysinfo.minor_version == 0 + && sysinfo.build_number == 0) { + int rfd = open("/proc/sys/kernel/osrelease", O_RDONLY); + if (rfd >= 0) { + char buf[64] = { 0 }; + ssize_t n = read(rfd, buf, sizeof(buf) - 1); + close(rfd); + if (n > 0) { + int major = 0, minor = 0, patch = 0; + if (sscanf(buf, "%d.%d.%d", &major, &minor, &patch) >= 1) { + sysinfo.major_version = (uint32_t)major; + sysinfo.minor_version = (uint32_t)minor; + sysinfo.build_number = (uint32_t)patch; + } + } + } + if (sysinfo.major_version == 0 && sysinfo.minor_version == 0 + && sysinfo.build_number == 0) { + SENTRY_WARNF( + "OS version unavailable (uname %s, /proc fallback failed)", + have_uname ? "release unparseable" : "blocked"); + } } sysinfo.csd_version_rva = write_minidump_string(writer, csd_version); if (!sysinfo.csd_version_rva) { @@ -812,6 +844,26 @@ write_thread_context( # endif } +/** + * Quickly verify a file is an ELF binary by reading its magic bytes. + * Used to filter out non-ELF mappings (e.g. /dev/shm/*, deleted files, + * sentry-native's own IPC shared memory) from the module list — LLDB and + * other consumers can choke on entries that look like modules but aren't. + */ +static bool +is_elf_file(const char *path) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) { + return false; + } + unsigned char magic[SELFMAG]; + bool is_elf = read(fd, magic, SELFMAG) == (ssize_t)SELFMAG + && memcmp(magic, ELFMAG, SELFMAG) == 0; + close(fd); + return is_elf; +} + /** * Extract Build ID from ELF file * Returns the Build ID length, or 0 if not found @@ -1499,11 +1551,22 @@ typedef struct { } resolved_module_t; /** - * Resolve all mappings into deduplicated modules using the shared modulefinder - * merge logic (sentry__module_mapping_push). For each unique named file, all - * contiguous /proc/pid/maps segments are merged into a single sentry_module_t, - * from which we extract base_of_image and size_of_image the same way the - * in-process modulefinder does. + * Resolve all mappings into deduplicated modules. + * + * For each named, readable mapping that points at a real ELF file, group + * every mapping of the same file (regardless of order in /proc/pid/maps) + * into a single resolved_module_t. This matches Crashpad's behavior and + * avoids two pitfalls that confuse downstream consumers (notably LLDB's + * ModuleList on Windows reading Linux dumps): + * + * 1. Non-ELF entries (e.g. /dev/shm/* IPC files, "(deleted)" files) being + * emitted as modules with bogus zero-id CV records. + * 2. The same shared library appearing twice when its segments are + * separated by an unrelated mapping (e.g. ld-linux-aarch64.so.1 split + * across non-contiguous /proc/maps lines). + * + * base_of_image is set to the address of the offset==0 mapping — the real + * ELF load address — which is what Breakpad/Crashpad/rust-minidump expect. */ static size_t resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, @@ -1511,18 +1574,15 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, { size_t module_count = 0; - // Track the current module being built via sentry__module_mapping_push. - // When the filename changes, we finalize the current module and start a - // new one — /proc/pid/maps groups mappings by file, so consecutive lines - // with the same name belong to the same ELF. - sentry_module_t current; - memset(¤t, 0, sizeof(current)); - char *current_name = NULL; + // Names we've already classified as non-ELF, so we don't repeatedly open + // the same file (e.g. /dev/shm/sentry-IPC has multiple segments). + const char *skip_names[SENTRY_CRASH_MAX_MODULES]; + size_t skip_count = 0; for (size_t i = 0; i < writer->mapping_count; i++) { const memory_mapping_t *mapping = &writer->mappings[i]; - // Skip anonymous mappings and special kernel mappings + // Skip anonymous and special kernel mappings ([stack], [vdso], etc.) if (mapping->name[0] == '\0' || mapping->name[0] == '[') { continue; } @@ -1531,64 +1591,68 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, continue; } - // Build a sentry_parsed_module_t to feed to the shared merge function - sentry_parsed_module_t parsed; - parsed.start = mapping->start; - parsed.end = mapping->end; - parsed.offset = mapping->offset; - memcpy(parsed.permissions, mapping->permissions, 5); - parsed.file.ptr = mapping->name; - parsed.file.len = strlen(mapping->name); - - bool same_file - = current_name && strcmp(current_name, mapping->name) == 0; - - if (!same_file) { - // Finalize the previous module (if any) - if (current_name && current.num_mappings > 0 - && module_count < max_modules) { - const sentry_mapped_region_t *first = ¤t.mappings[0]; - const sentry_mapped_region_t *last - = ¤t.mappings[current.num_mappings - 1]; - resolved_module_t *mod = &modules[module_count]; - mod->name = current_name; - mod->base = first->addr; - mod->end = last->addr + last->size; - mod->build_id_len = 0; - mod->soname[0] = '\0'; - mod->elf_size = 0; - module_count++; + // Already classified as non-ELF? Cheap pointer compare first + // (mapping->name is stable for the writer's lifetime), then strcmp. + bool already_skipped = false; + for (size_t j = 0; j < skip_count; j++) { + if (skip_names[j] == mapping->name + || strcmp(skip_names[j], mapping->name) == 0) { + already_skipped = true; + break; } + } + if (already_skipped) { + continue; + } - // Start a new module - memset(¤t, 0, sizeof(current)); - current_name = (char *)mapping->name; + // Find an existing module entry by name. This merges across + // non-contiguous /proc/maps lines — fixes duplicate ld-linux entries. + resolved_module_t *mod = NULL; + for (size_t j = 0; j < module_count; j++) { + if (strcmp(modules[j].name, mapping->name) == 0) { + mod = &modules[j]; + break; + } } - // Use a consistent dummy inode so the merge function doesn't reject - // this mapping as belonging to a different file - parsed.inode = current.mappings_inode; - if (current.num_mappings == 0) { - parsed.inode = 1; // Any non-zero value for the first mapping + if (mod) { + // Extend the existing module to cover this segment. + if (mapping->end > mod->end) { + mod->end = mapping->end; + } + // base_of_image must be the address of the offset==0 mapping — + // the actual ELF load address. Prefer it when found. + if (mapping->offset == 0) { + mod->base = mapping->start; + } else if (mapping->start < mod->base) { + // No offset==0 mapping seen yet; track lowest address as a + // fallback (will be overridden when the offset==0 mapping + // arrives). + mod->base = mapping->start; + } + continue; + } + + // First time we see this file — confirm it's actually an ELF before + // adding it as a module. This drops sentry-native's own IPC shm, + // deleted semaphores, and any other non-ELF named mapping. + if (!is_elf_file(mapping->name)) { + if (skip_count < SENTRY_CRASH_MAX_MODULES) { + skip_names[skip_count++] = mapping->name; + } + SENTRY_DEBUGF("skipping non-ELF mapping: %s", mapping->name); + continue; + } + + if (module_count >= max_modules) { + continue; } - sentry__module_mapping_push(¤t, &parsed); - } - - // Finalize the last module - if (current_name && current.num_mappings > 0 - && module_count < max_modules) { - const sentry_mapped_region_t *first = ¤t.mappings[0]; - const sentry_mapped_region_t *last - = ¤t.mappings[current.num_mappings - 1]; - resolved_module_t *mod = &modules[module_count]; - mod->name = current_name; - mod->base = first->addr; - mod->end = last->addr + last->size; - mod->build_id_len = 0; - mod->soname[0] = '\0'; - mod->elf_size = 0; - module_count++; + mod = &modules[module_count++]; + memset(mod, 0, sizeof(*mod)); + mod->name = (char *)mapping->name; + mod->base = mapping->start; + mod->end = mapping->end; } // Extract Build IDs, ELF sizes, and SONAMEs for each resolved module From 82219eaeacbb6c88bb31d5bf285f2c32fd1c93c3 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Mon, 4 May 2026 11:59:19 +0200 Subject: [PATCH 2/7] Add missing streams --- .../native/minidump/sentry_minidump_format.h | 33 + .../native/minidump/sentry_minidump_linux.c | 591 ++++++++++++++++-- 2 files changed, 561 insertions(+), 63 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_format.h b/src/backends/native/minidump/sentry_minidump_format.h index 7f63d5cae..eaf5e16ef 100644 --- a/src/backends/native/minidump/sentry_minidump_format.h +++ b/src/backends/native/minidump/sentry_minidump_format.h @@ -36,8 +36,12 @@ typedef enum { MINIDUMP_STREAM_THREAD_NAMES = 24, // ThreadNamesStream (Crashpad/Windows) MINIDUMP_STREAM_LINUX_CPU_INFO = 0x47670003, MINIDUMP_STREAM_LINUX_PROC_STATUS = 0x47670004, + MINIDUMP_STREAM_LINUX_LSB_RELEASE = 0x47670005, + MINIDUMP_STREAM_LINUX_CMD_LINE = 0x47670006, + MINIDUMP_STREAM_LINUX_ENVIRON = 0x47670007, MINIDUMP_STREAM_LINUX_AUXV = 0x47670008, MINIDUMP_STREAM_LINUX_MAPS = 0x47670009, + MINIDUMP_STREAM_LINUX_DSO_DEBUG = 0x4767000A, } minidump_stream_type_t; // CPU types (MINIDUMP_PROCESSOR_ARCHITECTURE) @@ -469,4 +473,33 @@ typedef struct { } PACKED_ATTR minidump_thread_name_list_t; PACKED_STRUCT_END +/** + * Linux DSO debug structures (MD_LINUX_DSO_DEBUG, 0x4767000A) + * + * Mirrors Breakpad's MDRawLinkMap64 / MDRawDebug64. LLDB and rust-minidump + * use this stream to enumerate loaded shared objects when LinuxAuxv alone + * isn't enough (e.g. modules loaded via dlopen after process startup that + * aren't in /proc//maps in a usable order). + */ +PACKED_STRUCT_BEGIN +typedef struct { + uint64_t addr; // l_addr from struct link_map + minidump_rva_t name; // RVA to MINIDUMP_STRING with the SO path + uint32_t _pad; + uint64_t ld; // l_ld from struct link_map (PT_DYNAMIC of this DSO) +} PACKED_ATTR minidump_link_map64_t; +PACKED_STRUCT_END + +PACKED_STRUCT_BEGIN +typedef struct { + uint32_t version; // r_debug.r_version + minidump_rva_t map; // RVA to array of minidump_link_map64_t + uint32_t dso_count; // number of entries in map + uint32_t _pad; + uint64_t brk; // r_debug.r_brk + uint64_t ldbase; // r_debug.r_ldbase + uint64_t dynamic; // address of the program's _DYNAMIC section +} PACKED_ATTR minidump_debug64_t; +PACKED_STRUCT_END + #endif diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 7d9e17923..b798a8e45 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -5,6 +5,7 @@ # include # include # include +# include // ElfW, struct r_debug, struct link_map (DSO debug stream) # include # include # include @@ -103,6 +104,16 @@ typedef struct { [16]; // From /proc/[pid]/task/[tid]/comm size_t thread_count; + // Thread-stack memory descriptors recorded as we write the ThreadList + // stream. We replay them into MemoryListStream so consumers (notably + // LLDB's ProcessMinidump on Linux) can find stack bytes by virtual + // address — without this LLDB knows the stack region exists (from + // LinuxMaps) but cannot read its contents and unwinding stops after + // frame 0. Breakpad's MinidumpWriter does the equivalent via + // memory_blocks_.push_back(thread->stack). + minidump_memory_descriptor_t thread_stacks[SENTRY_CRASH_MAX_THREADS]; + size_t thread_stack_count; + // Ptrace state bool ptrace_attached; } minidump_writer_t; @@ -1358,6 +1369,24 @@ write_thread_stack(minidump_writer_t *writer, uint64_t stack_pointer, return rva; } +/** + * Push a thread's stack descriptor onto writer->thread_stacks so the + * MemoryListStream writer can replay it. We only record stacks whose + * write actually produced bytes — a zero-size or zero-rva descriptor + * would point into the minidump header and break parsers. + */ +static void +record_thread_stack(minidump_writer_t *writer, const minidump_thread_t *thread) +{ + if (thread->stack.memory.size == 0 || thread->stack.memory.rva == 0) { + return; + } + if (writer->thread_stack_count >= SENTRY_CRASH_MAX_THREADS) { + return; + } + writer->thread_stacks[writer->thread_stack_count++] = thread->stack; +} + /** * Capture a thread's context and stack via ptrace. * Writes the thread context and stack memory into the minidump and updates @@ -1406,6 +1435,7 @@ ptrace_capture_thread( = write_thread_stack(writer, ptrace_sp, &stack_size, &stack_start); thread->stack.memory.size = stack_size; thread->stack.start_address = stack_start; + record_thread_stack(writer, thread); SENTRY_DEBUGF("Thread %u: wrote ptrace context at RVA " "0x%x, stack at RVA 0x%x (size %zu)", @@ -1495,6 +1525,7 @@ write_thread_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) = write_thread_stack(writer, sp, &stack_size, &stack_start); thread->stack.memory.size = stack_size; thread->stack.start_address = stack_start; + record_thread_stack(writer, thread); SENTRY_DEBUGF("Thread %u: wrote context at RVA 0x%x, stack at " "RVA 0x%x (size %zu)", @@ -2025,19 +2056,31 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) } } + // Reserve slots for thread stacks first; their bytes are already on disk + // (written by the thread-list writer) so we just emit descriptors + // pointing at the same RVA. This is what lets LLDB resolve memory at + // stack addresses and walk the FP chain. + size_t total_count = region_count + writer->thread_stack_count; + // Allocate memory list size_t list_size = sizeof(uint32_t) - + (region_count * sizeof(minidump_memory_descriptor_t)); + + (total_count * sizeof(minidump_memory_descriptor_t)); minidump_memory_list_t *memory_list = sentry_malloc(list_size); if (!memory_list) { return -1; } - memory_list->count = region_count; + memory_list->count = total_count; - // Write memory regions size_t mem_idx = 0; - for (size_t i = 0; i < writer->mapping_count && mem_idx < region_count; + // Replay thread stacks first so they appear in deterministic order. Each + // descriptor was already validated (size>0, rva!=0) by record_thread_stack. + for (size_t s = 0; s < writer->thread_stack_count; s++) { + memory_list->ranges[mem_idx++] = writer->thread_stacks[s]; + } + + // Write memory regions + for (size_t i = 0; i < writer->mapping_count && mem_idx < total_count; i++) { if (!should_include_region(&writer->mappings[i], writer->crash_ctx->minidump_mode, crash_addr)) { @@ -2104,6 +2147,99 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) return dir->rva ? 0 : -1; } +/** + * Slurp a file into a freshly allocated buffer. + * + * Used for /proc//{auxv,cmdline,environ,maps,status}, /proc/cpuinfo, + * and /etc/lsb-release. Those are kernel seqfiles or short text files that + * cannot be stat()'d for size (st_size is 0), so we read until EOF, doubling + * the buffer up to `max_size`. Caller owns the returned pointer. + * + * Returns the buffer on success and writes its length to *size_out. + * Returns NULL on open/read failure or when the file is empty. + */ +static void * +read_proc_file(const char *path, size_t max_size, size_t *size_out) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) { + return NULL; + } + + size_t buf_size = 4096; + if (buf_size > max_size) { + buf_size = max_size; + } + char *buf = sentry_malloc(buf_size); + if (!buf) { + close(fd); + return NULL; + } + + size_t total = 0; + ssize_t n; + for (;;) { + if (total >= buf_size) { + if (buf_size >= max_size) { + break; + } + size_t new_size = buf_size * 2; + if (new_size > max_size) { + new_size = max_size; + } + char *new_buf = sentry_malloc(new_size); + if (!new_buf) { + break; + } + memcpy(new_buf, buf, total); + sentry_free(buf); + buf = new_buf; + buf_size = new_size; + } + + n = read(fd, buf + total, buf_size - total); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + total += (size_t)n; + } + close(fd); + + if (total == 0) { + sentry_free(buf); + return NULL; + } + *size_out = total; + return buf; +} + +/** + * Helper: write a /proc file (or any other path) as a single stream. + * Returns 0 on success, -1 on any failure (including file unavailable). + */ +static int +write_file_as_stream(minidump_writer_t *writer, minidump_directory_t *dir, + uint32_t stream_type, const char *path, size_t max_size) +{ + size_t size = 0; + void *buf = read_proc_file(path, max_size, &size); + if (!buf) { + return -1; + } + + dir->stream_type = stream_type; + dir->rva = write_data(writer, buf, size); + dir->data_size = (uint32_t)size; + sentry_free(buf); + return dir->rva ? 0 : -1; +} + /** * Write /proc/PID/status as LinuxProcStatus stream. * lldb reads this to get the process ID (Pid: line). @@ -2115,24 +2251,80 @@ write_linux_proc_status_stream( char path[64]; snprintf( path, sizeof(path), "/proc/%d/status", writer->crash_ctx->crashed_pid); + return write_file_as_stream( + writer, dir, MINIDUMP_STREAM_LINUX_PROC_STATUS, path, 64 * 1024); +} - int fd = open(path, O_RDONLY); - if (fd < 0) { - return -1; - } +/** + * Write /proc/PID/auxv as LinuxAuxv stream. + * + * The auxiliary vector is a sequence of (Elf{32,64}_auxv_t) entries the + * kernel hands the dynamic loader at process start. LLDB's + * ProcessMinidump uses AT_PHDR/AT_PHNUM/AT_ENTRY from this stream to + * locate the main executable and r_debug; without it, LLDB on Linux + * falls back to a degraded unwind path that on AArch64 stops after the + * first frame. + */ +static int +write_linux_auxv_stream(minidump_writer_t *writer, minidump_directory_t *dir) +{ + char path[64]; + snprintf( + path, sizeof(path), "/proc/%d/auxv", writer->crash_ctx->crashed_pid); + return write_file_as_stream( + writer, dir, MINIDUMP_STREAM_LINUX_AUXV, path, 16 * 1024); +} - char buf[4096]; - ssize_t n = read(fd, buf, sizeof(buf) - 1); - close(fd); - if (n <= 0) { - return -1; - } - buf[n] = '\0'; +/** + * Write /proc/cpuinfo as LinuxCpuInfo stream. Diagnostic only. + */ +static int +write_linux_cpu_info_stream( + minidump_writer_t *writer, minidump_directory_t *dir) +{ + return write_file_as_stream(writer, dir, MINIDUMP_STREAM_LINUX_CPU_INFO, + "/proc/cpuinfo", 64 * 1024); +} - dir->stream_type = MINIDUMP_STREAM_LINUX_PROC_STATUS; - dir->rva = write_data(writer, buf, n); - dir->data_size = n; - return dir->rva ? 0 : -1; +/** + * Write /etc/lsb-release as LinuxLsbRelease stream. Diagnostic only; + * /etc/lsb-release is missing on many distros and that's fine. + */ +static int +write_linux_lsb_release_stream( + minidump_writer_t *writer, minidump_directory_t *dir) +{ + return write_file_as_stream(writer, dir, MINIDUMP_STREAM_LINUX_LSB_RELEASE, + "/etc/lsb-release", 4 * 1024); +} + +/** + * Write /proc/PID/cmdline as LinuxCmdLine stream. + * Argv is NUL-separated; consumers (rust-minidump, LLDB) treat it as a blob. + */ +static int +write_linux_cmd_line_stream( + minidump_writer_t *writer, minidump_directory_t *dir) +{ + char path[64]; + snprintf( + path, sizeof(path), "/proc/%d/cmdline", writer->crash_ctx->crashed_pid); + return write_file_as_stream( + writer, dir, MINIDUMP_STREAM_LINUX_CMD_LINE, path, 32 * 1024); +} + +/** + * Write /proc/PID/environ as LinuxEnviron stream. + */ +static int +write_linux_environ_stream( + minidump_writer_t *writer, minidump_directory_t *dir) +{ + char path[64]; + snprintf( + path, sizeof(path), "/proc/%d/environ", writer->crash_ctx->crashed_pid); + return write_file_as_stream( + writer, dir, MINIDUMP_STREAM_LINUX_ENVIRON, path, 64 * 1024); } /** @@ -2145,53 +2337,275 @@ write_linux_maps_stream(minidump_writer_t *writer, minidump_directory_t *dir) char path[64]; snprintf( path, sizeof(path), "/proc/%d/maps", writer->crash_ctx->crashed_pid); + return write_file_as_stream( + writer, dir, MINIDUMP_STREAM_LINUX_MAPS, path, 1024 * 1024); +} - int fd = open(path, O_RDONLY); - if (fd < 0) { +/** + * Walk the dynamic linker's r_debug structure and write it as a + * LinuxDsoDebug stream (MD_LINUX_DSO_DEBUG / 0x4767000A). + * + * Tools that consume Linux minidumps — notably LLDB's ProcessMinidump on + * AArch64 — use this stream to enumerate every loaded DSO in load order + * with its load address (l_addr) and PT_DYNAMIC pointer (l_ld). When this + * stream is missing, LLDB cannot locate eh_frame for the modules and + * unwinding stops after the first frame. + * + * Logic mirrors Breakpad's MinidumpWriter::WriteDSODebugStream: + * 1. Read /proc//auxv → AT_PHDR (program headers vaddr in target), + * AT_PHNUM (count). + * 2. Compute the executable load base (page-align AT_PHDR, subtract + * p_vaddr of the PT_LOAD with file offset 0). + * 3. Walk program headers to find PT_DYNAMIC's vaddr. + * 4. Walk PT_DYNAMIC entries to find DT_DEBUG → r_debug pointer. + * 5. Walk r_debug.r_map (link_map list) and emit one + * minidump_link_map64_t per loaded DSO. + * + * All target-process reads go through read_process_memory (process_vm_readv + * with ptrace fallback); we never dereference target pointers directly. + */ +static int +write_linux_dso_debug_stream( + minidump_writer_t *writer, minidump_directory_t *dir) +{ + // Step 1: read auxv to find AT_PHDR and AT_PHNUM. + char auxv_path[64]; + snprintf(auxv_path, sizeof(auxv_path), "/proc/%d/auxv", + writer->crash_ctx->crashed_pid); + size_t auxv_size = 0; + void *auxv_buf = read_proc_file(auxv_path, 16 * 1024, &auxv_size); + if (!auxv_buf) { + SENTRY_DEBUG("dso_debug: could not read auxv"); return -1; } - // Read in chunks, growing the buffer as needed since /proc/maps can - // exceed 32KB for processes with many memory mappings. - size_t buf_size = 32768; - char *buf = sentry_malloc(buf_size); - if (!buf) { - close(fd); + uint64_t at_phdr = 0; + uint64_t at_phnum = 0; + uint64_t at_entry = 0; + uint64_t at_base = 0; // dynamic linker load address + const ElfW(auxv_t) *auxv = (const ElfW(auxv_t) *)auxv_buf; + size_t auxv_count = auxv_size / sizeof(ElfW(auxv_t)); + for (size_t i = 0; i < auxv_count; i++) { + if (auxv[i].a_type == AT_NULL) { + break; + } + switch (auxv[i].a_type) { + case AT_PHDR: + at_phdr = auxv[i].a_un.a_val; + break; + case AT_PHNUM: + at_phnum = auxv[i].a_un.a_val; + break; + case AT_ENTRY: + at_entry = auxv[i].a_un.a_val; + break; + case AT_BASE: + at_base = auxv[i].a_un.a_val; + break; + } + } + sentry_free(auxv_buf); + + if (!at_phdr || !at_phnum) { + SENTRY_DEBUGF( + "dso_debug: missing AT_PHDR/AT_PHNUM (phdr=0x%llx, phnum=%llu)", + (unsigned long long)at_phdr, (unsigned long long)at_phnum); return -1; } - size_t total = 0; - ssize_t n; - while ((n = read(fd, buf + total, buf_size - total - 1)) > 0) { - total += n; - if (total >= buf_size - 1) { - // Double the buffer, capped at 1MB - size_t new_size = buf_size * 2; - if (new_size > 1024 * 1024) { + // Step 2 + 3: read program headers, locate PT_DYNAMIC and adjust base. + // Cap phnum to a sane upper bound so a corrupt auxv can't drive a huge alloc. + if (at_phnum > 256) { + SENTRY_WARNF("dso_debug: phnum=%llu exceeds cap, refusing", + (unsigned long long)at_phnum); + return -1; + } + size_t phdr_size = (size_t)at_phnum * sizeof(ElfW(Phdr)); + ElfW(Phdr) *phdrs = sentry_malloc(phdr_size); + if (!phdrs) { + return -1; + } + if (read_process_memory(writer, at_phdr, phdrs, phdr_size) + != (ssize_t)phdr_size) { + SENTRY_DEBUG("dso_debug: failed to read program headers"); + sentry_free(phdrs); + return -1; + } + + // Page-align AT_PHDR for the initial base estimate; the PT_LOAD with + // p_offset==0 then tells us the real load offset. + uint64_t base = at_phdr & ~((uint64_t)0xfff); + uint64_t dyn_vaddr = 0; + size_t dyn_filesz = 0; + for (uint64_t i = 0; i < at_phnum; i++) { + const ElfW(Phdr) *ph = &phdrs[i]; + if (ph->p_type == PT_LOAD && ph->p_offset == 0) { + base -= ph->p_vaddr; + } + if (ph->p_type == PT_DYNAMIC) { + dyn_vaddr = ph->p_vaddr; + dyn_filesz = ph->p_filesz; + } + } + sentry_free(phdrs); + + if (!dyn_vaddr) { + SENTRY_DEBUG("dso_debug: no PT_DYNAMIC found"); + return -1; + } + + uint64_t dynamic_addr = dyn_vaddr + base; + + // Step 4: walk PT_DYNAMIC for DT_DEBUG. + // dyn_filesz can be 0 in rare ELFs; cap iteration with a sane upper bound. + size_t max_dyn_entries + = dyn_filesz ? dyn_filesz / sizeof(ElfW(Dyn)) : 4096; + if (max_dyn_entries > 4096) { + max_dyn_entries = 4096; + } + + uint64_t r_debug_addr = 0; + size_t dynamic_length = 0; + ElfW(Dyn) dyn; + for (size_t i = 0; i < max_dyn_entries; i++) { + uint64_t entry_addr = dynamic_addr + i * sizeof(dyn); + if (read_process_memory(writer, entry_addr, &dyn, sizeof(dyn)) + != (ssize_t)sizeof(dyn)) { + SENTRY_DEBUGF( + "dso_debug: failed to read PT_DYNAMIC entry %zu", i); + return -1; + } + dynamic_length += sizeof(dyn); + if (dyn.d_tag == DT_DEBUG) { + r_debug_addr = (uint64_t)dyn.d_un.d_ptr; + } else if (dyn.d_tag == DT_NULL) { + break; + } + } + + // Step 5: walk r_debug.r_map (struct link_map list). + // We declare local copies of struct r_debug / struct link_map fields + // explicitly so we don't depend on their exact glibc layout — the + // ABI for these is stable across glibc/musl: r_version (int) at offset 0, + // r_map (link_map*) at offset of pointer-aligned 1, then r_brk, r_state, + // r_ldbase. struct link_map starts with l_addr, l_name, l_ld, l_next, l_prev. + // Using 's definition is fine since target and dumper share libc. + struct r_debug rd; + memset(&rd, 0, sizeof(rd)); + bool have_rdebug = false; + if (r_debug_addr) { + if (read_process_memory(writer, r_debug_addr, &rd, sizeof(rd)) + == (ssize_t)sizeof(rd)) { + have_rdebug = true; + } else { + SENTRY_DEBUG("dso_debug: failed to read r_debug"); + } + } + + // Count and walk DSOs. Cap at SENTRY_CRASH_MAX_MAPPINGS to bound work. + size_t dso_count = 0; + void *next = have_rdebug ? rd.r_map : NULL; + while (next && dso_count < SENTRY_CRASH_MAX_MAPPINGS) { + struct link_map lm; + if (read_process_memory(writer, (uint64_t)(uintptr_t)next, &lm, + sizeof(lm)) + != (ssize_t)sizeof(lm)) { + SENTRY_DEBUGF("dso_debug: failed to read link_map at %p", next); + break; + } + dso_count++; + next = lm.l_next; + } + + // Pre-emit the link_map array header so we have a stable RVA, then + // stream entries. We'll backfill the array. + size_t links_size = dso_count * sizeof(minidump_link_map64_t); + minidump_link_map64_t *links = NULL; + minidump_rva_t links_rva = 0; + if (dso_count > 0) { + links = sentry_malloc(links_size); + if (!links) { + return -1; + } + memset(links, 0, links_size); + + // Walk again, this time emitting strings (which share the same RVA + // space as the link_map array — strings get written first, then the + // array of fixed-size entries last so its RVA is contiguous). + size_t idx = 0; + next = rd.r_map; + while (next && idx < dso_count) { + struct link_map lm; + if (read_process_memory(writer, (uint64_t)(uintptr_t)next, &lm, + sizeof(lm)) + != (ssize_t)sizeof(lm)) { break; } - char *new_buf = sentry_malloc(new_size); - if (!new_buf) { - break; + + char namebuf[257] = { 0 }; + if (lm.l_name) { + ssize_t got = read_process_memory(writer, + (uint64_t)(uintptr_t)lm.l_name, namebuf, + sizeof(namebuf) - 1); + if (got <= 0) { + namebuf[0] = '\0'; + } else { + namebuf[got] = '\0'; // ensure NUL-terminated + } } - memcpy(new_buf, buf, total); - sentry_free(buf); - buf = new_buf; - buf_size = new_size; + + links[idx].addr = (uint64_t)lm.l_addr; + links[idx].name = write_minidump_string(writer, namebuf); + links[idx].ld = (uint64_t)(uintptr_t)lm.l_ld; + idx++; + next = lm.l_next; + } + + links_rva = write_data(writer, links, links_size); + sentry_free(links); + if (!links_rva) { + return -1; } } - close(fd); - if (total == 0) { - sentry_free(buf); + // Now write the MD_LINUX_DSO_DEBUG header followed by a copy of the + // PT_DYNAMIC blob (matches Breakpad's layout: header is at the stream + // RVA, and the dynamic-section bytes follow contiguously). + size_t header_size = sizeof(minidump_debug64_t); + size_t total_size = header_size + dynamic_length; + uint8_t *blob = sentry_malloc(total_size); + if (!blob) { return -1; } + memset(blob, 0, total_size); + minidump_debug64_t *hdr = (minidump_debug64_t *)blob; + hdr->version = have_rdebug ? rd.r_version : 0; + hdr->map = links_rva; + hdr->dso_count = (uint32_t)dso_count; + hdr->brk = have_rdebug ? (uint64_t)rd.r_brk : 0; + hdr->ldbase = at_base; // matches Breakpad: AT_BASE from auxv + hdr->dynamic = dynamic_addr; + + // Best-effort: copy the actual PT_DYNAMIC bytes. Failure here is OK — + // consumers only need the header for module enumeration. + if (dynamic_length) { + if (read_process_memory(writer, dynamic_addr, blob + header_size, + dynamic_length) + != (ssize_t)dynamic_length) { + // Zero out the trailing bytes if we couldn't read; not fatal. + memset(blob + header_size, 0, dynamic_length); + } + } - dir->stream_type = MINIDUMP_STREAM_LINUX_MAPS; - dir->rva = write_data(writer, buf, total); - dir->data_size = total; + dir->stream_type = MINIDUMP_STREAM_LINUX_DSO_DEBUG; + dir->rva = write_data(writer, blob, total_size); + dir->data_size = (uint32_t)total_size; + sentry_free(blob); - sentry_free(buf); + SENTRY_DEBUGF("dso_debug: wrote %zu DSOs (dynamic@0x%llx, r_debug@0x%llx)", + dso_count, (unsigned long long)dynamic_addr, + (unsigned long long)r_debug_addr); + (void)at_entry; // currently unused; logged via auxv for debugging return dir->rva ? 0 : -1; } @@ -2234,12 +2648,30 @@ sentry__write_minidump( // Continue anyway - we can still write minidump without ptrace } - // Reserve space for header and directory - // Write 8 streams: system_info, threads, modules, exception, - // memory_list, linux_proc_status, linux_maps, thread_names. - // The Linux streams provide PID and memory map info for debuggers. - // ThreadNamesStream matches Crashpad format for server-side processing. - const uint32_t stream_count = 8; + // Reserve space for header and directory. + // + // Streams written (matches the Breakpad linux minidump_writer set so + // LLDB's ProcessMinidump can identify modules and unwind): + // 0 SystemInfo + // 1 ThreadList + // 2 ModuleList + // 3 Exception + // 4 MemoryList + // 5 LinuxProcStatus /proc//status + // 6 LinuxMaps /proc//maps + // 7 ThreadNames + // 8 LinuxAuxv /proc//auxv (LLDB needs this) + // 9 LinuxCpuInfo /proc/cpuinfo + // 10 LinuxLsbRelease /etc/lsb-release + // 11 LinuxCmdLine /proc//cmdline + // 12 LinuxEnviron /proc//environ + // 13 LinuxDsoDebug walked from r_debug (LLDB needs this) + // + // The optional /proc files (lsb-release missing on many distros, environ + // unreadable in some sandboxed configs) are wired with NullifyDirectoryEntry + // semantics: the slot is reserved, but if the writer fails the directory + // entry stays at type/size/rva = 0 (consumers ignore it). + const uint32_t stream_count = 14; writer.current_offset = sizeof(minidump_header_t) + (stream_count * sizeof(minidump_directory_t)); @@ -2257,7 +2689,8 @@ sentry__write_minidump( } // Write streams - minidump_directory_t directories[8]; + minidump_directory_t directories[14]; + memset(directories, 0, sizeof(directories)); int result = 0; SENTRY_DEBUG("writing system info stream"); @@ -2273,21 +2706,18 @@ sentry__write_minidump( result |= write_memory_list_stream(&writer, &directories[4]); // Write Linux-specific streams for debugger compatibility. - // These are non-fatal: if they fail, the minidump is still valid. + // These are non-fatal: if they fail, the minidump is still valid; the + // directory slot ends up zeroed (stream_type=0 = unused). SENTRY_DEBUG("writing linux proc status stream"); if (write_linux_proc_status_stream(&writer, &directories[5]) < 0) { SENTRY_WARN("failed to write linux proc status stream"); directories[5].stream_type = MINIDUMP_STREAM_LINUX_PROC_STATUS; - directories[5].data_size = 0; - directories[5].rva = 0; } SENTRY_DEBUG("writing linux maps stream"); if (write_linux_maps_stream(&writer, &directories[6]) < 0) { SENTRY_WARN("failed to write linux maps stream"); directories[6].stream_type = MINIDUMP_STREAM_LINUX_MAPS; - directories[6].data_size = 0; - directories[6].rva = 0; } // Write thread names stream (matches Crashpad format) @@ -2295,8 +2725,43 @@ sentry__write_minidump( if (write_thread_names_stream(&writer, &directories[7]) < 0) { SENTRY_WARN("failed to write thread names stream"); directories[7].stream_type = MINIDUMP_STREAM_THREAD_NAMES; - directories[7].data_size = 0; - directories[7].rva = 0; + } + + SENTRY_DEBUG("writing linux auxv stream"); + if (write_linux_auxv_stream(&writer, &directories[8]) < 0) { + SENTRY_WARN("failed to write linux auxv stream"); + directories[8].stream_type = MINIDUMP_STREAM_LINUX_AUXV; + } + + SENTRY_DEBUG("writing linux cpu info stream"); + if (write_linux_cpu_info_stream(&writer, &directories[9]) < 0) { + SENTRY_DEBUG("linux cpu info stream unavailable"); + directories[9].stream_type = MINIDUMP_STREAM_LINUX_CPU_INFO; + } + + SENTRY_DEBUG("writing linux lsb release stream"); + if (write_linux_lsb_release_stream(&writer, &directories[10]) < 0) { + // Many distros don't ship /etc/lsb-release; this is expected. + SENTRY_DEBUG("linux lsb release stream unavailable"); + directories[10].stream_type = MINIDUMP_STREAM_LINUX_LSB_RELEASE; + } + + SENTRY_DEBUG("writing linux cmd line stream"); + if (write_linux_cmd_line_stream(&writer, &directories[11]) < 0) { + SENTRY_DEBUG("linux cmd line stream unavailable"); + directories[11].stream_type = MINIDUMP_STREAM_LINUX_CMD_LINE; + } + + SENTRY_DEBUG("writing linux environ stream"); + if (write_linux_environ_stream(&writer, &directories[12]) < 0) { + SENTRY_DEBUG("linux environ stream unavailable"); + directories[12].stream_type = MINIDUMP_STREAM_LINUX_ENVIRON; + } + + SENTRY_DEBUG("writing linux dso debug stream"); + if (write_linux_dso_debug_stream(&writer, &directories[13]) < 0) { + SENTRY_WARN("failed to write linux dso debug stream"); + directories[13].stream_type = MINIDUMP_STREAM_LINUX_DSO_DEBUG; } if (result < 0) { From 32a4e2490dc5d3487af2bc2c26f5133c0e93f234 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Mon, 4 May 2026 12:05:56 +0200 Subject: [PATCH 3/7] Fix CHANGELOG.md --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcecd7a16..83b7bd640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ **Fixes**: -- Native/Linux: correct `MD_LINUX_MAPS` stream type (was tagged as `MD_LINUX_AUXV`). -- Native/Linux: drop non-ELF mappings (e.g. `/dev/shm/*`, `(deleted)` files) from the minidump module list. -- Native/Linux: merge non-contiguous mappings of the same shared library into a single module, and use the offset==0 mapping as `base_of_image`. Fixes duplicate `ld-linux` entries that confused some debuggers (notably Windows LLDB) reading Linux ARM64 minidumps. -- Native/Linux: log when `uname()` is blocked (sandbox/seccomp) and fall back to `/proc/sys/kernel/osrelease` for the OS version. +- Native/Linux: correct `MD_LINUX_MAPS` stream type (was tagged as `MD_LINUX_AUXV`). ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) +- Native/Linux: drop non-ELF mappings (e.g. `/dev/shm/*`, `(deleted)` files) from the minidump module list. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) +- Native/Linux: merge non-contiguous mappings of the same shared library into a single module, and use the offset==0 mapping as `base_of_image`. Fixes duplicate `ld-linux` entries that confused some debuggers (notably Windows LLDB) reading Linux ARM64 minidumps. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) +- Native/Linux: log when `uname()` is blocked (sandbox/seccomp) and fall back to `/proc/sys/kernel/osrelease` for the OS version. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) +- Native/Linux: emit `LinuxAuxv`, `LinuxCpuInfo`, `LinuxLsbRelease`, `LinuxCmdLine`, `LinuxEnviron`, and `LinuxDsoDebug` streams alongside the existing set, matching what Breakpad writes. LLDB needs `LinuxAuxv` and `LinuxDsoDebug` to identify the dynamic loader and enumerate loaded shared libraries; without them, opening a minidump in LLDB on Linux would only recover one frame per thread. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) +- Native/Linux: replay each thread's stack memory descriptor into `MemoryListStream`. Previously stack bytes were only referenced from the per-thread record, so debuggers that look up memory by virtual address (LLDB) could not read the stack and unwinding stopped at frame 0 even when `eh_frame` was available. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) ## 0.13.9 From 5fd69fbe59978d7b6562b66e30609bac5c6f565e Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Mon, 4 May 2026 12:15:09 +0200 Subject: [PATCH 4/7] Fix format --- .../native/minidump/sentry_minidump_linux.c | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index b798a8e45..c0632d9a9 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -2317,8 +2317,7 @@ write_linux_cmd_line_stream( * Write /proc/PID/environ as LinuxEnviron stream. */ static int -write_linux_environ_stream( - minidump_writer_t *writer, minidump_directory_t *dir) +write_linux_environ_stream(minidump_writer_t *writer, minidump_directory_t *dir) { char path[64]; snprintf( @@ -2414,7 +2413,8 @@ write_linux_dso_debug_stream( } // Step 2 + 3: read program headers, locate PT_DYNAMIC and adjust base. - // Cap phnum to a sane upper bound so a corrupt auxv can't drive a huge alloc. + // Cap phnum to a sane upper bound so a corrupt auxv can't drive a huge + // alloc. if (at_phnum > 256) { SENTRY_WARNF("dso_debug: phnum=%llu exceeds cap, refusing", (unsigned long long)at_phnum); @@ -2458,8 +2458,7 @@ write_linux_dso_debug_stream( // Step 4: walk PT_DYNAMIC for DT_DEBUG. // dyn_filesz can be 0 in rare ELFs; cap iteration with a sane upper bound. - size_t max_dyn_entries - = dyn_filesz ? dyn_filesz / sizeof(ElfW(Dyn)) : 4096; + size_t max_dyn_entries = dyn_filesz ? dyn_filesz / sizeof(ElfW(Dyn)) : 4096; if (max_dyn_entries > 4096) { max_dyn_entries = 4096; } @@ -2471,8 +2470,7 @@ write_linux_dso_debug_stream( uint64_t entry_addr = dynamic_addr + i * sizeof(dyn); if (read_process_memory(writer, entry_addr, &dyn, sizeof(dyn)) != (ssize_t)sizeof(dyn)) { - SENTRY_DEBUGF( - "dso_debug: failed to read PT_DYNAMIC entry %zu", i); + SENTRY_DEBUGF("dso_debug: failed to read PT_DYNAMIC entry %zu", i); return -1; } dynamic_length += sizeof(dyn); @@ -2488,8 +2486,9 @@ write_linux_dso_debug_stream( // explicitly so we don't depend on their exact glibc layout — the // ABI for these is stable across glibc/musl: r_version (int) at offset 0, // r_map (link_map*) at offset of pointer-aligned 1, then r_brk, r_state, - // r_ldbase. struct link_map starts with l_addr, l_name, l_ld, l_next, l_prev. - // Using 's definition is fine since target and dumper share libc. + // r_ldbase. struct link_map starts with l_addr, l_name, l_ld, l_next, + // l_prev. Using 's definition is fine since target and dumper share + // libc. struct r_debug rd; memset(&rd, 0, sizeof(rd)); bool have_rdebug = false; @@ -2507,8 +2506,8 @@ write_linux_dso_debug_stream( void *next = have_rdebug ? rd.r_map : NULL; while (next && dso_count < SENTRY_CRASH_MAX_MAPPINGS) { struct link_map lm; - if (read_process_memory(writer, (uint64_t)(uintptr_t)next, &lm, - sizeof(lm)) + if (read_process_memory( + writer, (uint64_t)(uintptr_t)next, &lm, sizeof(lm)) != (ssize_t)sizeof(lm)) { SENTRY_DEBUGF("dso_debug: failed to read link_map at %p", next); break; @@ -2536,8 +2535,8 @@ write_linux_dso_debug_stream( next = rd.r_map; while (next && idx < dso_count) { struct link_map lm; - if (read_process_memory(writer, (uint64_t)(uintptr_t)next, &lm, - sizeof(lm)) + if (read_process_memory( + writer, (uint64_t)(uintptr_t)next, &lm, sizeof(lm)) != (ssize_t)sizeof(lm)) { break; } @@ -2589,8 +2588,8 @@ write_linux_dso_debug_stream( // Best-effort: copy the actual PT_DYNAMIC bytes. Failure here is OK — // consumers only need the header for module enumeration. if (dynamic_length) { - if (read_process_memory(writer, dynamic_addr, blob + header_size, - dynamic_length) + if (read_process_memory( + writer, dynamic_addr, blob + header_size, dynamic_length) != (ssize_t)dynamic_length) { // Zero out the trailing bytes if we couldn't read; not fatal. memset(blob + header_size, 0, dynamic_length); @@ -2668,9 +2667,10 @@ sentry__write_minidump( // 13 LinuxDsoDebug walked from r_debug (LLDB needs this) // // The optional /proc files (lsb-release missing on many distros, environ - // unreadable in some sandboxed configs) are wired with NullifyDirectoryEntry - // semantics: the slot is reserved, but if the writer fails the directory - // entry stays at type/size/rva = 0 (consumers ignore it). + // unreadable in some sandboxed configs) are wired with + // NullifyDirectoryEntry semantics: the slot is reserved, but if the writer + // fails the directory entry stays at type/size/rva = 0 (consumers ignore + // it). const uint32_t stream_count = 14; writer.current_offset = sizeof(minidump_header_t) + (stream_count * sizeof(minidump_directory_t)); From f2a845ed7d205e5ca31d94903693692f8d6dda31 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Mon, 4 May 2026 12:32:03 +0200 Subject: [PATCH 5/7] Fix build errors --- src/backends/native/minidump/sentry_minidump_linux.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index c0632d9a9..1da9d00db 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -857,8 +857,8 @@ write_thread_context( /** * Quickly verify a file is an ELF binary by reading its magic bytes. - * Used to filter out non-ELF mappings (e.g. /dev/shm/*, deleted files, - * sentry-native's own IPC shared memory) from the module list — LLDB and + * Used to filter out non-ELF mappings (e.g. files under /dev/shm, deleted + * files, sentry-native's own IPC shared memory) from the module list — LLDB and * other consumers can choke on entries that look like modules but aren't. */ static bool @@ -1590,7 +1590,7 @@ typedef struct { * avoids two pitfalls that confuse downstream consumers (notably LLDB's * ModuleList on Windows reading Linux dumps): * - * 1. Non-ELF entries (e.g. /dev/shm/* IPC files, "(deleted)" files) being + * 1. Non-ELF entries (e.g. /dev/shm IPC files, "(deleted)" files) being * emitted as modules with bogus zero-id CV records. * 2. The same shared library appearing twice when its segments are * separated by an unrelated mapping (e.g. ld-linux-aarch64.so.1 split From c5d4fda533d65e249e86143ca39560acd7519778 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 5 May 2026 12:37:53 +0200 Subject: [PATCH 6/7] Fix comments + indirect memory --- CHANGELOG.md | 5 + src/CMakeLists.txt | 2 + .../minidump/sentry_minidump_indirect.c | 192 ++++++++++ .../minidump/sentry_minidump_indirect.h | 109 ++++++ .../native/minidump/sentry_minidump_linux.c | 332 ++++++++++++++++-- .../native/minidump/sentry_minidump_macos.c | 245 ++++++++++++- 6 files changed, 847 insertions(+), 38 deletions(-) create mode 100644 src/backends/native/minidump/sentry_minidump_indirect.c create mode 100644 src/backends/native/minidump/sentry_minidump_indirect.h diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b7bd640..b1e418807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ - Native/Linux: log when `uname()` is blocked (sandbox/seccomp) and fall back to `/proc/sys/kernel/osrelease` for the OS version. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) - Native/Linux: emit `LinuxAuxv`, `LinuxCpuInfo`, `LinuxLsbRelease`, `LinuxCmdLine`, `LinuxEnviron`, and `LinuxDsoDebug` streams alongside the existing set, matching what Breakpad writes. LLDB needs `LinuxAuxv` and `LinuxDsoDebug` to identify the dynamic loader and enumerate loaded shared libraries; without them, opening a minidump in LLDB on Linux would only recover one frame per thread. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) - Native/Linux: replay each thread's stack memory descriptor into `MemoryListStream`. Previously stack bytes were only referenced from the per-thread record, so debuggers that look up memory by virtual address (LLDB) could not read the stack and unwinding stopped at frame 0 even when `eh_frame` was available. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) +- Native/macOS: replay each thread's stack memory descriptor into `MemoryListStream` so LLDB can read stack contents (same fix as Linux above). ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) + +**Features**: + +- Native (Linux, macOS): SMART minidump mode now also captures memory referenced by the registers and stack contents of every captured thread, matching the semantics of `MiniDumpWithIndirectlyReferencedMemory` on Windows (already in effect for the native Windows backend). For each pointer that resolves into a writable heap region, ~1 KiB is captured around it; total budget capped at 4 MiB per dump. Heap-allocated structs reachable from the crashing call stack can now be inspected in LLDB / VS Code. ([#1694](https://github.com/getsentry/sentry-native/pull/1694)) ## 0.13.9 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d3cff005d..1c5e05f18 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -173,11 +173,13 @@ elseif(SENTRY_BACKEND_NATIVE) if(LINUX OR ANDROID) sentry_target_sources_cwd(sentry backends/native/minidump/sentry_minidump_common.c + backends/native/minidump/sentry_minidump_indirect.c backends/native/minidump/sentry_minidump_linux.c ) elseif(APPLE) sentry_target_sources_cwd(sentry backends/native/minidump/sentry_minidump_common.c + backends/native/minidump/sentry_minidump_indirect.c backends/native/minidump/sentry_minidump_macos.c ) elseif(WIN32) diff --git a/src/backends/native/minidump/sentry_minidump_indirect.c b/src/backends/native/minidump/sentry_minidump_indirect.c new file mode 100644 index 000000000..479133a90 --- /dev/null +++ b/src/backends/native/minidump/sentry_minidump_indirect.c @@ -0,0 +1,192 @@ +#include "sentry_boot.h" + +#include "sentry_minidump_indirect.h" + +#include "sentry_alloc.h" +#include "sentry_logger.h" +#include "sentry_minidump_common.h" + +#include +#include + +void +sentry__indirect_init(sentry_indirect_accumulator_t *acc) +{ + acc->region_count = 0; + acc->total_bytes = 0; +} + +/** + * Binary-search the sorted region list for the first entry whose `end` is + * strictly greater than `target`. The candidate at index `lo` (if any) + * is the only one that can possibly overlap [target, target+len). + * + * Returns region_count when no such entry exists (target is past the last). + */ +static size_t +find_first_after(const sentry_indirect_accumulator_t *acc, uint64_t target) +{ + size_t lo = 0; + size_t hi = acc->region_count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + if (acc->regions[mid].end <= target) { + lo = mid + 1; + } else { + hi = mid; + } + } + return lo; +} + +/** + * Returns true if [start, end) overlaps any region already in the + * accumulator. The list is sorted, so we only need to check the first + * region whose end > start — anything earlier ended before us, anything + * later starts after we'd want it to. + */ +static bool +overlaps_existing( + const sentry_indirect_accumulator_t *acc, uint64_t start, uint64_t end) +{ + size_t i = find_first_after(acc, start); + if (i >= acc->region_count) { + return false; + } + return acc->regions[i].start < end; +} + +/** + * Insert a new region at the sorted position. Caller must have verified + * via overlaps_existing() that no overlap exists, and via the cap checks + * that there's room. + */ +static void +insert_sorted(sentry_indirect_accumulator_t *acc, + const sentry_indirect_region_t *new_region) +{ + size_t i = find_first_after(acc, new_region->start); + if (i < acc->region_count) { + memmove(&acc->regions[i + 1], &acc->regions[i], + (acc->region_count - i) * sizeof(*acc->regions)); + } + acc->regions[i] = *new_region; + acc->region_count++; + acc->total_bytes += new_region->size; +} + +void +sentry__indirect_consider(sentry_indirect_accumulator_t *acc, + minidump_writer_base_t *writer, uint64_t addr, + const sentry_indirect_ops_t *ops) +{ + // Cap checks first — they're the cheapest filters and short-circuit the + // mapping lookup once we've spent the budget. + if (acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) { + return; + } + if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES) { + return; + } + + // Cheap rejects before paying for is_writable_heap. + // - 0/low values: NULLs and small ints + // - very high bits set: kernel addresses (canonical AArch64/x86_64 user + // space tops out below this) + if (addr < SENTRY_INDIRECT_PAGE_SIZE) { + return; + } + if ((addr >> 56) != 0) { + return; + } + + if (!ops->is_writable_heap(ops->ctx, addr)) { + return; + } + + // Page-align down so the captured region covers the page containing the + // pointee (and we can dedup multiple pointers landing in the same page). + // Capture is roughly centered on the pointer but always starts on a page + // boundary so adjacent allocations get covered too. + uint64_t centered = addr - (SENTRY_INDIRECT_PER_POINTER_BYTES / 2); + uint64_t start = centered & ~((uint64_t)SENTRY_INDIRECT_PAGE_SIZE - 1); + uint64_t end = start + SENTRY_INDIRECT_PER_POINTER_BYTES; + // Round end up to page boundary too — small bump but keeps reads aligned. + end = (end + SENTRY_INDIRECT_PAGE_SIZE - 1) + & ~((uint64_t)SENTRY_INDIRECT_PAGE_SIZE - 1); + + // Trim against the global byte budget so a candidate near the cap doesn't + // blow it. + size_t want = (size_t)(end - start); + size_t remaining = SENTRY_INDIRECT_MAX_TOTAL_BYTES - acc->total_bytes; + if (want > remaining) { + end = start + remaining; + if (end <= start) { + return; + } + want = (size_t)(end - start); + } + + if (overlaps_existing(acc, start, end)) { + return; + } + + // Read from the target. Soft-fail on read errors — a pointer into a + // mapped-but-paged-out region or a recently-unmapped one is common during + // crash handling and shouldn't abort the whole walk. + void *buf = sentry_malloc(want); + if (!buf) { + return; + } + ssize_t got = ops->read_memory(ops->ctx, start, buf, want); + if (got <= 0) { + sentry_free(buf); + return; + } + + minidump_rva_t rva = sentry__minidump_write_data(writer, buf, (size_t)got); + sentry_free(buf); + if (!rva) { + return; + } + + sentry_indirect_region_t r; + r.start = start; + r.end = start + (uint64_t)got; + r.rva = rva; + r.size = (uint32_t)got; + insert_sorted(acc, &r); +} + +void +sentry__indirect_walk_words(sentry_indirect_accumulator_t *acc, + minidump_writer_base_t *writer, const void *buf, size_t len_bytes, + const sentry_indirect_ops_t *ops) +{ + const size_t word_size = sizeof(void *); + const size_t word_count = len_bytes / word_size; + if (word_size == 8) { + const uint64_t *words = (const uint64_t *)buf; + for (size_t i = 0; i < word_count; i++) { + sentry__indirect_consider(acc, writer, words[i], ops); + // Bail early once the byte cap is fully spent — no point grinding + // through the rest of the stack. The region cap also acts as a + // floor here. + if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES + || acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) { + return; + } + } + } else { + // 32-bit hosts (Linux i386 / arm). Pointers are 32-bit but our + // accumulator stores them as 64-bit just fine. + const uint32_t *words = (const uint32_t *)buf; + for (size_t i = 0; i < word_count; i++) { + sentry__indirect_consider(acc, writer, (uint64_t)words[i], ops); + if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES + || acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) { + return; + } + } + } +} diff --git a/src/backends/native/minidump/sentry_minidump_indirect.h b/src/backends/native/minidump/sentry_minidump_indirect.h new file mode 100644 index 000000000..24366e815 --- /dev/null +++ b/src/backends/native/minidump/sentry_minidump_indirect.h @@ -0,0 +1,109 @@ +#ifndef SENTRY_MINIDUMP_INDIRECT_H_INCLUDED +#define SENTRY_MINIDUMP_INDIRECT_H_INCLUDED + +#include "sentry_minidump_common.h" +#include "sentry_minidump_format.h" +#include +#include +#include +#include + +/** + * Indirectly-referenced memory capture for SMART minidump mode. + * + * Mirrors the Windows MiniDumpWithIndirectlyReferencedMemory contract: + * for every captured thread, scan its registers and stack words, and for + * each value that resolves into a writable heap region capture a small + * page-aligned chunk of memory around it. Lets debuggers (LLDB, VS Code) + * deref pointers held in struct locals or registers at crash time. + * + * This file is a small algorithm + a 2-function vtable. Each platform + * (linux, macos) provides the vtable and feeds in the per-thread + * registers and stack bytes; the dedup, paging, and dump-emission logic + * lives here once. + */ + +// Per-pointer capture size — matches the Windows API default of 1 KiB. +#define SENTRY_INDIRECT_PER_POINTER_BYTES 1024 +// Hard caps so dump size doesn't blow up under pathological pointer density. +#define SENTRY_INDIRECT_MAX_REGIONS 1024 +#define SENTRY_INDIRECT_MAX_TOTAL_BYTES (4 * 1024 * 1024) +// Page size used for region alignment. 4 KiB on every arch we target. +#define SENTRY_INDIRECT_PAGE_SIZE 4096 + +typedef struct { + uint64_t start; // page-aligned target VA + uint64_t end; // exclusive + minidump_rva_t rva; + uint32_t size; // bytes actually present in the dump +} sentry_indirect_region_t; + +/** + * Platform shim. Each backend supplies its own pointer-validation and + * remote-memory-read implementations; the algorithm calls these via this + * tiny vtable instead of duplicating the loop in every platform file. + */ +typedef struct sentry_indirect_ops_s { + /** + * Returns true iff `addr` falls inside a readable, writable, non-executable + * mapping that is NOT a thread stack, vDSO, or other kernel-private region + * — i.e. the kind of mapping a heap pointer would target. + * + * Called O(words-on-stack) times per dump, so should be O(log n) over the + * cached mappings table (binary search) rather than a linear scan. + */ + bool (*is_writable_heap)(void *ctx, uint64_t addr); + + /** + * Read up to `len` bytes from the *target* (crashed) process at virtual + * address `addr` into `buf`. Returns bytes read on success, or -1 on + * failure. + */ + ssize_t (*read_memory)(void *ctx, uint64_t addr, void *buf, size_t len); + + /** Opaque pointer passed to the callbacks above. */ + void *ctx; +} sentry_indirect_ops_t; + +/** + * Bounded accumulator. Holds the regions captured so far plus the running + * byte total used to enforce SENTRY_INDIRECT_MAX_TOTAL_BYTES. + * + * Regions are kept sorted by `start` so dedup is O(log n) per candidate. + */ +typedef struct { + sentry_indirect_region_t regions[SENTRY_INDIRECT_MAX_REGIONS]; + size_t region_count; + size_t total_bytes; +} sentry_indirect_accumulator_t; + +void sentry__indirect_init(sentry_indirect_accumulator_t *acc); + +/** + * Consider one address as a candidate heap pointer. If `addr` resolves into + * a writable-heap mapping (per ops->is_writable_heap), is not already covered + * by a previously-captured region, and the global byte budget hasn't been + * exhausted, this captures SENTRY_INDIRECT_PER_POINTER_BYTES around `addr`, + * page-aligned, and writes them to the dump file via `writer`. + * + * Safe to call with junk values — non-pointer addresses are filtered cheaply + * via the writable-heap check. + */ +void sentry__indirect_consider(sentry_indirect_accumulator_t *acc, + minidump_writer_base_t *writer, uint64_t addr, + const sentry_indirect_ops_t *ops); + +/** + * Walk a buffer of pointer-sized words and call sentry__indirect_consider on + * each. `buf` is treated as an array of native-endian uint64_t (or uint32_t + * on 32-bit; the function handles both based on sizeof(void*)). `len_bytes` + * may be unaligned — the trailing bytes shorter than a word are ignored. + * + * This is the hot loop for stack scanning: a 1 MiB stack on AArch64 yields + * 128 K candidates, each costing one O(log n) mapping lookup. + */ +void sentry__indirect_walk_words(sentry_indirect_accumulator_t *acc, + minidump_writer_base_t *writer, const void *buf, size_t len_bytes, + const sentry_indirect_ops_t *ops); + +#endif // SENTRY_MINIDUMP_INDIRECT_H_INCLUDED diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 1da9d00db..6fc2893ad 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -24,6 +24,7 @@ # include "sentry_logger.h" # include "sentry_minidump_common.h" # include "sentry_minidump_format.h" +# include "sentry_minidump_indirect.h" # include "sentry_minidump_writer.h" // NT_PRSTATUS is defined in linux/elf.h but we can't include that @@ -111,7 +112,12 @@ typedef struct { // LinuxMaps) but cannot read its contents and unwinding stops after // frame 0. Breakpad's MinidumpWriter does the equivalent via // memory_blocks_.push_back(thread->stack). + // + // thread_stack_tids[i] is the thread id whose stack is in + // thread_stacks[i] — needed by the SMART-mode indirect-memory walker so + // it can find the matching ucontext for register scanning. minidump_memory_descriptor_t thread_stacks[SENTRY_CRASH_MAX_THREADS]; + pid_t thread_stack_tids[SENTRY_CRASH_MAX_THREADS]; size_t thread_stack_count; // Ptrace state @@ -1384,7 +1390,10 @@ record_thread_stack(minidump_writer_t *writer, const minidump_thread_t *thread) if (writer->thread_stack_count >= SENTRY_CRASH_MAX_THREADS) { return; } - writer->thread_stacks[writer->thread_stack_count++] = thread->stack; + writer->thread_stacks[writer->thread_stack_count] = thread->stack; + writer->thread_stack_tids[writer->thread_stack_count] + = (pid_t)thread->thread_id; + writer->thread_stack_count++; } /** @@ -1976,14 +1985,18 @@ write_thread_names_stream(minidump_writer_t *writer, minidump_directory_t *dir) name_list->thread_names[i].thread_name_rva = name_rvas[i]; } - dir->stream_type = MINIDUMP_STREAM_THREAD_NAMES; - dir->rva = write_data(writer, name_list, list_size); - dir->data_size = list_size; - + // Stage rva first so a write_data failure leaves dir untouched + // (data_size > 0 with rva == 0 would point parsers at the header). + minidump_rva_t rva = write_data(writer, name_list, list_size); sentry_free(name_list); sentry_free(name_rvas); - - return dir->rva ? 0 : -1; + if (!rva) { + return -1; + } + dir->stream_type = MINIDUMP_STREAM_THREAD_NAMES; + dir->rva = rva; + dir->data_size = list_size; + return 0; } /** @@ -2038,6 +2051,201 @@ should_include_region(const memory_mapping_t *mapping, return false; } +// ===================================================================== +// Indirectly-referenced memory capture (SMART mode) +// ===================================================================== +// +// Mirrors Windows' MiniDumpWithIndirectlyReferencedMemory: walks every +// captured thread's stack words and the crashing thread's GPRs and, for +// each value that lands inside a writable heap mapping, captures a small +// page-aligned chunk so debuggers can chase pointers held in struct +// locals at crash time. +// +// The algorithm + accumulator + dedup live in sentry_minidump_indirect.c. +// This block is the Linux platform shim: it provides the two callbacks +// (is_writable_heap, read_memory) and drives the walker for each thread. + +/** + * Find the mapping containing addr via binary search over writer->mappings, + * which is sorted by /proc//maps in ascending start order. Returns NULL + * if addr is not mapped. + */ +static const memory_mapping_t * +find_mapping_for_addr(const minidump_writer_t *writer, uint64_t addr) +{ + size_t lo = 0; + size_t hi = writer->mapping_count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + const memory_mapping_t *m = &writer->mappings[mid]; + if (addr < m->start) { + hi = mid; + } else if (addr >= m->end) { + lo = mid + 1; + } else { + return m; + } + } + return NULL; +} + +static bool +linux_indirect_is_writable_heap(void *ctx, uint64_t addr) +{ + const minidump_writer_t *writer = (const minidump_writer_t *)ctx; + const memory_mapping_t *m = find_mapping_for_addr(writer, addr); + if (!m) { + return false; + } + // Must be readable+writable, not executable. This admits [heap] and + // anonymous rw-p mappings (typical heap allocators); rejects code + // pages, rwx JIT pages (rare and risky), and read-only data segments. + if (m->permissions[0] != 'r' || m->permissions[1] != 'w') { + return false; + } + if (m->permissions[2] == 'x') { + return false; + } + // Reject kernel-private/special regions: [stack...], [vdso], [vvar], + // [vsyscall]. Stacks are already in MemoryListStream via thread_stacks + // so visiting them here would just waste budget on dedup work. + if (m->name[0] == '[') { + // [heap] is the one named-bracket region we DO want. + if (strncmp(m->name, "[heap]", 6) == 0) { + return true; + } + return false; + } + return true; +} + +static ssize_t +linux_indirect_read_memory(void *ctx, uint64_t addr, void *buf, size_t len) +{ + return read_process_memory((minidump_writer_t *)ctx, addr, buf, len); +} + +/** + * Walk the registers of the captured ucontext for `tid` (if available) + * through sentry__indirect_consider. The crash daemon only stores ucontext + * for threads whose context arrived via the signal handler — typically the + * crashing thread plus any other thread that hit the same handler. For + * threads captured via ptrace the registers are already gone by the time + * we get here, so we silently skip them; their stack contents still get + * walked below. + */ +static void +walk_indirect_registers_for_tid(minidump_writer_t *writer, pid_t tid, + sentry_indirect_accumulator_t *acc, const sentry_indirect_ops_t *ops) +{ + const ucontext_t *uctx = NULL; + size_t num = writer->crash_ctx->platform.num_threads; + if (num > SENTRY_CRASH_MAX_THREADS) { + num = SENTRY_CRASH_MAX_THREADS; + } + for (size_t j = 0; j < num; j++) { + if (writer->crash_ctx->platform.threads[j].tid == tid) { + uctx = &writer->crash_ctx->platform.threads[j].context; + break; + } + } + if (!uctx) { + return; + } + + minidump_writer_base_t *base = (minidump_writer_base_t *)writer; +# if defined(__aarch64__) + for (int r = 0; r <= 30; r++) { + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.regs[r], ops); + } + sentry__indirect_consider(acc, base, (uint64_t)uctx->uc_mcontext.sp, ops); +# elif defined(__x86_64__) + static const int x86_64_gprs[] = { REG_RAX, REG_RBX, REG_RCX, REG_RDX, + REG_RSI, REG_RDI, REG_RBP, REG_RSP, REG_R8, REG_R9, REG_R10, REG_R11, + REG_R12, REG_R13, REG_R14, REG_R15 }; + for (size_t i = 0; i < sizeof(x86_64_gprs) / sizeof(x86_64_gprs[0]); i++) { + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.gregs[x86_64_gprs[i]], ops); + } +# elif defined(__i386__) + static const int x86_gprs[] = { REG_EAX, REG_EBX, REG_ECX, REG_EDX, REG_ESI, + REG_EDI, REG_EBP, REG_ESP }; + for (size_t i = 0; i < sizeof(x86_gprs) / sizeof(x86_gprs[0]); i++) { + sentry__indirect_consider(acc, base, + (uint64_t)(uint32_t)uctx->uc_mcontext.gregs[x86_gprs[i]], ops); + } +# elif defined(__arm__) + // ARMv7's mcontext fields are individual struct members rather than + // an array; walk them explicitly. Most heap pointers we care about + // either come through r0..r3 (call args / return value), r7 (frame + // pointer in some calling conventions), sp, or lr. + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.arm_r0, ops); + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.arm_r1, ops); + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.arm_r2, ops); + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.arm_r3, ops); + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.arm_r7, ops); + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.arm_sp, ops); + sentry__indirect_consider( + acc, base, (uint64_t)uctx->uc_mcontext.arm_lr, ops); +# endif +} + +/** + * For SMART mode: scan every captured thread's stack and the crashing + * thread's registers for heap pointers, capturing a chunk around each. + * Stack bytes are pread()'d back from disk (we already wrote them in + * write_thread_list_stream) rather than retained in memory, to avoid + * doubling crash-time memory pressure. + */ +static void +capture_indirect_memory( + minidump_writer_t *writer, sentry_indirect_accumulator_t *acc) +{ + sentry_indirect_ops_t ops = { + .is_writable_heap = linux_indirect_is_writable_heap, + .read_memory = linux_indirect_read_memory, + .ctx = writer, + }; + minidump_writer_base_t *base = (minidump_writer_base_t *)writer; + + for (size_t i = 0; i < writer->thread_stack_count; i++) { + const minidump_memory_descriptor_t *desc = &writer->thread_stacks[i]; + size_t stack_size = desc->memory.size; + if (stack_size == 0 || desc->memory.rva == 0) { + continue; + } + void *stack_buf = sentry_malloc(stack_size); + if (!stack_buf) { + continue; + } + ssize_t got + = pread(writer->fd, stack_buf, stack_size, (off_t)desc->memory.rva); + if (got > 0) { + sentry__indirect_walk_words( + acc, base, stack_buf, (size_t)got, &ops); + } + sentry_free(stack_buf); + + // After scanning this thread's stack, also scan its registers if + // we have them. Registers usually point into either the same stack + // (already walked) or live heap objects we want to capture. + walk_indirect_registers_for_tid( + writer, writer->thread_stack_tids[i], acc, &ops); + + if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES + || acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) { + break; + } + } +} + /** * Write memory list stream (heap memory based on minidump mode) */ @@ -2047,6 +2255,19 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) // Get crash address for SMART mode filtering uint64_t crash_addr = (uint64_t)writer->crash_ctx->platform.siginfo.si_addr; + // SMART mode: scan registers + stack words for pointers into writable + // heap mappings and capture a chunk around each. Has to run BEFORE the + // memory_list allocation below so we know the final region count. The + // bytes are written into the dump file as we go; we only collect the + // descriptors here and merge them into the unified list at the end. + sentry_indirect_accumulator_t indirect_acc; + sentry__indirect_init(&indirect_acc); + if (writer->crash_ctx->minidump_mode == SENTRY_MINIDUMP_MODE_SMART) { + capture_indirect_memory(writer, &indirect_acc); + SENTRY_DEBUGF("indirect memory: captured %zu regions, %zu bytes", + indirect_acc.region_count, indirect_acc.total_bytes); + } + // Count regions to include based on mode size_t region_count = 0; for (size_t i = 0; i < writer->mapping_count; i++) { @@ -2060,7 +2281,8 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) // (written by the thread-list writer) so we just emit descriptors // pointing at the same RVA. This is what lets LLDB resolve memory at // stack addresses and walk the FP chain. - size_t total_count = region_count + writer->thread_stack_count; + size_t total_count + = region_count + writer->thread_stack_count + indirect_acc.region_count; // Allocate memory list size_t list_size = sizeof(uint32_t) @@ -2079,6 +2301,15 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) memory_list->ranges[mem_idx++] = writer->thread_stacks[s]; } + // Append indirectly-referenced regions (heap chunks pointed to by + // registers/stack contents). Already deduped + capped by the walker. + for (size_t s = 0; s < indirect_acc.region_count; s++) { + minidump_memory_descriptor_t *mem = &memory_list->ranges[mem_idx++]; + mem->start_address = indirect_acc.regions[s].start; + mem->memory.rva = indirect_acc.regions[s].rva; + mem->memory.size = indirect_acc.regions[s].size; + } + // Write memory regions for (size_t i = 0; i < writer->mapping_count && mem_idx < total_count; i++) { @@ -2222,6 +2453,10 @@ read_proc_file(const char *path, size_t max_size, size_t *size_out) /** * Helper: write a /proc file (or any other path) as a single stream. * Returns 0 on success, -1 on any failure (including file unavailable). + * + * On failure (read or write_data) the directory entry is fully zeroed — + * a non-zero data_size paired with rva=0 would point parsers at the + * minidump header. */ static int write_file_as_stream(minidump_writer_t *writer, minidump_directory_t *dir, @@ -2233,11 +2468,15 @@ write_file_as_stream(minidump_writer_t *writer, minidump_directory_t *dir, return -1; } + minidump_rva_t rva = write_data(writer, buf, size); + sentry_free(buf); + if (!rva) { + return -1; + } dir->stream_type = stream_type; - dir->rva = write_data(writer, buf, size); + dir->rva = rva; dir->data_size = (uint32_t)size; - sentry_free(buf); - return dir->rva ? 0 : -1; + return 0; } /** @@ -2521,6 +2760,7 @@ write_linux_dso_debug_stream( size_t links_size = dso_count * sizeof(minidump_link_map64_t); minidump_link_map64_t *links = NULL; minidump_rva_t links_rva = 0; + size_t links_written = 0; if (dso_count > 0) { links = sentry_malloc(links_size); if (!links) { @@ -2531,9 +2771,8 @@ write_linux_dso_debug_stream( // Walk again, this time emitting strings (which share the same RVA // space as the link_map array — strings get written first, then the // array of fixed-size entries last so its RVA is contiguous). - size_t idx = 0; next = rd.r_map; - while (next && idx < dso_count) { + while (next && links_written < dso_count) { struct link_map lm; if (read_process_memory( writer, (uint64_t)(uintptr_t)next, &lm, sizeof(lm)) @@ -2553,16 +2792,23 @@ write_linux_dso_debug_stream( } } - links[idx].addr = (uint64_t)lm.l_addr; - links[idx].name = write_minidump_string(writer, namebuf); - links[idx].ld = (uint64_t)(uintptr_t)lm.l_ld; - idx++; + links[links_written].addr = (uint64_t)lm.l_addr; + links[links_written].name = write_minidump_string(writer, namebuf); + links[links_written].ld = (uint64_t)(uintptr_t)lm.l_ld; + links_written++; next = lm.l_next; } - links_rva = write_data(writer, links, links_size); + // Only emit the entries we actually populated. Trailing zero entries + // (from the initial memset) would point parsers at the minidump + // header via name=rva 0, so use links_written rather than dso_count + // to size the array on disk and in the header. + if (links_written > 0) { + links_rva = write_data( + writer, links, links_written * sizeof(minidump_link_map64_t)); + } sentry_free(links); - if (!links_rva) { + if (links_written > 0 && !links_rva) { return -1; } } @@ -2580,7 +2826,7 @@ write_linux_dso_debug_stream( minidump_debug64_t *hdr = (minidump_debug64_t *)blob; hdr->version = have_rdebug ? rd.r_version : 0; hdr->map = links_rva; - hdr->dso_count = (uint32_t)dso_count; + hdr->dso_count = (uint32_t)links_written; hdr->brk = have_rdebug ? (uint64_t)rd.r_brk : 0; hdr->ldbase = at_base; // matches Breakpad: AT_BASE from auxv hdr->dynamic = dynamic_addr; @@ -2596,16 +2842,22 @@ write_linux_dso_debug_stream( } } + // Stage rva first so a write_data failure leaves dir untouched + // (data_size > 0 with rva == 0 would point parsers at the header). + minidump_rva_t rva = write_data(writer, blob, total_size); + sentry_free(blob); + if (!rva) { + return -1; + } dir->stream_type = MINIDUMP_STREAM_LINUX_DSO_DEBUG; - dir->rva = write_data(writer, blob, total_size); + dir->rva = rva; dir->data_size = (uint32_t)total_size; - sentry_free(blob); SENTRY_DEBUGF("dso_debug: wrote %zu DSOs (dynamic@0x%llx, r_debug@0x%llx)", - dso_count, (unsigned long long)dynamic_addr, + links_written, (unsigned long long)dynamic_addr, (unsigned long long)r_debug_addr); (void)at_entry; // currently unused; logged via auxv for debugging - return dir->rva ? 0 : -1; + return 0; } /** @@ -2706,63 +2958,73 @@ sentry__write_minidump( result |= write_memory_list_stream(&writer, &directories[4]); // Write Linux-specific streams for debugger compatibility. - // These are non-fatal: if they fail, the minidump is still valid; the - // directory slot ends up zeroed (stream_type=0 = unused). + // These are non-fatal: if they fail, the minidump is still valid. The + // fallback explicitly zeros data_size and rva (in addition to the type) + // so a partially-written entry can't end up with data_size > 0 and + // rva = 0 — which would point parsers at the minidump header. +# define NULLIFY_DIR_ENTRY(idx, type) \ + do { \ + directories[(idx)].stream_type = (type); \ + directories[(idx)].data_size = 0; \ + directories[(idx)].rva = 0; \ + } while (0) + SENTRY_DEBUG("writing linux proc status stream"); if (write_linux_proc_status_stream(&writer, &directories[5]) < 0) { SENTRY_WARN("failed to write linux proc status stream"); - directories[5].stream_type = MINIDUMP_STREAM_LINUX_PROC_STATUS; + NULLIFY_DIR_ENTRY(5, MINIDUMP_STREAM_LINUX_PROC_STATUS); } SENTRY_DEBUG("writing linux maps stream"); if (write_linux_maps_stream(&writer, &directories[6]) < 0) { SENTRY_WARN("failed to write linux maps stream"); - directories[6].stream_type = MINIDUMP_STREAM_LINUX_MAPS; + NULLIFY_DIR_ENTRY(6, MINIDUMP_STREAM_LINUX_MAPS); } // Write thread names stream (matches Crashpad format) SENTRY_DEBUG("writing thread names stream"); if (write_thread_names_stream(&writer, &directories[7]) < 0) { SENTRY_WARN("failed to write thread names stream"); - directories[7].stream_type = MINIDUMP_STREAM_THREAD_NAMES; + NULLIFY_DIR_ENTRY(7, MINIDUMP_STREAM_THREAD_NAMES); } SENTRY_DEBUG("writing linux auxv stream"); if (write_linux_auxv_stream(&writer, &directories[8]) < 0) { SENTRY_WARN("failed to write linux auxv stream"); - directories[8].stream_type = MINIDUMP_STREAM_LINUX_AUXV; + NULLIFY_DIR_ENTRY(8, MINIDUMP_STREAM_LINUX_AUXV); } SENTRY_DEBUG("writing linux cpu info stream"); if (write_linux_cpu_info_stream(&writer, &directories[9]) < 0) { SENTRY_DEBUG("linux cpu info stream unavailable"); - directories[9].stream_type = MINIDUMP_STREAM_LINUX_CPU_INFO; + NULLIFY_DIR_ENTRY(9, MINIDUMP_STREAM_LINUX_CPU_INFO); } SENTRY_DEBUG("writing linux lsb release stream"); if (write_linux_lsb_release_stream(&writer, &directories[10]) < 0) { // Many distros don't ship /etc/lsb-release; this is expected. SENTRY_DEBUG("linux lsb release stream unavailable"); - directories[10].stream_type = MINIDUMP_STREAM_LINUX_LSB_RELEASE; + NULLIFY_DIR_ENTRY(10, MINIDUMP_STREAM_LINUX_LSB_RELEASE); } SENTRY_DEBUG("writing linux cmd line stream"); if (write_linux_cmd_line_stream(&writer, &directories[11]) < 0) { SENTRY_DEBUG("linux cmd line stream unavailable"); - directories[11].stream_type = MINIDUMP_STREAM_LINUX_CMD_LINE; + NULLIFY_DIR_ENTRY(11, MINIDUMP_STREAM_LINUX_CMD_LINE); } SENTRY_DEBUG("writing linux environ stream"); if (write_linux_environ_stream(&writer, &directories[12]) < 0) { SENTRY_DEBUG("linux environ stream unavailable"); - directories[12].stream_type = MINIDUMP_STREAM_LINUX_ENVIRON; + NULLIFY_DIR_ENTRY(12, MINIDUMP_STREAM_LINUX_ENVIRON); } SENTRY_DEBUG("writing linux dso debug stream"); if (write_linux_dso_debug_stream(&writer, &directories[13]) < 0) { SENTRY_WARN("failed to write linux dso debug stream"); - directories[13].stream_type = MINIDUMP_STREAM_LINUX_DSO_DEBUG; + NULLIFY_DIR_ENTRY(13, MINIDUMP_STREAM_LINUX_DSO_DEBUG); } +# undef NULLIFY_DIR_ENTRY if (result < 0) { if (writer.ptrace_attached) { diff --git a/src/backends/native/minidump/sentry_minidump_macos.c b/src/backends/native/minidump/sentry_minidump_macos.c index bde715c79..cdd02fef5 100644 --- a/src/backends/native/minidump/sentry_minidump_macos.c +++ b/src/backends/native/minidump/sentry_minidump_macos.c @@ -22,6 +22,7 @@ # include "sentry_logger.h" # include "sentry_minidump_common.h" # include "sentry_minidump_format.h" +# include "sentry_minidump_indirect.h" # include "sentry_minidump_writer.h" // Use shared constants from crash context @@ -65,6 +66,17 @@ typedef struct { memory_region_t regions[SENTRY_CRASH_MAX_MAPPINGS]; size_t region_count; + + // Thread-stack memory descriptors recorded as we write the ThreadList + // stream. We replay them into MemoryListStream so debuggers (notably + // LLDB's ProcessMinidump) can find stack bytes by virtual address — + // without this LLDB knows the stack region exists but cannot read its + // contents and unwinding stops after frame 0. thread_stack_tids[i] + // identifies the thread for the SMART-mode indirect-memory walker so + // it can find the matching thread context for register scanning. + minidump_memory_descriptor_t thread_stacks[SENTRY_CRASH_MAX_THREADS]; + uint32_t thread_stack_tids[SENTRY_CRASH_MAX_THREADS]; + size_t thread_stack_count; } minidump_writer_t; // Use common minidump functions (cast writer to base type) @@ -456,6 +468,26 @@ write_thread_context( # endif } +/** + * Push a thread's stack descriptor onto writer->thread_stacks so the + * MemoryListStream writer can replay it. We only record stacks whose + * write actually produced bytes — a zero-size or zero-rva descriptor + * would point into the minidump header and break parsers. + */ +static void +record_thread_stack(minidump_writer_t *writer, const minidump_thread_t *thread) +{ + if (thread->stack.memory.size == 0 || thread->stack.memory.rva == 0) { + return; + } + if (writer->thread_stack_count >= SENTRY_CRASH_MAX_THREADS) { + return; + } + writer->thread_stacks[writer->thread_stack_count] = thread->stack; + writer->thread_stack_tids[writer->thread_stack_count] = thread->thread_id; + writer->thread_stack_count++; +} + /** * Read and write stack memory for a thread */ @@ -607,6 +639,7 @@ write_thread_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) = write_thread_stack(writer, sp, &stack_size, &stack_start); thread->stack.memory.size = stack_size; thread->stack.start_address = stack_start; + record_thread_stack(writer, thread); } } } else if (writer->crash_ctx @@ -660,6 +693,7 @@ write_thread_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) : 0; thread->stack.start_address = thread->stack.memory.rva ? sp : 0; + record_thread_stack(writer, thread); SENTRY_DEBUGF( "Thread %zu: wrote stack from file at RVA " "0x%x, size %llu, start_addr 0x%llx", @@ -691,6 +725,7 @@ write_thread_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) = write_thread_stack(writer, sp, &stack_size, &stack_start); thread->stack.memory.size = stack_size; thread->stack.start_address = stack_start; + record_thread_stack(writer, thread); SENTRY_DEBUGF( "Thread %zu: wrote stack from memory at RVA 0x%x, size %zu", i, thread->stack.memory.rva, stack_size); @@ -962,6 +997,177 @@ write_module_headers_from_capture(minidump_writer_t *writer, return written_count; } +// ===================================================================== +// Indirectly-referenced memory capture (SMART mode) +// ===================================================================== +// +// Mirrors Windows' MiniDumpWithIndirectlyReferencedMemory: walks every +// captured thread's stack words and the crashing thread's GPRs and, for +// each value that lands inside a writable heap mapping, captures a small +// page-aligned chunk so debuggers can chase pointers held in struct +// locals at crash time. +// +// The algorithm + accumulator + dedup live in sentry_minidump_indirect.c. +// This block is the macOS platform shim. + +/** + * Find the VM region containing addr via binary search over writer->regions. + * mach_vm_region enumerates in ascending address order so the array is sorted. + */ +static const memory_region_t * +find_region_for_addr(const minidump_writer_t *writer, uint64_t addr) +{ + size_t lo = 0; + size_t hi = writer->region_count; + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + const memory_region_t *r = &writer->regions[mid]; + if (addr < r->address) { + hi = mid; + } else if (addr >= r->address + r->size) { + lo = mid + 1; + } else { + return r; + } + } + return NULL; +} + +static bool +macos_indirect_is_writable_heap(void *ctx, uint64_t addr) +{ + const minidump_writer_t *writer = (const minidump_writer_t *)ctx; + const memory_region_t *r = find_region_for_addr(writer, addr); + if (!r) { + return false; + } + // Must be readable+writable, not executable. macOS regions don't carry + // names, so we can't reject thread stacks here cheaply — but the + // accumulator's overlap-check against thread stacks already in the + // memory list keeps duplicate captures bounded. + if ((r->protection & VM_PROT_READ) == 0) { + return false; + } + if ((r->protection & VM_PROT_WRITE) == 0) { + return false; + } + if ((r->protection & VM_PROT_EXECUTE) != 0) { + return false; + } + return true; +} + +static ssize_t +macos_indirect_read_memory(void *ctx, uint64_t addr, void *buf, size_t len) +{ + minidump_writer_t *writer = (minidump_writer_t *)ctx; + kern_return_t kr = read_task_memory( + writer->task, (mach_vm_address_t)addr, buf, (mach_vm_size_t)len); + return kr == KERN_SUCCESS ? (ssize_t)len : -1; +} + +/** + * Walk the crashing thread's GPRs through sentry__indirect_consider. We + * only have stored mcontexts for threads captured via the signal handler + * (typically the crashing one); thread_get_state-derived contexts for + * other threads are not retained, so their registers aren't scanned here. + * Their stack contents still get walked. + */ +static void +walk_indirect_registers_for_tid(minidump_writer_t *writer, uint32_t tid, + sentry_indirect_accumulator_t *acc, const sentry_indirect_ops_t *ops) +{ + const _STRUCT_MCONTEXT *state = NULL; + size_t num = writer->crash_ctx->platform.num_threads; + if (num > SENTRY_CRASH_MAX_THREADS) { + num = SENTRY_CRASH_MAX_THREADS; + } + for (size_t j = 0; j < num; j++) { + if (writer->crash_ctx->platform.threads[j].tid == tid) { + state = &writer->crash_ctx->platform.threads[j].state; + break; + } + } + if (!state) { + return; + } + + minidump_writer_base_t *base = (minidump_writer_base_t *)writer; +# if defined(__x86_64__) + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rax, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rbx, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rcx, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rdx, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rsi, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rdi, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rbp, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__rsp, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r8, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r9, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r10, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r11, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r12, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r13, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r14, ops); + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__r15, ops); +# elif defined(__aarch64__) + for (int r = 0; r <= 28; r++) { + sentry__indirect_consider(acc, base, (uint64_t)state->__ss.__x[r], ops); + } + sentry__indirect_consider( + acc, base, (uint64_t)SENTRY__ARM64_GET_FP(state->__ss), ops); + sentry__indirect_consider( + acc, base, (uint64_t)SENTRY__ARM64_GET_LR(state->__ss), ops); + sentry__indirect_consider( + acc, base, (uint64_t)SENTRY__ARM64_GET_SP(state->__ss), ops); +# endif +} + +/** + * For SMART mode: scan every captured thread's stack and the crashing + * thread's registers for heap pointers, capturing a chunk around each. + * Stack bytes are pread()'d back from disk (they were written earlier + * when the thread list was emitted) rather than retained in memory. + */ +static void +capture_indirect_memory_macos( + minidump_writer_t *writer, sentry_indirect_accumulator_t *acc) +{ + sentry_indirect_ops_t ops = { + .is_writable_heap = macos_indirect_is_writable_heap, + .read_memory = macos_indirect_read_memory, + .ctx = writer, + }; + minidump_writer_base_t *base = (minidump_writer_base_t *)writer; + + for (size_t i = 0; i < writer->thread_stack_count; i++) { + const minidump_memory_descriptor_t *desc = &writer->thread_stacks[i]; + size_t stack_size = desc->memory.size; + if (stack_size == 0 || desc->memory.rva == 0) { + continue; + } + void *stack_buf = sentry_malloc(stack_size); + if (!stack_buf) { + continue; + } + ssize_t got + = pread(writer->fd, stack_buf, stack_size, (off_t)desc->memory.rva); + if (got > 0) { + sentry__indirect_walk_words( + acc, base, stack_buf, (size_t)got, &ops); + } + sentry_free(stack_buf); + + walk_indirect_registers_for_tid( + writer, writer->thread_stack_tids[i], acc, &ops); + + if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES + || acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) { + break; + } + } +} + /** * Write memory list stream with memory based on minidump mode. * @@ -987,6 +1193,18 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) // When we have VM regions (task_for_pid succeeded), use them if (writer->region_count > 0) { + // SMART mode: scan registers + stack words for pointers into + // writable heap regions and capture a chunk around each. Has to + // run before the memory_list allocation below so we know the + // final region count. + sentry_indirect_accumulator_t indirect_acc; + sentry__indirect_init(&indirect_acc); + if (mode == SENTRY_MINIDUMP_MODE_SMART) { + capture_indirect_memory_macos(writer, &indirect_acc); + SENTRY_DEBUGF("indirect memory: captured %zu regions, %zu bytes", + indirect_acc.region_count, indirect_acc.total_bytes); + } + // Count regions to include size_t region_count = 0; for (size_t i = 0; i < writer->region_count; i++) { @@ -996,17 +1214,38 @@ write_memory_list_stream(minidump_writer_t *writer, minidump_directory_t *dir) } } + size_t total_count = region_count + writer->thread_stack_count + + indirect_acc.region_count; + size_t list_size = sizeof(uint32_t) - + (region_count * sizeof(minidump_memory_descriptor_t)); + + (total_count * sizeof(minidump_memory_descriptor_t)); minidump_memory_list_t *memory_list = sentry_malloc(list_size); if (!memory_list) { goto empty_list; } - memory_list->count = region_count; + memory_list->count = total_count; size_t mem_idx = 0; - for (size_t i = 0; i < writer->region_count && mem_idx < region_count; + + // Replay thread stacks first — bytes are already on disk via the + // thread-list writer, so this just emits descriptors pointing at + // the same RVA. Lets debuggers resolve memory at stack addresses + // and walk the FP chain. + for (size_t s = 0; s < writer->thread_stack_count; s++) { + memory_list->ranges[mem_idx++] = writer->thread_stacks[s]; + } + + // Append indirectly-referenced regions. Already deduped + capped + // by the walker. + for (size_t s = 0; s < indirect_acc.region_count; s++) { + minidump_memory_descriptor_t *mem = &memory_list->ranges[mem_idx++]; + mem->start_address = indirect_acc.regions[s].start; + mem->memory.rva = indirect_acc.regions[s].rva; + mem->memory.size = indirect_acc.regions[s].size; + } + + for (size_t i = 0; i < writer->region_count && mem_idx < total_count; i++) { if (!should_include_region_macos( &writer->regions[i], writer->crash_ctx, crash_addr)) { From 6fca3ddf8a976947a2979cdfda9336ee4b68ea92 Mon Sep 17 00:00:00 2001 From: Amir Mujacic Date: Tue, 5 May 2026 13:07:52 +0200 Subject: [PATCH 7/7] Fixes --- .../native/minidump/sentry_minidump_format.h | 14 ++++++++------ .../native/minidump/sentry_minidump_linux.c | 6 ++++-- .../native/minidump/sentry_minidump_macos.c | 6 ++++-- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_format.h b/src/backends/native/minidump/sentry_minidump_format.h index eaf5e16ef..4f14a9a5c 100644 --- a/src/backends/native/minidump/sentry_minidump_format.h +++ b/src/backends/native/minidump/sentry_minidump_format.h @@ -476,16 +476,19 @@ PACKED_STRUCT_END /** * Linux DSO debug structures (MD_LINUX_DSO_DEBUG, 0x4767000A) * - * Mirrors Breakpad's MDRawLinkMap64 / MDRawDebug64. LLDB and rust-minidump - * use this stream to enumerate loaded shared objects when LinuxAuxv alone - * isn't enough (e.g. modules loaded via dlopen after process startup that - * aren't in /proc//maps in a usable order). + * Wire-compatible with Breakpad's MDRawLinkMap64 / MDRawDebug64. Breakpad + * builds these under `#pragma pack(push, 4)`, so a uint64_t can sit on a + * 4-byte boundary with no padding inserted between the preceding uint32_t + * and itself. We use PACKED_ATTR (== __attribute__((packed))) to get the + * same byte-exact layout: 20 bytes for link_map64, 36 bytes for debug64. + * + * Adding explicit _pad fields here would break the layout — LLDB and + * rust-minidump compute offsets assuming the Breakpad sizes. */ PACKED_STRUCT_BEGIN typedef struct { uint64_t addr; // l_addr from struct link_map minidump_rva_t name; // RVA to MINIDUMP_STRING with the SO path - uint32_t _pad; uint64_t ld; // l_ld from struct link_map (PT_DYNAMIC of this DSO) } PACKED_ATTR minidump_link_map64_t; PACKED_STRUCT_END @@ -495,7 +498,6 @@ typedef struct { uint32_t version; // r_debug.r_version minidump_rva_t map; // RVA to array of minidump_link_map64_t uint32_t dso_count; // number of entries in map - uint32_t _pad; uint64_t brk; // r_debug.r_brk uint64_t ldbase; // r_debug.r_ldbase uint64_t dynamic; // address of the program's _DYNAMIC section diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 6fc2893ad..d16ad0616 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -2874,8 +2874,10 @@ sentry__write_minidump( minidump_writer_t writer = { 0 }; writer.crash_ctx = ctx; - // Open output file - writer.fd = open(output_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + // Open output file. O_RDWR (not O_WRONLY) because SMART-mode indirect + // memory capture pread()'s thread-stack bytes back from the dump after + // they're written, so it can scan them for heap pointers. + writer.fd = open(output_path, O_RDWR | O_CREAT | O_TRUNC, 0600); if (writer.fd < 0) { SENTRY_WARNF("failed to create minidump: %s", strerror(errno)); return -1; diff --git a/src/backends/native/minidump/sentry_minidump_macos.c b/src/backends/native/minidump/sentry_minidump_macos.c index cdd02fef5..c3563b8c1 100644 --- a/src/backends/native/minidump/sentry_minidump_macos.c +++ b/src/backends/native/minidump/sentry_minidump_macos.c @@ -1378,9 +1378,11 @@ sentry__write_minidump( minidump_writer_t writer = { 0 }; writer.crash_ctx = ctx; - // Open output file + // Open output file. O_RDWR (not O_WRONLY) because SMART-mode indirect + // memory capture pread()'s thread-stack bytes back from the dump after + // they're written, so it can scan them for heap pointers. SENTRY_DEBUGF("write_minidump: opening file %s", output_path); - writer.fd = open(output_path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + writer.fd = open(output_path, O_RDWR | O_CREAT | O_TRUNC, 0600); if (writer.fd < 0) { SENTRY_WARN("write_minidump: failed to open file"); return -1;