From 79ef475390d7d838213876b915ca108a6c38d672 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 28 Oct 2026 12:37:58 +0100 Subject: [PATCH 1/5] feat(native): Support ELF modules at non-zero file offsets The minidump writer and `capture_modules_from_proc_maps()` grouped `/proc//maps` entries by file name and expected an ELF header at offset 0. An ELF image can also start inside a larger file, such as an uncompressed library that Android maps straight from an APK, and such images were dropped from the module list. Start a module at each mapping whose file offset holds an ELF header, group the image's other segments by its load range, and read the build ID, size, and SONAME at that offset. Move `compute_elf_size_from_phdrs()` to `sentry_elf.h` to share it. This prepares the native backend for Android, where AGP's default packaging maps every app library from the APK. Ref: #1725 --- .../native/minidump/sentry_minidump_linux.c | 148 ++++++------------ src/backends/native/sentry_crash_context.h | 2 + src/backends/native/sentry_crash_daemon.c | 50 ++++-- src/sentry_elf.h | 94 ++++++++++- tests/unit/test_native_backend.c | 21 +++ tests/unit/tests.inc | 1 + 6 files changed, 199 insertions(+), 117 deletions(-) diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 39a2d5be97..25c314ea16 100644 --- a/src/backends/native/minidump/sentry_minidump_linux.c +++ b/src/backends/native/minidump/sentry_minidump_linux.c @@ -862,11 +862,12 @@ write_thread_context( } /** - * Extract Build ID from ELF file + * Extract Build ID from the ELF image at `elf_offset` in a file * Returns the Build ID length, or 0 if not found */ static size_t -extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) +extract_elf_build_id(const char *elf_path, uint64_t elf_offset, + uint8_t *build_id, size_t max_len) { int fd = sentry__elf_open(elf_path); if (fd < 0) { @@ -880,7 +881,7 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) Elf32_Ehdr ehdr; # endif - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) { + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr)) { close(fd); return 0; } @@ -906,7 +907,8 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) return 0; } - if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff + uint64_t shoff = elf_offset + ehdr.e_shoff; + if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); @@ -932,8 +934,8 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) if (!note_buf) continue; - if (lseek(fd, sections[i].sh_offset, SEEK_SET) - == (off_t)sections[i].sh_offset + uint64_t note_offset = elf_offset + sections[i].sh_offset; + if (lseek(fd, (off_t)note_offset, SEEK_SET) == (off_t)note_offset && read(fd, note_buf, note_size) == (ssize_t)note_size) { // Find the GNU build-id note. SHT_NOTE uses 4-byte alignment. size_t desc_size = 0; @@ -958,87 +960,6 @@ extract_elf_build_id(const char *elf_path, uint8_t *build_id, size_t max_len) return build_id_len; } -/** - * Compute the virtual memory size of an ELF file from its PT_LOAD segments. - * This matches Crashpad's behavior: size = max(p_vaddr + p_memsz) - - * min(p_vaddr) across all PT_LOAD segments, rather than using page-aligned - * /proc/maps sizes. - * Returns the computed size, or 0 on failure (caller should fall back to - * /proc/maps size). - */ -static uint64_t -compute_elf_size_from_phdrs(const char *elf_path) -{ - int fd = sentry__elf_open(elf_path); - if (fd < 0) { - return 0; - } - -# if defined(__x86_64__) || defined(__aarch64__) - Elf64_Ehdr ehdr; -# else - Elf32_Ehdr ehdr; -# endif - - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr) - || memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 || ehdr.e_phnum == 0) { - close(fd); - return 0; - } - - if (!sentry__elf_has_phdr_size(ehdr.e_ident, ehdr.e_phentsize)) { - close(fd); - return 0; - } - - // Read program headers - size_t phdr_size = (size_t)ehdr.e_phentsize * ehdr.e_phnum; - void *phdr_buf = sentry_malloc(phdr_size); - if (!phdr_buf) { - close(fd); - return 0; - } - - if (lseek(fd, ehdr.e_phoff, SEEK_SET) != (off_t)ehdr.e_phoff - || read(fd, phdr_buf, phdr_size) != (ssize_t)phdr_size) { - sentry_free(phdr_buf); - close(fd); - return 0; - } - -# if defined(__x86_64__) || defined(__aarch64__) - Elf64_Phdr *phdrs = (Elf64_Phdr *)phdr_buf; -# else - Elf32_Phdr *phdrs = (Elf32_Phdr *)phdr_buf; -# endif - - uint64_t min_vaddr = UINT64_MAX; - uint64_t max_vaddr = 0; - bool found_load = false; - - for (int i = 0; i < ehdr.e_phnum; i++) { - if (phdrs[i].p_type == PT_LOAD) { - found_load = true; - if (phdrs[i].p_vaddr < min_vaddr) { - min_vaddr = phdrs[i].p_vaddr; - } - uint64_t end = phdrs[i].p_vaddr + phdrs[i].p_memsz; - if (end > max_vaddr) { - max_vaddr = end; - } - } - } - - sentry_free(phdr_buf); - close(fd); - - if (!found_load || max_vaddr <= min_vaddr) { - return 0; - } - - return max_vaddr - min_vaddr; -} - /** * Read the DT_SONAME from an ELF file's dynamic section. * Crashpad uses SONAME as the module name, falling back to the /proc/maps @@ -1047,7 +968,8 @@ compute_elf_size_from_phdrs(const char *elf_path) * Returns true if SONAME was found and written to soname_buf. */ static bool -read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) +read_elf_soname(const char *elf_path, uint64_t elf_offset, char *soname_buf, + size_t soname_buf_size) { int fd = sentry__elf_open(elf_path); if (fd < 0) { @@ -1066,7 +988,7 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) bool result = false; - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr) + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr) || memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) { close(fd); return false; @@ -1085,7 +1007,8 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) return false; } - if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff + uint64_t shoff = elf_offset + ehdr.e_shoff; + if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); @@ -1123,7 +1046,7 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) // Read .dynstr char *dynstr = sentry__elf_read_metadata_section( - fd, dynstr_shdr->sh_offset, dynstr_shdr->sh_size); + fd, elf_offset + dynstr_shdr->sh_offset, dynstr_shdr->sh_size); if (!dynstr) { sentry_free(shdr_buf); close(fd); @@ -1133,7 +1056,7 @@ read_elf_soname(const char *elf_path, char *soname_buf, size_t soname_buf_size) // Read .dynamic entries and find DT_SONAME void *dyn_buf = sentry__elf_read_metadata_section( - fd, dynamic_shdr->sh_offset, dynamic_shdr->sh_size); + fd, elf_offset + dynamic_shdr->sh_offset, dynamic_shdr->sh_size); if (!dyn_buf) { sentry_free(dynstr); sentry_free(shdr_buf); @@ -1540,6 +1463,7 @@ typedef struct { uint64_t end; // End of last mapping for this file char *name; // Pointer into mappings[].name (not owned) char soname[256]; // SONAME from ELF dynamic section (empty if not found) + uint64_t elf_offset; // File offset of the ELF image (non-zero in an APK) uint64_t elf_size; // Size from PT_LOAD segments (0 = use maps size) uint8_t build_id[32]; size_t build_id_len; @@ -1562,6 +1486,11 @@ typedef struct { * * 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. + * + * Android maps uncompressed libraries straight from the APK, so one file can + * hold several ELF images. Such an image starts at the mapping whose file + * offset holds the ELF header, and its other segments are grouped by the + * image's load range instead of by file name. */ static size_t resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, @@ -1596,15 +1525,21 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, break; } } - if (already_skipped) { + // A non-ELF file can still hold ELF images at non-zero offsets. + if (already_skipped && mapping->offset == 0) { continue; } // Find an existing module entry by name. This merges across // non-contiguous /proc/maps lines — fixes duplicate ld-linux entries. + // An ELF image inside a larger file only owns its load range. resolved_module_t *mod = NULL; for (size_t j = 0; j < module_count; j++) { - if (strcmp(modules[j].name, mapping->name) == 0) { + if (strcmp(modules[j].name, mapping->name) == 0 + && (modules[j].elf_offset == 0 + || (mapping->start >= modules[j].base + && mapping->start + < modules[j].base + modules[j].elf_size))) { mod = &modules[j]; break; } @@ -1627,10 +1562,18 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, continue; } + // An ELF image can also start inside a larger file, such as a library + // that Android maps straight from an APK. + bool embedded = mapping->offset != 0 + && sentry__elf_is_file_at(mapping->name, mapping->offset); + if (!embedded && already_skipped) { + 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 (!sentry__elf_is_file(mapping->name)) { + if (!embedded && !sentry__elf_is_file(mapping->name)) { if (skip_count < SENTRY_CRASH_MAX_MODULES) { skip_names[skip_count++] = mapping->name; } @@ -1647,15 +1590,18 @@ resolve_modules(const minidump_writer_t *writer, resolved_module_t *modules, mod->name = (char *)mapping->name; mod->base = mapping->start; mod->end = mapping->end; + mod->elf_offset = embedded ? mapping->offset : 0; + mod->elf_size + = sentry__elf_compute_size_from_phdrs(mod->name, mod->elf_offset); } - // Extract Build IDs, ELF sizes, and SONAMEs for each resolved module + // Extract Build IDs and SONAMEs for each resolved module for (size_t i = 0; i < module_count; i++) { - modules[i].build_id_len = extract_elf_build_id( - modules[i].name, modules[i].build_id, sizeof(modules[i].build_id)); - modules[i].elf_size = compute_elf_size_from_phdrs(modules[i].name); - if (!read_elf_soname(modules[i].name, modules[i].soname, - sizeof(modules[i].soname))) { + modules[i].build_id_len + = extract_elf_build_id(modules[i].name, modules[i].elf_offset, + modules[i].build_id, sizeof(modules[i].build_id)); + if (!read_elf_soname(modules[i].name, modules[i].elf_offset, + modules[i].soname, sizeof(modules[i].soname))) { modules[i].soname[0] = '\0'; } } diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 359df783fb..8a64cf6a36 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -155,6 +155,8 @@ typedef struct { uint32_t pdb_age; // PDB age (Windows PE only, appended to debug_id) #if defined(SENTRY_PLATFORM_WINDOWS) char pdb_name[SENTRY_CRASH_MAX_PATH]; // PDB filename (Windows PE only) +#elif defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + uint64_t elf_offset; // File offset of the ELF image (non-zero in an APK) #endif } sentry_module_info_t; diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 44a296875a..eddcbe81d4 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1272,12 +1272,13 @@ build_stacktrace_for_thread( # include /** - * Extract Build ID from ELF file (for debug_meta) + * Extract Build ID from the ELF image at `elf_offset` in a file + * (for debug_meta) * Returns the Build ID length, or 0 if not found */ static size_t -extract_elf_build_id_for_module( - const char *elf_path, uint8_t *build_id, size_t max_len) +extract_elf_build_id_for_module(const char *elf_path, uint64_t elf_offset, + uint8_t *build_id, size_t max_len) { int fd = sentry__elf_open(elf_path); if (fd < 0) { @@ -1291,7 +1292,7 @@ extract_elf_build_id_for_module( Elf32_Ehdr ehdr; # endif - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) { + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr)) { close(fd); return 0; } @@ -1345,8 +1346,8 @@ extract_elf_build_id_for_module( continue; } - if (lseek(fd, sections[i].sh_offset, SEEK_SET) - != (off_t)sections[i].sh_offset + uint64_t note_offset = elf_offset + sections[i].sh_offset; + if (lseek(fd, (off_t)note_offset, SEEK_SET) != (off_t)note_offset || read(fd, note_buf, note_size) != (ssize_t)note_size) { sentry_free(note_buf); continue; @@ -1442,7 +1443,8 @@ elf_locate_symtab(const char *path, int allow_dynsym, sym_source_t *src, return 0; } - if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff + uint64_t shoff = elf_offset + ehdr.e_shoff; + if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); @@ -1571,7 +1573,7 @@ get_sym_source(const sentry_module_info_t *mod, uint32_t mod_idx) if (debuglink[0]) { uint8_t mod_bid[16]; size_t mod_bid_len = extract_elf_build_id_for_module( - mod->name, mod_bid, sizeof(mod_bid)); + mod->name, mod->elf_offset, mod_bid, sizeof(mod_bid)); const char *slash = strrchr(mod->name, '/'); int dir_len = slash ? (int)(slash - mod->name) : 0; @@ -1605,7 +1607,7 @@ get_sym_source(const sentry_module_info_t *mod, uint32_t mod_idx) uint8_t dbg_bid[16]; size_t dbg_bid_len = extract_elf_build_id_for_module( - candidate, dbg_bid, sizeof(dbg_bid)); + candidate, 0, dbg_bid, sizeof(dbg_bid)); if (mod_bid_len && dbg_bid_len && (mod_bid_len != dbg_bid_len || memcmp(mod_bid, dbg_bid, mod_bid_len) != 0)) { @@ -1826,10 +1828,20 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) // offset 0) sentry_module_info_t *existing_mod = NULL; for (uint32_t j = ctx->module_count; j > 0; j--) { - if (strncmp(ctx->modules[j - 1].name, pathname, len) == 0 - && ctx->modules[j - 1].name[len] == '\0') { + sentry_module_info_t *other = &ctx->modules[j - 1]; + if (strncmp(other->name, pathname, len) == 0 + && other->name[len] == '\0') { + // An ELF image inside a larger file only owns its load range + if (other->elf_offset != 0) { + if (start >= other->base_address + && start < other->base_address + other->size) { + existing_mod = other; + break; + } + continue; + } if (offset != 0 || j == ctx->module_count) { - existing_mod = &ctx->modules[j - 1]; + existing_mod = other; } break; } @@ -1863,7 +1875,17 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) memcpy(mod->name, pathname, copy_len); mod->name[copy_len] = '\0'; - if (!sentry__elf_is_file(mod->name)) { + // An ELF image can also start inside a larger file, such as a library + // that Android maps straight from an APK. Its load size is known up + // front to group the image's other segments. + mod->elf_offset = 0; + if (offset != 0 && sentry__elf_is_file_at(mod->name, offset)) { + uint64_t elf_size + = sentry__elf_compute_size_from_phdrs(mod->name, offset); + mod->elf_offset = offset; + mod->base_address = start; + mod->size = MAX(end - start, elf_size); + } else if (!sentry__elf_is_file(mod->name)) { continue; } @@ -1871,7 +1893,7 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) memset(mod->uuid, 0, sizeof(mod->uuid)); mod->pdb_age = 0; // Not used on Linux, only for Windows PE modules extract_elf_build_id_for_module( - mod->name, mod->uuid, sizeof(mod->uuid)); + mod->name, mod->elf_offset, mod->uuid, sizeof(mod->uuid)); // Convert to little-endian GUID format for Sentry debug_id sentry__uuid_swap_guid_bytes(mod->uuid); diff --git a/src/sentry_elf.h b/src/sentry_elf.h index bb80d3f019..9c161bce0d 100644 --- a/src/sentry_elf.h +++ b/src/sentry_elf.h @@ -37,9 +37,11 @@ sentry__elf_open(const char *path) * 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. + * A non-zero offset checks for an ELF image inside a larger file, such as a + * library that Android maps straight from an APK. */ static inline bool -sentry__elf_is_file(const char *path) +sentry__elf_is_file_at(const char *path, uint64_t offset) { int fd = sentry__elf_open(path); if (fd < 0) { @@ -47,12 +49,18 @@ sentry__elf_is_file(const char *path) } unsigned char magic[SELFMAG]; - bool is_elf = read(fd, magic, SELFMAG) == (ssize_t)SELFMAG + bool is_elf = pread(fd, magic, SELFMAG, (off_t)offset) == (ssize_t)SELFMAG && memcmp(magic, ELFMAG, SELFMAG) == 0; close(fd); return is_elf; } +static inline bool +sentry__elf_is_file(const char *path) +{ + return sentry__elf_is_file_at(path, 0); +} + static inline bool sentry__elf_is_native_class(const unsigned char e_ident[EI_NIDENT]) { @@ -203,6 +211,88 @@ sentry__elf_find_note(const void *buf, size_t buf_size, size_t alignment, return NULL; } +/** + * Compute the virtual memory size of an ELF file from its PT_LOAD segments. + * This matches Crashpad's behavior: size = max(p_vaddr + p_memsz) - + * min(p_vaddr) across all PT_LOAD segments, rather than using page-aligned + * /proc/maps sizes. + * Returns the computed size, or 0 on failure (caller should fall back to + * /proc/maps size). + */ +static inline uint64_t +sentry__elf_compute_size_from_phdrs(const char *elf_path, uint64_t elf_offset) +{ + int fd = sentry__elf_open(elf_path); + if (fd < 0) { + return 0; + } + +# if defined(__x86_64__) || defined(__aarch64__) + Elf64_Ehdr ehdr; +# else + Elf32_Ehdr ehdr; +# endif + + if (pread(fd, &ehdr, sizeof(ehdr), (off_t)elf_offset) != sizeof(ehdr) + || memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 || ehdr.e_phnum == 0) { + close(fd); + return 0; + } + + if (!sentry__elf_has_phdr_size(ehdr.e_ident, ehdr.e_phentsize)) { + close(fd); + return 0; + } + + // Read program headers + size_t phdr_size = (size_t)ehdr.e_phentsize * ehdr.e_phnum; + void *phdr_buf = sentry_malloc(phdr_size); + if (!phdr_buf) { + close(fd); + return 0; + } + + uint64_t phoff = elf_offset + ehdr.e_phoff; + if (lseek(fd, (off_t)phoff, SEEK_SET) != (off_t)phoff + || read(fd, phdr_buf, phdr_size) != (ssize_t)phdr_size) { + sentry_free(phdr_buf); + close(fd); + return 0; + } + +# if defined(__x86_64__) || defined(__aarch64__) + Elf64_Phdr *phdrs = (Elf64_Phdr *)phdr_buf; +# else + Elf32_Phdr *phdrs = (Elf32_Phdr *)phdr_buf; +# endif + + uint64_t min_vaddr = UINT64_MAX; + uint64_t max_vaddr = 0; + bool found_load = false; + + for (int i = 0; i < ehdr.e_phnum; i++) { + if (phdrs[i].p_type == PT_LOAD) { + found_load = true; + if (phdrs[i].p_vaddr < min_vaddr) { + min_vaddr = phdrs[i].p_vaddr; + } + uint64_t end = phdrs[i].p_vaddr + phdrs[i].p_memsz; + if (end > max_vaddr) { + max_vaddr = end; + } + } + } + + sentry_free(phdr_buf); + close(fd); + + if (!found_load || max_vaddr <= min_vaddr) { + return 0; + } + + return max_vaddr - min_vaddr; +} + #endif #endif diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index ff7bcaf171..d95f8da1c2 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -241,6 +241,27 @@ SENTRY_TEST(elf_rejects_non_regular_files) #endif } +SENTRY_TEST(elf_offset) +{ +#if !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + SKIP_TEST(); +#else + // an APK-like zip with an uncompressed library at a page-aligned offset + char path[] = "/tmp/sentry-minidump-apk-XXXXXX"; + int fd = mkstemp(path); + TEST_ASSERT(fd >= 0); + TEST_ASSERT(write(fd, "PK\3\4", 4) == 4); + TEST_ASSERT(pwrite(fd, ELFMAG, SELFMAG, 4096) == (ssize_t)SELFMAG); + close(fd); + + TEST_CHECK(!sentry__elf_is_file(path)); + TEST_CHECK(sentry__elf_is_file_at(path, 4096)); + TEST_CHECK(!sentry__elf_is_file_at(path, 8192)); + + TEST_CHECK(unlink(path) == 0); +#endif +} + /** * Test minidump directory entry structure */ diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 33b2c78ce2..db9c2d38e2 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -206,6 +206,7 @@ XX(effective_org_id_resolution) XX(elf_find_note) XX(elf_header_entry_sizes) XX(elf_metadata_section) +XX(elf_offset) XX(elf_rejects_non_regular_files) XX(embedded_info_basic) XX(embedded_info_build_id) From 1bfff50f780a216e487442175f526e59dc84c570 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 28 Oct 2026 15:16:34 +0100 Subject: [PATCH 2/5] fix(native): Apply the ELF offset to the build ID section headers The previous commit adjusted the section header offset in `elf_locate_symtab()` instead of `extract_elf_build_id_for_module()`, where `elf_offset` is not declared, so the crash daemon failed to compile. Move the adjustment to where the build ID is read. --- src/backends/native/sentry_crash_daemon.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index eddcbe81d4..946fbc4b92 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1316,7 +1316,8 @@ extract_elf_build_id_for_module(const char *elf_path, uint64_t elf_offset, return 0; } - if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff + uint64_t shoff = elf_offset + ehdr.e_shoff; + if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); @@ -1443,8 +1444,7 @@ elf_locate_symtab(const char *path, int allow_dynsym, sym_source_t *src, return 0; } - uint64_t shoff = elf_offset + ehdr.e_shoff; - if (lseek(fd, (off_t)shoff, SEEK_SET) != (off_t)shoff + if (lseek(fd, ehdr.e_shoff, SEEK_SET) != (off_t)ehdr.e_shoff || read(fd, shdr_buf, shdr_size) != (ssize_t)shdr_size) { sentry_free(shdr_buf); close(fd); From 6388a2f0e906091eeba4e81d9d0cc3e42fcf773e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 28 Oct 2026 15:19:08 +0100 Subject: [PATCH 3/5] fix(native): Keep embedded ELFs mapped below their file offset The `offset > start` guard in `capture_modules_from_proc_maps()` dropped mappings before an ELF image inside a larger file could be detected. The guard only protects the `start - offset` load base of a regular ELF file, so apply it there instead. --- src/backends/native/sentry_crash_daemon.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 946fbc4b92..2888664686 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1818,10 +1818,6 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) continue; } - if (offset > start) { - continue; - } - // Check if this file is already captured - if so, extend size if needed // A mapping at offset 0 starts another load of the file, unless the // file was captured last (small libraries can map all segments at @@ -1885,7 +1881,7 @@ capture_modules_from_proc_maps(sentry_crash_context_t *ctx) mod->elf_offset = offset; mod->base_address = start; mod->size = MAX(end - start, elf_size); - } else if (!sentry__elf_is_file(mod->name)) { + } else if (offset > start || !sentry__elf_is_file(mod->name)) { continue; } From 9e8b565a074f3d4dfdad9ac9f2ff3d98ae5e03eb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 28 Oct 2026 15:22:25 +0100 Subject: [PATCH 4/5] feat(unwinder): Unwind remote threads with libunwindstack The crash daemon could only unwind the threads that did not crash with `libunwind_remote`, which Android builds lack because they use libunwindstack. Add a `libunwindstack_remote` backend, compiled in only with the native backend, that attaches to a thread with ptrace, reads its registers with `Regs::RemoteGet()`, and unwinds it. Share the ptrace attach loop with `libunwind_remote`. This prepares the native backend for Android. --- CMakeLists.txt | 5 + src/CMakeLists.txt | 6 + src/backends/native/sentry_crash_daemon.c | 14 +- src/unwinder/sentry_unwinder.c | 66 ++++++++++ src/unwinder/sentry_unwinder.h | 2 + .../sentry_unwinder_libunwind_remote.c | 39 +----- .../sentry_unwinder_libunwindstack_remote.cpp | 120 ++++++++++++++++++ 7 files changed, 211 insertions(+), 41 deletions(-) create mode 100644 src/unwinder/sentry_unwinder_libunwindstack_remote.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d967bfddd..b6d1e85715 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -978,6 +978,11 @@ elseif(SENTRY_BACKEND_NATIVE) ${PROJECT_SOURCE_DIR}/src ${PROJECT_SOURCE_DIR}/src/backends/native ) + if(SENTRY_WITH_LIBUNWINDSTACK) + target_include_directories(sentry-crash PRIVATE + ${PROJECT_SOURCE_DIR}/external/libunwindstack-ndk/include + ) + endif() # Link same libraries as sentry target_link_libraries(sentry-crash PRIVATE ${_SENTRY_PLATFORM_LIBS}) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 11ee4f4c9b..2aa4739b79 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -178,6 +178,12 @@ elseif(SENTRY_BACKEND_NATIVE) backends/native/sentry_crash_ipc.c backends/native/sentry_crash_handler.c ) + if(SENTRY_WITH_LIBUNWINDSTACK) + target_compile_definitions(sentry PRIVATE SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) + sentry_target_sources_cwd(sentry + unwinder/sentry_unwinder_libunwindstack_remote.cpp + ) + endif() # Add include directory for native backend headers target_include_directories(sentry PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/backends/native) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 2888664686..118b8fa9a7 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -536,7 +536,8 @@ build_registers_from_ctx(const sentry_crash_context_t *ctx, size_t thread_idx) return sentry__build_registers(&uctx); } -#ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE +#if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) static sentry_value_t build_registers_from_remote_registers( const sentry_remote_registers_t *remote_registers) @@ -1002,10 +1003,11 @@ build_stacktrace_for_thread( int frame_count = 0; #if defined(SENTRY_PLATFORM_LINUX) - // Remote DWARF unwinding via libunwind ptrace accessors. Do not attach to + // Remote DWARF unwinding via ptrace. Do not attach to // the crashed thread from the daemon; use the saved fault context below. { -# ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) pid_t tid = 0; if (thread_idx == SIZE_MAX) { tid = ctx->crashed_tid; @@ -1075,6 +1077,12 @@ build_stacktrace_for_thread( sentry_value_set_by_key(stacktrace, "registers", build_registers_from_remote_registers(®isters)); } +# ifdef SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE + // libunwindstack already adjusts the caller frames + sentry_value_set_by_key(stacktrace, + "instruction_addr_adjustment", + sentry_value_new_string("none")); +# endif sentry_free(remote_frames); return stacktrace; } diff --git a/src/unwinder/sentry_unwinder.c b/src/unwinder/sentry_unwinder.c index 68737b5cef..24c160456b 100644 --- a/src/unwinder/sentry_unwinder.c +++ b/src/unwinder/sentry_unwinder.c @@ -1,6 +1,17 @@ #include "sentry_unwinder.h" #include "sentry_boot.h" +#if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) +# include "sentry_logger.h" + +# include +# include +# include +# include +# include +#endif + #define DEFINE_UNWINDER(Func) \ size_t sentry__unwind_stack_##Func(void *addr, \ const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames) @@ -36,6 +47,7 @@ DEFINE_UNWINDER(psunwind); } while (0) DEFINE_THREAD_UNWINDER(libunwind_remote); +DEFINE_THREAD_UNWINDER(libunwindstack_remote); #endif static size_t @@ -88,7 +100,61 @@ sentry__unwind_stack_from_thread(pid_t tid, sentry_remote_frame_t *frames, (void)symbolize; # ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE TRY_THREAD_UNWINDER(libunwind_remote); +# endif +# ifdef SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE + TRY_THREAD_UNWINDER(libunwindstack_remote); # endif return 0; } + +# if defined(SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE) \ + || defined(SENTRY_WITH_UNWINDER_LIBUNWINDSTACK_REMOTE) +/** + * Attach to a thread of another process and wait until it stops. + * Returns false, leaving the thread detached, if either fails. + */ +bool +sentry__ptrace_attach_thread(pid_t tid) +{ + if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) != 0) { + SENTRY_WARNF("remote_unwind: ptrace attach failed for %d: %s", tid, + strerror(errno)); + return false; + } + + // Poll with WNOHANG to avoid hanging on threads in D state. PTRACE_ATTACH + // sends SIGSTOP but the thread won't stop until it leaves uninterruptible + // sleep, which may never happen. + int status = 0; + int retries = 20; + for (;;) { + pid_t waited = waitpid(tid, &status, __WALL | WNOHANG); + if (waited == tid) { + break; + } + if (waited < 0) { + SENTRY_WARNF("remote_unwind: waitpid failed for %d: %s", tid, + strerror(errno)); + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return false; + } + if (--retries <= 0) { + SENTRY_WARNF("remote_unwind: waitpid timed out for %d", tid); + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return false; + } + usleep(100000); + } + + if (!WIFSTOPPED(status)) { + SENTRY_WARNF( + "remote_unwind: thread %d did not stop after attach: status=%d", + tid, status); + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return false; + } + + return true; +} +# endif #endif diff --git a/src/unwinder/sentry_unwinder.h b/src/unwinder/sentry_unwinder.h index 75add8d77f..32548f0aca 100644 --- a/src/unwinder/sentry_unwinder.h +++ b/src/unwinder/sentry_unwinder.h @@ -38,6 +38,8 @@ size_t sentry__unwind_stack_from_thread(pid_t tid, sentry_remote_frame_t *frames, size_t max_frames, sentry_remote_registers_t *registers, bool symbolize); +bool sentry__ptrace_attach_thread(pid_t tid); + #endif #endif // SENTRY_UNWINDER_H_INCLUDED diff --git a/src/unwinder/sentry_unwinder_libunwind_remote.c b/src/unwinder/sentry_unwinder_libunwind_remote.c index ca99aa7d53..1c1743841e 100644 --- a/src/unwinder/sentry_unwinder_libunwind_remote.c +++ b/src/unwinder/sentry_unwinder_libunwind_remote.c @@ -15,11 +15,8 @@ # include "sentry_logger.h" -# include # include # include -# include -# include # include # include @@ -78,41 +75,7 @@ sentry__unwind_stack_from_thread_libunwind_remote(pid_t tid, memset(registers, 0, sizeof(*registers)); } - if (ptrace(PTRACE_ATTACH, tid, NULL, NULL) != 0) { - SENTRY_WARNF("remote_unwind: ptrace attach failed for %d: %s", tid, - strerror(errno)); - return 0; - } - - // Poll with WNOHANG to avoid hanging on threads in D state. PTRACE_ATTACH - // sends SIGSTOP but the thread won't stop until it leaves uninterruptible - // sleep, which may never happen. - int status = 0; - int retries = 20; - for (;;) { - pid_t waited = waitpid(tid, &status, __WALL | WNOHANG); - if (waited == tid) { - break; - } - if (waited < 0) { - SENTRY_WARNF("remote_unwind: waitpid failed for %d: %s", tid, - strerror(errno)); - ptrace(PTRACE_DETACH, tid, NULL, NULL); - return 0; - } - if (--retries <= 0) { - SENTRY_WARNF("remote_unwind: waitpid timed out for %d", tid); - ptrace(PTRACE_DETACH, tid, NULL, NULL); - return 0; - } - usleep(100000); - } - - if (!WIFSTOPPED(status)) { - SENTRY_WARNF( - "remote_unwind: thread %d did not stop after attach: status=%d", - tid, status); - ptrace(PTRACE_DETACH, tid, NULL, NULL); + if (!sentry__ptrace_attach_thread(tid)) { return 0; } diff --git a/src/unwinder/sentry_unwinder_libunwindstack_remote.cpp b/src/unwinder/sentry_unwinder_libunwindstack_remote.cpp new file mode 100644 index 0000000000..93a09d1a9e --- /dev/null +++ b/src/unwinder/sentry_unwinder_libunwindstack_remote.cpp @@ -0,0 +1,120 @@ +extern "C" { +#include "sentry_boot.h" +#include "sentry_logger.h" +#include "sentry_unwinder.h" +} + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static void +capture_registers(unwindstack::Regs *regs, sentry_remote_registers_t *registers) +{ + regs->IterateRegisters([registers](const char *name, uint64_t value) { + size_t len = strlen(name); + if (registers->count >= SENTRY_REMOTE_UNWIND_MAX_REGISTERS || len == 0 + || len >= sizeof(registers->values[0].name)) { + return; + } + sentry_remote_register_t *remote_register + = ®isters->values[registers->count++]; + memcpy(remote_register->name, name, len + 1); + remote_register->value = value; + }); +} + +static size_t +unwind_thread(pid_t tid, sentry_remote_frame_t *frames, size_t max_frames, + sentry_remote_registers_t *registers, bool symbolize) +{ + // an uncaught exception, e.g. std::bad_alloc, would abort the daemon + try { + std::unique_ptr regs( + unwindstack::Regs::RemoteGet(tid)); + if (!regs) { + SENTRY_WARNF("libunwindstack failed to read registers of %d", tid); + return 0; + } + if (registers) { + capture_registers(regs.get(), registers); + } + + unwindstack::RemoteMaps maps(tid); + if (!maps.Parse()) { + SENTRY_WARNF("libunwindstack failed to parse /proc/%d/maps", tid); + return 0; + } + + std::shared_ptr process_memory + = unwindstack::Memory::CreateProcessMemoryCached(tid); + if (!process_memory) { + SENTRY_WARNF( + "libunwindstack failed to read process memory for %d", tid); + return 0; + } + + unwindstack::Unwinder unwinder( + max_frames, &maps, regs.get(), process_memory); + unwinder.SetResolveNames(symbolize); + unwinder.Unwind(); + + if (unwinder.LastErrorCode() != unwindstack::ERROR_NONE) { + SENTRY_DEBUGF("libunwindstack finished with %s at 0x%llx", + unwinder.LastErrorCodeString(), + (unsigned long long)unwinder.LastErrorAddress()); + } + + size_t count = 0; + for (const unwindstack::FrameData &frame : unwinder.frames()) { + if (count >= max_frames) { + break; + } + frames[count].ip = frame.pc; + frames[count].symbol[0] = '\0'; + frames[count].symbol_offset = 0; + if (!frame.function_name.empty()) { + snprintf(frames[count].symbol, sizeof(frames[count].symbol), + "%s", frame.function_name.c_str()); + frames[count].symbol_offset = frame.function_offset; + } + count++; + } + + SENTRY_DEBUGF( + "libunwindstack remote unwind captured %zu frames", count); + return count; + } catch (...) { + SENTRY_WARN("libunwindstack failed with an exception"); + return 0; + } +} + +extern "C" size_t +sentry__unwind_stack_from_thread_libunwindstack_remote(pid_t tid, + sentry_remote_frame_t *frames, size_t max_frames, + sentry_remote_registers_t *registers, bool symbolize) +{ + if (registers) { + registers->count = 0; + } + + if (tid <= 0 || !frames || max_frames == 0) { + return 0; + } + + if (!sentry__ptrace_attach_thread(tid)) { + return 0; + } + + size_t count = unwind_thread(tid, frames, max_frames, registers, symbolize); + + ptrace(PTRACE_DETACH, tid, NULL, NULL); + return count; +} From bb0341b65ff3d6c1b20ad9fd913ad084702c329b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 28 Oct 2026 18:07:34 +0100 Subject: [PATCH 5/5] feat(native): Support preloading the crash handler `sentry__backend_preload()` was a no-op for the native backend. Install its signal handlers early, so that they keep their place in the signal chain ahead of a managed runtime, and let a later `sentry_init()` adopt them. Like with `inproc`, shutdown keeps the preloaded handlers installed, forwarding crashes to the previous handlers until the next initialization. --- src/backends/native/sentry_crash_handler.c | 48 ++++++++++++++++++++-- src/backends/native/sentry_crash_handler.h | 5 +++ src/backends/sentry_backend_native.c | 5 +++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index db8ce22919..f9a83e60c9 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -125,6 +125,8 @@ static sentry_handler_strategy_t g_handler_strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; static struct sigaction g_previous_handlers[16]; static stack_t g_signal_stack = { 0 }; +static volatile long g_handlers_installed = 0; +static volatile long g_preloaded = 0; static void reset_signal_handlers(void) @@ -973,14 +975,22 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) reraise_signal(signum); } -int -sentry__crash_handler_init(sentry_crash_ipc_t *ipc, +static int +install_signal_handlers(sentry_crash_ipc_t *ipc, bool preload, sentry_handler_strategy_t strategy, void (*callback)(void)) { - if (!ipc && !callback) { + if (!preload && !ipc && !callback) { return -1; } + if (sentry__atomic_fetch(&g_handlers_installed)) { + if (!preload) { + g_crash_ipc = ipc; + g_handler_strategy = strategy; + } + return 0; + } + g_crash_ipc = ipc; g_handler_strategy = strategy; g_crash_callback = callback; @@ -1019,6 +1029,12 @@ sentry__crash_handler_init(sentry_crash_ipc_t *ipc, installed_count++; } + g_crash_ipc = ipc; + sentry__atomic_store(&g_handlers_installed, 1); + if (preload) { + sentry__atomic_store(&g_preloaded, 1); + } + // Prime libunwind's internal cache by performing a short unwind. // This forces dl_iterate_phdr to be called now (while safe) rather than // in the signal handler where it could deadlock if the crash occurs @@ -1052,6 +1068,20 @@ sentry__crash_handler_init(sentry_crash_ipc_t *ipc, return 0; } +int +sentry__crash_handler_init(sentry_crash_ipc_t *ipc, + sentry_handler_strategy_t strategy, void (*callback)(void)) +{ + return install_signal_handlers(ipc, false, strategy, callback); +} + +int +sentry__crash_handler_preload(void) +{ + return install_signal_handlers( + NULL, true, SENTRY_HANDLER_STRATEGY_DEFAULT, NULL); +} + int sentry__crash_handler_reinstall(void) { @@ -1087,6 +1117,16 @@ sentry__crash_handler_reinstall(void) void sentry__crash_handler_shutdown(void) { + if (sentry__atomic_fetch(&g_preloaded)) { + g_crash_ipc = NULL; + SENTRY_DEBUG("crash handler deactivated, keeping preloaded handlers"); + return; + } + + if (!sentry__atomic_fetch(&g_handlers_installed)) { + return; + } + // Restore previous signal handlers reset_signal_handlers(); @@ -1095,6 +1135,8 @@ sentry__crash_handler_shutdown(void) g_crash_ipc = NULL; g_crash_callback = NULL; + sentry__atomic_store(&g_handlers_installed, 0); + sentry__atomic_store(&g_preloaded, 0); SENTRY_DEBUG("crash handler shutdown"); } diff --git a/src/backends/native/sentry_crash_handler.h b/src/backends/native/sentry_crash_handler.h index a2c12054bc..cf409f9969 100644 --- a/src/backends/native/sentry_crash_handler.h +++ b/src/backends/native/sentry_crash_handler.h @@ -15,6 +15,11 @@ int sentry__crash_handler_init(sentry_crash_ipc_t *ipc, */ int sentry__crash_handler_reinstall(void); +/** + * Preload crash handler before full initialization. + */ +int sentry__crash_handler_preload(void); + /** * Shutdown crash handler (restore previous handlers) */ diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 3a486e1176..b595ab34d9 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -1662,6 +1662,11 @@ native_backend_reinstall(sentry_backend_t *backend) void sentry__backend_preload(void) { +#ifdef SENTRY_PLATFORM_UNIX + if (sentry__crash_handler_preload() < 0) { + SENTRY_WARN("failed to preload native crash handler"); + } +#endif } /**