|
| 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 | +} |
0 commit comments