diff --git a/src/backends/native/minidump/sentry_minidump_linux.c b/src/backends/native/minidump/sentry_minidump_linux.c index 39a2d5be9..25c314ea1 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 359df783f..8a64cf6a3 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 44a296875..288866468 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; } @@ -1315,7 +1316,8 @@ extract_elf_build_id_for_module( 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); @@ -1345,8 +1347,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; @@ -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)) { @@ -1816,20 +1818,26 @@ 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 // 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 +1871,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 (offset > start || !sentry__elf_is_file(mod->name)) { continue; } @@ -1871,7 +1889,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 bb80d3f01..9c161bce0 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 ff7bcaf17..d95f8da1c 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 33b2c78ce..db9c2d38e 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)