Skip to content

Commit c5d4fda

Browse files
committed
Fix comments + indirect memory
1 parent f2a845e commit c5d4fda

6 files changed

Lines changed: 847 additions & 38 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
- 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))
1616
- 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))
1717
- 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))
18+
- 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))
19+
20+
**Features**:
21+
22+
- 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))
1823

1924
## 0.13.9
2025

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,13 @@ elseif(SENTRY_BACKEND_NATIVE)
173173
if(LINUX OR ANDROID)
174174
sentry_target_sources_cwd(sentry
175175
backends/native/minidump/sentry_minidump_common.c
176+
backends/native/minidump/sentry_minidump_indirect.c
176177
backends/native/minidump/sentry_minidump_linux.c
177178
)
178179
elseif(APPLE)
179180
sentry_target_sources_cwd(sentry
180181
backends/native/minidump/sentry_minidump_common.c
182+
backends/native/minidump/sentry_minidump_indirect.c
181183
backends/native/minidump/sentry_minidump_macos.c
182184
)
183185
elseif(WIN32)
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#include "sentry_boot.h"
2+
3+
#include "sentry_minidump_indirect.h"
4+
5+
#include "sentry_alloc.h"
6+
#include "sentry_logger.h"
7+
#include "sentry_minidump_common.h"
8+
9+
#include <stdint.h>
10+
#include <string.h>
11+
12+
void
13+
sentry__indirect_init(sentry_indirect_accumulator_t *acc)
14+
{
15+
acc->region_count = 0;
16+
acc->total_bytes = 0;
17+
}
18+
19+
/**
20+
* Binary-search the sorted region list for the first entry whose `end` is
21+
* strictly greater than `target`. The candidate at index `lo` (if any)
22+
* is the only one that can possibly overlap [target, target+len).
23+
*
24+
* Returns region_count when no such entry exists (target is past the last).
25+
*/
26+
static size_t
27+
find_first_after(const sentry_indirect_accumulator_t *acc, uint64_t target)
28+
{
29+
size_t lo = 0;
30+
size_t hi = acc->region_count;
31+
while (lo < hi) {
32+
size_t mid = lo + (hi - lo) / 2;
33+
if (acc->regions[mid].end <= target) {
34+
lo = mid + 1;
35+
} else {
36+
hi = mid;
37+
}
38+
}
39+
return lo;
40+
}
41+
42+
/**
43+
* Returns true if [start, end) overlaps any region already in the
44+
* accumulator. The list is sorted, so we only need to check the first
45+
* region whose end > start — anything earlier ended before us, anything
46+
* later starts after we'd want it to.
47+
*/
48+
static bool
49+
overlaps_existing(
50+
const sentry_indirect_accumulator_t *acc, uint64_t start, uint64_t end)
51+
{
52+
size_t i = find_first_after(acc, start);
53+
if (i >= acc->region_count) {
54+
return false;
55+
}
56+
return acc->regions[i].start < end;
57+
}
58+
59+
/**
60+
* Insert a new region at the sorted position. Caller must have verified
61+
* via overlaps_existing() that no overlap exists, and via the cap checks
62+
* that there's room.
63+
*/
64+
static void
65+
insert_sorted(sentry_indirect_accumulator_t *acc,
66+
const sentry_indirect_region_t *new_region)
67+
{
68+
size_t i = find_first_after(acc, new_region->start);
69+
if (i < acc->region_count) {
70+
memmove(&acc->regions[i + 1], &acc->regions[i],
71+
(acc->region_count - i) * sizeof(*acc->regions));
72+
}
73+
acc->regions[i] = *new_region;
74+
acc->region_count++;
75+
acc->total_bytes += new_region->size;
76+
}
77+
78+
void
79+
sentry__indirect_consider(sentry_indirect_accumulator_t *acc,
80+
minidump_writer_base_t *writer, uint64_t addr,
81+
const sentry_indirect_ops_t *ops)
82+
{
83+
// Cap checks first — they're the cheapest filters and short-circuit the
84+
// mapping lookup once we've spent the budget.
85+
if (acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) {
86+
return;
87+
}
88+
if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES) {
89+
return;
90+
}
91+
92+
// Cheap rejects before paying for is_writable_heap.
93+
// - 0/low values: NULLs and small ints
94+
// - very high bits set: kernel addresses (canonical AArch64/x86_64 user
95+
// space tops out below this)
96+
if (addr < SENTRY_INDIRECT_PAGE_SIZE) {
97+
return;
98+
}
99+
if ((addr >> 56) != 0) {
100+
return;
101+
}
102+
103+
if (!ops->is_writable_heap(ops->ctx, addr)) {
104+
return;
105+
}
106+
107+
// Page-align down so the captured region covers the page containing the
108+
// pointee (and we can dedup multiple pointers landing in the same page).
109+
// Capture is roughly centered on the pointer but always starts on a page
110+
// boundary so adjacent allocations get covered too.
111+
uint64_t centered = addr - (SENTRY_INDIRECT_PER_POINTER_BYTES / 2);
112+
uint64_t start = centered & ~((uint64_t)SENTRY_INDIRECT_PAGE_SIZE - 1);
113+
uint64_t end = start + SENTRY_INDIRECT_PER_POINTER_BYTES;
114+
// Round end up to page boundary too — small bump but keeps reads aligned.
115+
end = (end + SENTRY_INDIRECT_PAGE_SIZE - 1)
116+
& ~((uint64_t)SENTRY_INDIRECT_PAGE_SIZE - 1);
117+
118+
// Trim against the global byte budget so a candidate near the cap doesn't
119+
// blow it.
120+
size_t want = (size_t)(end - start);
121+
size_t remaining = SENTRY_INDIRECT_MAX_TOTAL_BYTES - acc->total_bytes;
122+
if (want > remaining) {
123+
end = start + remaining;
124+
if (end <= start) {
125+
return;
126+
}
127+
want = (size_t)(end - start);
128+
}
129+
130+
if (overlaps_existing(acc, start, end)) {
131+
return;
132+
}
133+
134+
// Read from the target. Soft-fail on read errors — a pointer into a
135+
// mapped-but-paged-out region or a recently-unmapped one is common during
136+
// crash handling and shouldn't abort the whole walk.
137+
void *buf = sentry_malloc(want);
138+
if (!buf) {
139+
return;
140+
}
141+
ssize_t got = ops->read_memory(ops->ctx, start, buf, want);
142+
if (got <= 0) {
143+
sentry_free(buf);
144+
return;
145+
}
146+
147+
minidump_rva_t rva = sentry__minidump_write_data(writer, buf, (size_t)got);
148+
sentry_free(buf);
149+
if (!rva) {
150+
return;
151+
}
152+
153+
sentry_indirect_region_t r;
154+
r.start = start;
155+
r.end = start + (uint64_t)got;
156+
r.rva = rva;
157+
r.size = (uint32_t)got;
158+
insert_sorted(acc, &r);
159+
}
160+
161+
void
162+
sentry__indirect_walk_words(sentry_indirect_accumulator_t *acc,
163+
minidump_writer_base_t *writer, const void *buf, size_t len_bytes,
164+
const sentry_indirect_ops_t *ops)
165+
{
166+
const size_t word_size = sizeof(void *);
167+
const size_t word_count = len_bytes / word_size;
168+
if (word_size == 8) {
169+
const uint64_t *words = (const uint64_t *)buf;
170+
for (size_t i = 0; i < word_count; i++) {
171+
sentry__indirect_consider(acc, writer, words[i], ops);
172+
// Bail early once the byte cap is fully spent — no point grinding
173+
// through the rest of the stack. The region cap also acts as a
174+
// floor here.
175+
if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES
176+
|| acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) {
177+
return;
178+
}
179+
}
180+
} else {
181+
// 32-bit hosts (Linux i386 / arm). Pointers are 32-bit but our
182+
// accumulator stores them as 64-bit just fine.
183+
const uint32_t *words = (const uint32_t *)buf;
184+
for (size_t i = 0; i < word_count; i++) {
185+
sentry__indirect_consider(acc, writer, (uint64_t)words[i], ops);
186+
if (acc->total_bytes >= SENTRY_INDIRECT_MAX_TOTAL_BYTES
187+
|| acc->region_count >= SENTRY_INDIRECT_MAX_REGIONS) {
188+
return;
189+
}
190+
}
191+
}
192+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#ifndef SENTRY_MINIDUMP_INDIRECT_H_INCLUDED
2+
#define SENTRY_MINIDUMP_INDIRECT_H_INCLUDED
3+
4+
#include "sentry_minidump_common.h"
5+
#include "sentry_minidump_format.h"
6+
#include <stdbool.h>
7+
#include <stddef.h>
8+
#include <stdint.h>
9+
#include <sys/types.h>
10+
11+
/**
12+
* Indirectly-referenced memory capture for SMART minidump mode.
13+
*
14+
* Mirrors the Windows MiniDumpWithIndirectlyReferencedMemory contract:
15+
* for every captured thread, scan its registers and stack words, and for
16+
* each value that resolves into a writable heap region capture a small
17+
* page-aligned chunk of memory around it. Lets debuggers (LLDB, VS Code)
18+
* deref pointers held in struct locals or registers at crash time.
19+
*
20+
* This file is a small algorithm + a 2-function vtable. Each platform
21+
* (linux, macos) provides the vtable and feeds in the per-thread
22+
* registers and stack bytes; the dedup, paging, and dump-emission logic
23+
* lives here once.
24+
*/
25+
26+
// Per-pointer capture size — matches the Windows API default of 1 KiB.
27+
#define SENTRY_INDIRECT_PER_POINTER_BYTES 1024
28+
// Hard caps so dump size doesn't blow up under pathological pointer density.
29+
#define SENTRY_INDIRECT_MAX_REGIONS 1024
30+
#define SENTRY_INDIRECT_MAX_TOTAL_BYTES (4 * 1024 * 1024)
31+
// Page size used for region alignment. 4 KiB on every arch we target.
32+
#define SENTRY_INDIRECT_PAGE_SIZE 4096
33+
34+
typedef struct {
35+
uint64_t start; // page-aligned target VA
36+
uint64_t end; // exclusive
37+
minidump_rva_t rva;
38+
uint32_t size; // bytes actually present in the dump
39+
} sentry_indirect_region_t;
40+
41+
/**
42+
* Platform shim. Each backend supplies its own pointer-validation and
43+
* remote-memory-read implementations; the algorithm calls these via this
44+
* tiny vtable instead of duplicating the loop in every platform file.
45+
*/
46+
typedef struct sentry_indirect_ops_s {
47+
/**
48+
* Returns true iff `addr` falls inside a readable, writable, non-executable
49+
* mapping that is NOT a thread stack, vDSO, or other kernel-private region
50+
* — i.e. the kind of mapping a heap pointer would target.
51+
*
52+
* Called O(words-on-stack) times per dump, so should be O(log n) over the
53+
* cached mappings table (binary search) rather than a linear scan.
54+
*/
55+
bool (*is_writable_heap)(void *ctx, uint64_t addr);
56+
57+
/**
58+
* Read up to `len` bytes from the *target* (crashed) process at virtual
59+
* address `addr` into `buf`. Returns bytes read on success, or -1 on
60+
* failure.
61+
*/
62+
ssize_t (*read_memory)(void *ctx, uint64_t addr, void *buf, size_t len);
63+
64+
/** Opaque pointer passed to the callbacks above. */
65+
void *ctx;
66+
} sentry_indirect_ops_t;
67+
68+
/**
69+
* Bounded accumulator. Holds the regions captured so far plus the running
70+
* byte total used to enforce SENTRY_INDIRECT_MAX_TOTAL_BYTES.
71+
*
72+
* Regions are kept sorted by `start` so dedup is O(log n) per candidate.
73+
*/
74+
typedef struct {
75+
sentry_indirect_region_t regions[SENTRY_INDIRECT_MAX_REGIONS];
76+
size_t region_count;
77+
size_t total_bytes;
78+
} sentry_indirect_accumulator_t;
79+
80+
void sentry__indirect_init(sentry_indirect_accumulator_t *acc);
81+
82+
/**
83+
* Consider one address as a candidate heap pointer. If `addr` resolves into
84+
* a writable-heap mapping (per ops->is_writable_heap), is not already covered
85+
* by a previously-captured region, and the global byte budget hasn't been
86+
* exhausted, this captures SENTRY_INDIRECT_PER_POINTER_BYTES around `addr`,
87+
* page-aligned, and writes them to the dump file via `writer`.
88+
*
89+
* Safe to call with junk values — non-pointer addresses are filtered cheaply
90+
* via the writable-heap check.
91+
*/
92+
void sentry__indirect_consider(sentry_indirect_accumulator_t *acc,
93+
minidump_writer_base_t *writer, uint64_t addr,
94+
const sentry_indirect_ops_t *ops);
95+
96+
/**
97+
* Walk a buffer of pointer-sized words and call sentry__indirect_consider on
98+
* each. `buf` is treated as an array of native-endian uint64_t (or uint32_t
99+
* on 32-bit; the function handles both based on sizeof(void*)). `len_bytes`
100+
* may be unaligned — the trailing bytes shorter than a word are ignored.
101+
*
102+
* This is the hot loop for stack scanning: a 1 MiB stack on AArch64 yields
103+
* 128 K candidates, each costing one O(log n) mapping lookup.
104+
*/
105+
void sentry__indirect_walk_words(sentry_indirect_accumulator_t *acc,
106+
minidump_writer_base_t *writer, const void *buf, size_t len_bytes,
107+
const sentry_indirect_ops_t *ops);
108+
109+
#endif // SENTRY_MINIDUMP_INDIRECT_H_INCLUDED

0 commit comments

Comments
 (0)