diff --git a/CHANGELOG.md b/CHANGELOG.md index a21fb77ea..1b555b6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ **Fixes**: - Native/Windows: ensure valid event IDs for fast-fail crash envelopes to fix launching of the external crash reporter for fast-fail crashes. ([#1832](https://github.com/getsentry/sentry-native/pull/1832)) +- Native/Linux: resolve symbols from split-debug files on Linux. ([#1836](https://github.com/getsentry/sentry-native/pull/1836)) ## 0.15.2 diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 95a3638ed..4252f2cb3 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1308,159 +1308,381 @@ extract_elf_build_id_for_module( } /** - * Resolve the symbol name for a given instruction address from an ELF symbol - * table and set the "function" key on the frame value. + * A symbol-table source for a module: either the module's own ELF file or + * its split-debug companion located via the `.gnu_debuglink` section. + * Cached per module so the section-header parse and the debug-file search + * run once per module rather than once per frame. */ -static void -enrich_frame_with_symbol( - const sentry_crash_context_t *ctx, sentry_value_t frame, uint64_t addr) -{ - uint32_t module_count = MIN(ctx->module_count, SENTRY_CRASH_MAX_MODULES); - for (uint32_t i = 0; i < module_count; i++) { - const sentry_module_info_t *mod = &ctx->modules[i]; - if (addr < mod->base_address || addr >= mod->base_address + mod->size) { - continue; - } +typedef struct { + char sym_path[SENTRY_CRASH_MAX_PATH]; + int state; // 0 = unused, 1 = usable, -1 = no symbol table found + uint16_t e_type; // e_type of the loaded module (st_value semantics) + uint64_t symtab_off; + uint64_t symtab_size; + uint64_t strtab_off; + uint64_t strtab_size; +} sym_source_t; + +// Upper bound on a resolved symbol name; mirrors dbghelp's MAX_SYM_NAME. +# define SENTRY_MAX_SYM_NAME 2000 + +// Direct-mapped by module index; see get_sym_source. Being BSS, only the +// slots touched during resolution are committed to physical memory. +static sym_source_t g_sym_sources[SENTRY_CRASH_MAX_MODULES]; - uint64_t offset = addr - mod->base_address; +/** + * Parse the ELF file at `path` and record the location of its symbol table + * in `src`. Prefers the full `.symtab` and only falls back to `.dynsym` + * when `allow_dynsym` is set: the dynamic table covers exported symbols + * only, so lookups against it are unreliable for binaries whose `.symtab` + * was stripped. When `debuglink_out` is non-NULL, also extracts the file + * name stored in a `.gnu_debuglink` section, if present. + * Returns 1 when a usable symbol table was found, 0 otherwise. + */ +static int +elf_locate_symtab(const char *path, int allow_dynsym, sym_source_t *src, + char *debuglink_out, size_t debuglink_out_size, uint16_t *e_type_out) +{ + if (debuglink_out && debuglink_out_size > 0) { + debuglink_out[0] = '\0'; + } - int fd = open(mod->name, O_RDONLY); - if (fd < 0) { - return; - } + int fd = open(path, O_RDONLY); + if (fd < 0) { + return 0; + } # if defined(__x86_64__) || defined(__aarch64__) - Elf64_Ehdr ehdr; + Elf64_Ehdr ehdr; # else - Elf32_Ehdr ehdr; + Elf32_Ehdr ehdr; # endif - if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) { - close(fd); - return; - } + if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr) + || memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 + || !sentry__elf_is_native_class(ehdr.e_ident) + || !sentry__elf_has_shdr_size(ehdr.e_ident, ehdr.e_shentsize)) { + close(fd); + return 0; + } - if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0 - || !sentry__elf_is_native_class(ehdr.e_ident) - || !sentry__elf_has_shdr_size(ehdr.e_ident, ehdr.e_shentsize)) { - close(fd); - return; - } + if (e_type_out) { + *e_type_out = ehdr.e_type; + } - size_t shdr_size = (size_t)ehdr.e_shentsize * ehdr.e_shnum; - void *shdr_buf = sentry_malloc(shdr_size); - if (!shdr_buf) { - close(fd); - return; - } + size_t shdr_size = (size_t)ehdr.e_shentsize * ehdr.e_shnum; + void *shdr_buf = sentry_malloc(shdr_size); + if (!shdr_buf) { + close(fd); + return 0; + } - 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); - return; - } + 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); + return 0; + } # if defined(__x86_64__) || defined(__aarch64__) - Elf64_Shdr *sections = (Elf64_Shdr *)shdr_buf; + Elf64_Shdr *sections = (Elf64_Shdr *)shdr_buf; # else - Elf32_Shdr *sections = (Elf32_Shdr *)shdr_buf; + Elf32_Shdr *sections = (Elf32_Shdr *)shdr_buf; # endif - // Find symbol table section - prefer .dynsym (always present in .so), - // fall back to .symtab - int symtab_idx = -1; - for (int j = 0; j < ehdr.e_shnum; j++) { - if (sections[j].sh_type == SHT_DYNSYM) { - symtab_idx = j; - break; - } + int symtab_idx = -1; + int dynsym_idx = -1; + for (int j = 0; j < ehdr.e_shnum; j++) { + if (sections[j].sh_type == SHT_SYMTAB && symtab_idx < 0) { + symtab_idx = j; + } else if (sections[j].sh_type == SHT_DYNSYM && dynsym_idx < 0) { + dynsym_idx = j; } - if (symtab_idx < 0) { - for (int j = 0; j < ehdr.e_shnum; j++) { - if (sections[j].sh_type == SHT_SYMTAB) { - symtab_idx = j; + } + + // Extract the .gnu_debuglink file name, identified via the section name + // string table (the section has no dedicated sh_type). + if (debuglink_out && ehdr.e_shstrndx < ehdr.e_shnum) { + size_t shstr_size = sections[ehdr.e_shstrndx].sh_size; + if (shstr_size > 0 && shstr_size <= 1024 * 1024) { + char *shstr_buf = sentry_malloc(shstr_size); + if (shstr_buf + && lseek(fd, sections[ehdr.e_shstrndx].sh_offset, SEEK_SET) + == (off_t)sections[ehdr.e_shstrndx].sh_offset + && read(fd, shstr_buf, shstr_size) == (ssize_t)shstr_size) { + for (int j = 0; j < ehdr.e_shnum; j++) { + size_t name_off = sections[j].sh_name; + if (name_off >= shstr_size + || strnlen(shstr_buf + name_off, shstr_size - name_off) + == shstr_size - name_off + || strcmp(shstr_buf + name_off, ".gnu_debuglink") + != 0) { + continue; + } + // Content: NUL-terminated file name, padding, CRC32. + size_t link_size = sections[j].sh_size; + if (link_size < 5 || link_size > 4096 + 4) { + break; + } + char link_buf[4096 + 4]; + if (lseek(fd, sections[j].sh_offset, SEEK_SET) + == (off_t)sections[j].sh_offset + && read(fd, link_buf, link_size) + == (ssize_t)link_size) { + size_t name_max = link_size - 4; + size_t name_len = strnlen(link_buf, name_max); + if (name_len > 0 && name_len < name_max + && name_len < debuglink_out_size) { + memcpy(debuglink_out, link_buf, name_len + 1); + } + } break; } } + sentry_free(shstr_buf); } + } - if (symtab_idx >= 0 - && sentry__elf_has_sym_entsize( - ehdr.e_ident, sections[symtab_idx].sh_entsize)) { - // For ET_DYN (shared libs, PIE) st_value is base-relative; for - // ET_EXEC (non-PIE) st_value is an absolute virtual address. - uint64_t sym_target = ehdr.e_type == ET_EXEC ? addr : offset; - size_t sym_size = sections[symtab_idx].sh_size; - size_t sym_count = sym_size / sections[symtab_idx].sh_entsize; - int strtab_idx = sections[symtab_idx].sh_link; - if (strtab_idx < 0 || (size_t)strtab_idx >= ehdr.e_shnum) { - sentry_free(shdr_buf); - close(fd); - return; + int chosen + = symtab_idx >= 0 ? symtab_idx : (allow_dynsym ? dynsym_idx : -1); + int found = 0; + if (chosen >= 0 + && sentry__elf_has_sym_entsize( + ehdr.e_ident, sections[chosen].sh_entsize)) { + uint32_t strtab_idx = sections[chosen].sh_link; + if (strtab_idx > 0 && strtab_idx < ehdr.e_shnum + && sections[strtab_idx].sh_size > 0 + && (size_t)snprintf( + src->sym_path, sizeof(src->sym_path), "%s", path) + < sizeof(src->sym_path)) { + src->symtab_off = sections[chosen].sh_offset; + src->symtab_size = sections[chosen].sh_size; + src->strtab_off = sections[strtab_idx].sh_offset; + src->strtab_size = sections[strtab_idx].sh_size; + found = 1; + } + } + + sentry_free(shdr_buf); + close(fd); + return found; +} + +/** + * Resolve the symbol-table source for a module. If the module's own ELF was + * stripped of its `.symtab`, follow the GDB split-debug conventions to find + * the debug companion (`