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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@
- 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`). ([#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))
- 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

**Features**:
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 63 additions & 1 deletion src/backends/native/minidump/sentry_minidump_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +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_MAPS = 0x47670008,
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)
Expand Down Expand Up @@ -468,4 +473,61 @@ typedef struct {
} PACKED_ATTR minidump_thread_name_list_t;
PACKED_STRUCT_END

/**
* Linux DSO debug structures (MD_LINUX_DSO_DEBUG, 0x4767000A)
*
* Wire-compatible with Breakpad's MDRawLinkMap{32,64} / MDRawDebug{32,64}.
* 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:
* - link_map32: 12 bytes (4 + 4 + 4)
* - link_map64: 20 bytes (8 + 4 + 8)
* - debug32: 24 bytes (4 * 6)
* - debug64: 36 bytes (4 + 4 + 4 + 8 + 8 + 8)
*
* Adding explicit _pad fields here would break the layout — LLDB and
* rust-minidump compute offsets assuming the Breakpad sizes.
*
* The writer picks 32 vs 64 based on the target process's pointer size
* (which is the host pointer size — we don't support cross-bitness dumps).
*/
PACKED_STRUCT_BEGIN
typedef struct {
uint32_t addr; // l_addr from struct link_map
minidump_rva_t name; // RVA to MINIDUMP_STRING with the SO path
uint32_t ld; // l_ld from struct link_map (PT_DYNAMIC of this DSO)
} PACKED_ATTR minidump_link_map32_t;
PACKED_STRUCT_END

PACKED_STRUCT_BEGIN
typedef struct {
uint64_t addr;
minidump_rva_t name;
uint64_t ld;
} 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_map32_t
uint32_t dso_count; // number of entries in map
uint32_t brk; // r_debug.r_brk
uint32_t ldbase; // r_debug.r_ldbase
uint32_t dynamic; // address of the program's _DYNAMIC section
} PACKED_ATTR minidump_debug32_t;
PACKED_STRUCT_END

PACKED_STRUCT_BEGIN
typedef struct {
uint32_t version;
minidump_rva_t map; // RVA to array of minidump_link_map64_t
uint32_t dso_count;
uint64_t brk;
uint64_t ldbase;
uint64_t dynamic;
} PACKED_ATTR minidump_debug64_t;
PACKED_STRUCT_END
Comment thread
cursor[bot] marked this conversation as resolved.

#endif
192 changes: 192 additions & 0 deletions src/backends/native/minidump/sentry_minidump_indirect.c
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <string.h>

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;
}
}
}
}
Loading
Loading