From e642cf9a0740757ea5ac4c5506153f98d6fd8448 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 28 May 2026 10:32:35 +0200 Subject: [PATCH 1/6] fix(native): resolve symbol names for crashed thread on Linux MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve function names from on-disk ELF symbol tables (.dynsym/.symtab) for the crashed thread's stacktrace in the crash daemon. The daemon already had module paths and base addresses from ctx->modules[], so symbol resolution is a straightforward ELF section header parse + symbol table lookup — no ptrace needed. Previously, the pre-captured backtrace (DWARF-based, signal-frame-aware) from the signal handler was preferred over remote unwinding for the crashed thread, but only set instruction_addr and module package — never function names. The FP-walking and single-IP fallback paths had the same gap. --- src/backends/native/sentry_crash_daemon.c | 158 ++++++++++++++++++++++ tests/test_integration_native.py | 4 +- 2 files changed, 160 insertions(+), 2 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index fec72acb1..19799c60d 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -623,6 +623,11 @@ enrich_frame_with_module_info( (unsigned long long)addr, ctx->module_count); } +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) +static void enrich_frame_with_symbol( + const sentry_crash_context_t *ctx, sentry_value_t frame, uint64_t addr); +#endif + /** * Build stacktrace frames for a specific thread using frame pointer-based * unwinding. Reads the captured stack memory and walks the frame chain. @@ -923,6 +928,7 @@ build_stacktrace_for_thread( sentry_value_new_string(i == 0 ? "context" : "cfi")); enrich_frame_with_module_info( ctx, temp_frames[frame_count], frame_ip); + enrich_frame_with_symbol(ctx, temp_frames[frame_count], frame_ip); frame_count++; } @@ -957,6 +963,9 @@ build_stacktrace_for_thread( sentry_value_set_by_key(temp_frames[frame_count], "trust", sentry_value_new_string("context")); enrich_frame_with_module_info(ctx, temp_frames[frame_count], ip); +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + enrich_frame_with_symbol(ctx, temp_frames[frame_count], ip); +#endif frame_count++; } @@ -1015,6 +1024,10 @@ build_stacktrace_for_thread( sentry_value_new_string("fp")); enrich_frame_with_module_info( ctx, temp_frames[frame_count], return_addr); +#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + enrich_frame_with_symbol( + ctx, temp_frames[frame_count], return_addr); +#endif frame_count++; walk_count++; @@ -1189,6 +1202,151 @@ extract_elf_build_id_for_module( return build_id_len; } +/** + * Resolve the symbol name for a given instruction address from an ELF symbol + * table and set the "function" key on the frame value. + */ +static void +enrich_frame_with_symbol( + const sentry_crash_context_t *ctx, sentry_value_t frame, uint64_t addr) +{ + for (uint32_t i = 0; i < ctx->module_count; i++) { + const sentry_module_info_t *mod = &ctx->modules[i]; + if (addr < mod->base_address || addr >= mod->base_address + mod->size) { + continue; + } + + uint64_t offset = addr - mod->base_address; + + int fd = open(mod->name, O_RDONLY); + if (fd < 0) { + return; + } + +# if defined(__x86_64__) || defined(__aarch64__) + Elf64_Ehdr ehdr; +# else + Elf32_Ehdr ehdr; +# endif + if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) { + close(fd); + return; + } + + 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; + } + + 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; + } + + 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 defined(__x86_64__) || defined(__aarch64__) + Elf64_Shdr *sections = (Elf64_Shdr *)shdr_buf; +# else + 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; + } + } + if (symtab_idx < 0) { + for (int j = 0; j < ehdr.e_shnum; j++) { + if (sections[j].sh_type == SHT_SYMTAB) { + symtab_idx = j; + break; + } + } + } + + if (symtab_idx >= 0) { + 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; + + void *sym_buf = sentry_malloc(sym_size); + void *strtab_buf = NULL; + + if (sym_buf + && lseek(fd, sections[symtab_idx].sh_offset, SEEK_SET) + == (off_t)sections[symtab_idx].sh_offset + && read(fd, sym_buf, sym_size) == (ssize_t)sym_size) { + + size_t strtab_size = sections[strtab_idx].sh_size; + strtab_buf = sentry_malloc(strtab_size); + if (strtab_buf + && lseek(fd, sections[strtab_idx].sh_offset, SEEK_SET) + == (off_t)sections[strtab_idx].sh_offset + && read(fd, strtab_buf, strtab_size) + == (ssize_t)strtab_size) { + + const char *best_name = NULL; + uint64_t best_value = 0; + +# if defined(__x86_64__) || defined(__aarch64__) + Elf64_Sym *syms = (Elf64_Sym *)sym_buf; + for (size_t k = 1; k < sym_count; k++) { + unsigned char sym_type = ELF64_ST_TYPE(syms[k].st_info); +# else + Elf32_Sym *syms = (Elf32_Sym *)sym_buf; + for (size_t k = 1; k < sym_count; k++) { + unsigned char sym_type = ELF32_ST_TYPE(syms[k].st_info); +# endif + if (syms[k].st_value == 0 + || syms[k].st_value > offset) { + continue; + } + if (sym_type != STT_FUNC && sym_type != STT_NOTYPE) { + continue; + } + if (!best_name || syms[k].st_value > best_value) { + if (syms[k].st_name < strtab_size) { + const char *name = (const char *)strtab_buf + + syms[k].st_name; + if (name[0]) { + best_name = name; + best_value = syms[k].st_value; + } + } + } + } + + if (best_name) { + sentry_value_set_by_key(frame, "function", + sentry_value_new_string(best_name)); + } + } + } + + sentry_free(sym_buf); + sentry_free(strtab_buf); + } + + sentry_free(shdr_buf); + close(fd); + return; + } +} + /** * Capture modules from /proc//maps for debug_meta * This is called from the daemon to populate ctx->modules[] on Linux, diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 138564cd1..f9040c058 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -894,7 +894,7 @@ def test_crash_mode_native_only(cmake, httpserver): for frame in exc["stacktrace"]["frames"]: assert "instruction_addr" in frame - if sys.platform == "win32": + if sys.platform == "win32" or sys.platform == "linux": # At least some frames should have symbolicated function names assert any( frame.get("function") is not None for frame in exc["stacktrace"]["frames"] @@ -941,7 +941,7 @@ def test_crash_mode_native_with_minidump(cmake, httpserver): assert exc["mechanism"]["type"] == "signalhandler" assert "stacktrace" in exc assert len(exc["stacktrace"]["frames"]) > 0 - if sys.platform == "win32": + if sys.platform == "win32" or sys.platform == "linux": # At least some frames should have symbolicated function names assert any( frame.get("function") is not None for frame in exc["stacktrace"]["frames"] From 7b6a6ee28ada329e2491f86c06a2413132bcd4a7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 28 May 2026 10:38:26 +0200 Subject: [PATCH 2/6] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc70695fe..9b4e61df0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ **Fixes**: +- Native/Linux: resolve function names for the crashed thread's stacktrace from on-disk ELF symbol tables in the crash daemon, so the most important thread gets symbolicated without ptrace. ([#1764](https://github.com/getsentry/sentry-native/pull/1764)) + - Finish active trace on crash. ([#1667](https://github.com/getsentry/sentry-native/pull/1667)) - Native/macOS: fix module `image_size` computation, which could have caused the symbolicator to misattribute every frame to the lowest-addressed image (typically `dyld` or `libsystem`). ([#1740](https://github.com/getsentry/sentry-native/pull/1740)) - Native: raise `SENTRY_CRASH_MAX_MODULES` from `512` to `2048` so processes that load many shared libraries no longer have their minidump module list truncated, which left frames in unrecorded modules without a `debug_id` and unsymbolicatable. @@ -22,6 +24,7 @@ - Native/macOS: honor the `system_crash_reporter_enabled` option. ([#1743](https://github.com/getsentry/sentry-native/pull/1743)) - Cap rate-limit retry-after values at 24 hours to prevent a MITM-provided response from disabling event delivery for the process lifetime. ([#1744](https://github.com/getsentry/sentry-native/pull/1744)) - Fix a shutdown-time use-after-free window in `sentry_close()`. ([#1750](https://github.com/getsentry/sentry-native/pull/1750)) +- Native/Linux: resolve symbol names for crashed thread on Linux. ([#1764](https://github.com/getsentry/sentry-native/pull/1764)) - Native: validate ELF header entry sizes. ([#1746](https://github.com/getsentry/sentry-native/pull/1746)) - Structured logs: respect printf argument widths when extracting log parameters to avoid stack-data disclosure and corrupted attributes on 32-bit platforms. ([#1752](https://github.com/getsentry/sentry-native/pull/1752)) - Fix a potential out-of-bounds read when parsing non-NUL-terminated `sentry-trace` headers. ([#1749](https://github.com/getsentry/sentry-native/pull/1749)) From 862335394edbb5c519137bced72cab51617a563c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 28 May 2026 10:46:32 +0200 Subject: [PATCH 3/6] sentry__elf_has_sym_entsize --- src/backends/native/sentry_crash_daemon.c | 4 +++- src/backends/native/sentry_elf.h | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 19799c60d..bd392ed79 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1278,7 +1278,9 @@ enrich_frame_with_symbol( } } - if (symtab_idx >= 0) { + if (symtab_idx >= 0 + && sentry__elf_has_sym_entsize( + ehdr.e_ident, sections[symtab_idx].sh_entsize)) { 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; diff --git a/src/backends/native/sentry_elf.h b/src/backends/native/sentry_elf.h index dac68021a..f646c5326 100644 --- a/src/backends/native/sentry_elf.h +++ b/src/backends/native/sentry_elf.h @@ -34,6 +34,21 @@ sentry__elf_has_shdr_size( # endif } +static inline bool +sentry__elf_has_sym_entsize( + const unsigned char e_ident[EI_NIDENT], size_t sh_entsize) +{ + if (!sentry__elf_is_native_class(e_ident)) { + return false; + } + +# if defined(__x86_64__) || defined(__aarch64__) + return sh_entsize == sizeof(Elf64_Sym); +# else + return sh_entsize == sizeof(Elf32_Sym); +# endif +} + static inline bool sentry__elf_has_phdr_size( const unsigned char e_ident[EI_NIDENT], size_t e_phentsize) From a352a2995da64547ab7cc32a0a01f19bbebc4e43 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 28 May 2026 10:47:17 +0200 Subject: [PATCH 4/6] strtab_idx --- src/backends/native/sentry_crash_daemon.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index bd392ed79..739ec8f75 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1284,6 +1284,11 @@ enrich_frame_with_symbol( 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; + } void *sym_buf = sentry_malloc(sym_size); void *strtab_buf = NULL; From 4855289e8dd6b0072dc42530adedb3b677da8071 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 28 May 2026 10:55:07 +0200 Subject: [PATCH 5/6] st_name --- src/backends/native/sentry_crash_daemon.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 739ec8f75..2e4e1e0b3 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1329,7 +1329,9 @@ enrich_frame_with_symbol( if (syms[k].st_name < strtab_size) { const char *name = (const char *)strtab_buf + syms[k].st_name; - if (name[0]) { + if (name[0] + && memchr(name, '\0', + strtab_size - syms[k].st_name)) { best_name = name; best_value = syms[k].st_value; } From 315781d29a9c1e4a47697f7966458991fe56bcc6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 28 May 2026 11:10:48 +0200 Subject: [PATCH 6/6] sym_target --- src/backends/native/sentry_crash_daemon.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 2e4e1e0b3..be660d432 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -1281,6 +1281,9 @@ enrich_frame_with_symbol( 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; @@ -1319,7 +1322,7 @@ enrich_frame_with_symbol( unsigned char sym_type = ELF32_ST_TYPE(syms[k].st_info); # endif if (syms[k].st_value == 0 - || syms[k].st_value > offset) { + || syms[k].st_value > sym_target) { continue; } if (sym_type != STT_FUNC && sym_type != STT_NOTYPE) {