Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a6504fa
feat(native): add remote DWARF unwinding + symbol names for Linux
jpnurmi Mar 27, 2026
d1818f5
fix(native): keep crash stack buffer for unwind fallback
jpnurmi May 25, 2026
d23877a
fix(native): heap allocate remote unwind frames
jpnurmi May 25, 2026
6a4bb99
Update VENDORING.md
jpnurmi May 25, 2026
0602eda
ref(native): align remote unwinder with unwinder dispatch
jpnurmi May 25, 2026
5e16a6e
fix(native): prefer signal backtrace for crashed thread
jpnurmi May 25, 2026
26c07d5
Update CHANGELOG.md
jpnurmi May 25, 2026
dcd8ebc
docs(native): clarify stack frame trust assignment
jpnurmi May 25, 2026
311e4c4
fix(native): avoid ptrace unwind of crashed thread
jpnurmi May 25, 2026
a506274
fix(native): verify ptrace stop before remote unwind
jpnurmi May 25, 2026
af1e9b8
fix(native): wire arm32 remote libunwind build
jpnurmi May 25, 2026
2d1abb8
fix(native): silence remote unwinder dispatcher warnings
jpnurmi May 25, 2026
95db646
fix(native): omit bogus registers from remote stacks
jpnurmi May 25, 2026
4cf3b68
fix(native): attach registers to remote unwind stacks
jpnurmi May 25, 2026
5949cbc
Merge remote-tracking branch 'origin/master' into jpnurmi/feat/native…
jpnurmi May 28, 2026
469c5e7
Merge remote-tracking branch 'origin/master' into jpnurmi/feat/native…
jpnurmi May 28, 2026
f11ba37
fix(native): don't map thread index 0 to crashed_tid in remote unwinding
jpnurmi May 28, 2026
0de2641
fix(build): link libunwind-ptrace for sentry-crash with system libunwind
jpnurmi May 28, 2026
8c90ccf
fix(vendor/libunwind): detect PT_* declarations via cmake instead of …
jpnurmi May 28, 2026
5a66928
fix(unwinder): guard waitpid with WNOHANG poll loop to avoid daemon hang
jpnurmi May 28, 2026
71e02e5
Update CHANGELOG.md
jpnurmi May 28, 2026
06c3238
fix(unwinder): reduce waitpid timeout from 5s to 2s
jpnurmi May 28, 2026
722e510
unistd
jpnurmi May 28, 2026
f77bef2
symbol_addr
jpnurmi May 28, 2026
8a820be
missing SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE guard
jpnurmi May 28, 2026
4721331
fix guard
jpnurmi May 28, 2026
0906520
guard build_registers_from_remote_registers
jpnurmi May 28, 2026
33e9f08
UNW_ENOMEM
jpnurmi May 28, 2026
79f17f0
symbol_offset
jpnurmi May 28, 2026
8312214
for (;;)
jpnurmi May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

**Features**:

- Native/Linux: symbolicate stack frames in the crash daemon. ([#1747](https://github.com/getsentry/sentry-native/pull/1747))
- Native: add opt-in async crash upload mode so crashed apps can exit early after crash data is captured, while the crash daemon finishes potentially large uploads in the background. ([#1739](https://github.com/getsentry/sentry-native/pull/1739))
- Add a `transfer_timeout` option for SDK-managed HTTP transports. ([#1741](https://github.com/getsentry/sentry-native/pull/1741))

Expand Down
15 changes: 13 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,10 @@ if(SENTRY_WITH_LIBUNWIND)
endif()
else()
# Use vendored libunwind
if(SENTRY_BACKEND_NATIVE)
# Build remote unwinding support for the native backend's crash daemon
set(LIBUNWIND_BUILD_REMOTE TRUE)
endif()
add_subdirectory(vendor/libunwind)
target_link_libraries(sentry PRIVATE unwind)
if(NOT SENTRY_BUILD_SHARED_LIBS)
Expand Down Expand Up @@ -851,6 +855,10 @@ elseif(SENTRY_BACKEND_NATIVE)
SENTRY_BUILD_STATIC
)

if(SENTRY_WITH_LIBUNWIND AND LINUX)
target_compile_definitions(sentry-crash PRIVATE SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE)
endif()
Comment thread
cursor[bot] marked this conversation as resolved.

# Copy include directories from sentry target
target_include_directories(sentry-crash PRIVATE
${PROJECT_SOURCE_DIR}/include
Expand Down Expand Up @@ -891,9 +899,12 @@ elseif(SENTRY_BACKEND_NATIVE)

if(SENTRY_WITH_LIBUNWIND AND LINUX)
if(SENTRY_LIBUNWIND_SYSTEM)
target_link_libraries(sentry-crash PRIVATE PkgConfig::LIBUNWIND)
pkg_check_modules(LIBUNWIND_PTRACE REQUIRED IMPORTED_TARGET libunwind-ptrace)
target_link_libraries(sentry-crash PRIVATE PkgConfig::LIBUNWIND PkgConfig::LIBUNWIND_PTRACE)
else()
target_link_libraries(sentry-crash PRIVATE unwind)
# Use unwind_remote for the daemon (includes ptrace accessors
# for remote DWARF unwinding of the crashed process)
target_link_libraries(sentry-crash PRIVATE unwind_remote)
endif()
endif()

Expand Down
7 changes: 7 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ elseif(SENTRY_BACKEND_NATIVE)
backends/native/minidump/sentry_minidump_writer.h
)

# Remote unwinding (Linux only — uses libunwind ptrace accessors)
if(LINUX)
sentry_target_sources_cwd(sentry
unwinder/sentry_unwinder_libunwind_remote.c
)
endif()

# Platform-specific minidump writers
if(LINUX OR ANDROID)
sentry_target_sources_cwd(sentry
Expand Down
108 changes: 105 additions & 3 deletions src/backends/native/sentry_crash_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
# include <mach-o/dyld.h>
# include <spawn.h>
# endif
# if defined(SENTRY_PLATFORM_LINUX)
# include "unwinder/sentry_unwinder.h"
# endif
#elif defined(SENTRY_PLATFORM_WINDOWS)
# include <dbghelp.h>
# include <fcntl.h>
Expand Down Expand Up @@ -544,6 +547,24 @@ build_registers_from_ctx(const sentry_crash_context_t *ctx, size_t thread_idx)
return registers;
}

#if defined(SENTRY_PLATFORM_LINUX)
static sentry_value_t
build_registers_from_remote_registers(
const sentry_remote_registers_t *remote_registers)
{
sentry_value_t registers = sentry_value_new_object();

for (size_t i = 0; i < remote_registers->count; i++) {
const sentry_remote_register_t *remote_register
= &remote_registers->values[i];
sentry_value_set_by_key(registers, remote_register->name,
sentry__value_new_addr(remote_register->value));
}

return registers;
}
#endif

/**
* Maximum number of frames to unwind
*/
Expand Down Expand Up @@ -905,10 +926,88 @@ build_stacktrace_for_thread(
sentry_value_t temp_frames[MAX_STACK_FRAMES];
int frame_count = 0;

#if defined(SENTRY_PLATFORM_LINUX)
// Remote DWARF unwinding via libunwind ptrace accessors. Do not attach to
// the crashed thread from the daemon; use the saved fault context below.
{
pid_t tid = 0;
if (thread_idx == SIZE_MAX) {
tid = ctx->crashed_tid;
} else if (thread_idx < ctx->platform.num_threads) {
tid = ctx->platform.threads[thread_idx].tid;
}
Comment thread
cursor[bot] marked this conversation as resolved.

bool is_crashed_thread
= thread_idx == SIZE_MAX || tid == ctx->crashed_tid;

if (tid > 0 && !is_crashed_thread) {
sentry_remote_registers_t registers = { 0 };
sentry_remote_frame_t *remote_frames
= sentry_malloc(sizeof(*remote_frames) * MAX_STACK_FRAMES);
size_t remote_count = remote_frames
? sentry__unwind_stack_from_thread(
tid, remote_frames, MAX_STACK_FRAMES, &registers)
: 0;

if (remote_count > 0) {
SENTRY_DEBUGF("Remote unwound %zu frames for thread %d",
remote_count, tid);

for (size_t i = 0;
i < remote_count && frame_count < MAX_STACK_FRAMES; i++) {
if (remote_frames[i].ip == 0
|| !is_valid_code_addr(remote_frames[i].ip)) {
continue;
}
temp_frames[frame_count] = sentry_value_new_object();
sentry_value_set_by_key(temp_frames[frame_count],
"instruction_addr",
sentry__value_new_addr(remote_frames[i].ip));
// Trust describes the unwind source, not the emitted
// frame index. If the initial cursor frame is filtered
// out, the next emitted frame was still reached via CFI.
sentry_value_set_by_key(temp_frames[frame_count], "trust",
sentry_value_new_string(i == 0 ? "context" : "cfi"));
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
enrich_frame_with_module_info(
ctx, temp_frames[frame_count], remote_frames[i].ip);
if (remote_frames[i].symbol[0]) {
sentry_value_set_by_key(temp_frames[frame_count],
"function",
sentry_value_new_string(remote_frames[i].symbol));
}
frame_count++;
Comment thread
cursor[bot] marked this conversation as resolved.
}

if (frame_count > 0) {
if (stack_buf) {
sentry_free(stack_buf);
}

for (int i = frame_count - 1; i >= 0; i--) {
sentry_value_append(frames, temp_frames[i]);
}
sentry_value_set_by_key(stacktrace, "frames", frames);
if (registers.count > 0) {
sentry_value_set_by_key(stacktrace, "registers",
build_registers_from_remote_registers(&registers));
}
sentry_free(remote_frames);
return stacktrace;
Comment thread
cursor[bot] marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

if (remote_frames) {
sentry_free(remote_frames);
}
}
}
// Fall through to pre-captured backtrace or FP-walking if remote
// unwinding failed
#endif
Comment thread
cursor[bot] marked this conversation as resolved.

#if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID)
// Use pre-captured libunwind backtrace if available (DWARF-based, works
// without frame pointers). This is preferred over FP-based walking for
// the crashed thread.
// Fallback: use pre-captured libunwind backtrace if available
// (DWARF-based, works without frame pointers).
if (ctx->platform.backtrace_count > 0
&& (thread_idx == SIZE_MAX || thread_idx == 0)) {
SENTRY_DEBUGF("Using pre-captured libunwind backtrace (%zu frames)",
Expand All @@ -924,6 +1023,9 @@ build_stacktrace_for_thread(
temp_frames[frame_count] = sentry_value_new_object();
sentry_value_set_by_key(temp_frames[frame_count],
"instruction_addr", sentry__value_new_addr(frame_ip));
// Trust describes the unwind source, not the emitted frame index.
// If the initial cursor frame is filtered out, the next emitted
// frame was still reached via CFI.
sentry_value_set_by_key(temp_frames[frame_count], "trust",
sentry_value_new_string(i == 0 ? "context" : "cfi"));
enrich_frame_with_module_info(
Expand Down
35 changes: 35 additions & 0 deletions src/unwinder/sentry_unwinder.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "sentry_unwinder.h"
#include "sentry_boot.h"

#define DEFINE_UNWINDER(Func) \
Expand All @@ -19,6 +20,24 @@ DEFINE_UNWINDER(libunwind);
DEFINE_UNWINDER(libunwind_mac);
DEFINE_UNWINDER(psunwind);

#if defined(SENTRY_PLATFORM_LINUX)
# define DEFINE_THREAD_UNWINDER(Func) \
size_t sentry__unwind_stack_from_thread_##Func(pid_t tid, \
sentry_remote_frame_t *frames, size_t max_frames, \
sentry_remote_registers_t *registers)

# define TRY_THREAD_UNWINDER(Func) \
do { \
size_t rv = sentry__unwind_stack_from_thread_##Func( \
tid, frames, max_frames, registers); \
if (rv > 0) { \
return rv; \
} \
} while (0)

DEFINE_THREAD_UNWINDER(libunwind_remote);
#endif

static size_t
unwind_stack(
void *addr, const sentry_ucontext_t *uctx, void **ptrs, size_t max_frames)
Expand Down Expand Up @@ -56,3 +75,19 @@ sentry_unwind_stack_from_ucontext(
{
return unwind_stack(NULL, uctx, stacktrace_out, max_len);
}

#if defined(SENTRY_PLATFORM_LINUX)
size_t
sentry__unwind_stack_from_thread(pid_t tid, sentry_remote_frame_t *frames,
size_t max_frames, sentry_remote_registers_t *registers)
{
(void)tid;
(void)frames;
(void)max_frames;
(void)registers;
# ifdef SENTRY_WITH_UNWINDER_LIBUNWIND_REMOTE
TRY_THREAD_UNWINDER(libunwind_remote);
# endif
return 0;
}
#endif
35 changes: 35 additions & 0 deletions src/unwinder/sentry_unwinder.h
Original file line number Diff line number Diff line change
@@ -1,8 +1,43 @@
#ifndef SENTRY_UNWINDER_H_INCLUDED
#define SENTRY_UNWINDER_H_INCLUDED

#include "sentry_boot.h"

#include <stdint.h>

typedef struct {
uintptr_t lo, hi;
} mem_range_t;

#if defined(SENTRY_PLATFORM_LINUX)

# include <stddef.h>
# include <sys/types.h>

# define SENTRY_REMOTE_UNWIND_MAX_REGISTER_NAME 16
# define SENTRY_REMOTE_UNWIND_MAX_REGISTERS 64
# define SENTRY_REMOTE_UNWIND_MAX_SYMBOL 256

typedef struct {
char name[SENTRY_REMOTE_UNWIND_MAX_REGISTER_NAME];
uint64_t value;
} sentry_remote_register_t;

typedef struct {
uint64_t ip;
char symbol[SENTRY_REMOTE_UNWIND_MAX_SYMBOL];
uint64_t symbol_offset;
} sentry_remote_frame_t;

typedef struct {
size_t count;
sentry_remote_register_t values[SENTRY_REMOTE_UNWIND_MAX_REGISTERS];
} sentry_remote_registers_t;

size_t sentry__unwind_stack_from_thread(pid_t tid,
sentry_remote_frame_t *frames, size_t max_frames,
sentry_remote_registers_t *registers);

#endif

#endif // SENTRY_UNWINDER_H_INCLUDED
Loading
Loading