From 3eee2411e23a2c7b2ad0b9dd22d678677d892c92 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 08:19:00 +0200 Subject: [PATCH 1/3] fix(unwinder): Guard invalid ARM32 instruction pointers On ARM32, unw_step first asks libunwind whether the current frame is a signal frame. The vendored implementation answers that by reading an instruction directly from the cursor's initial IP. Its local address-space validation is enabled only after this probe, so an unmapped IP faults inside the crash handler instead of returning an unwind error. This surfaced when unrelated stack-layout changes made the handler fallback produce 0x10c as its initial IP. No attachment code ran on that path; the changed layout only exposed the unwinder's unchecked read. The resulting SIGSEGV recursively entered the crash handler and prevented the original abort from being captured. Check /proc/self/maps before calling unw_step. Keep the initial frame, as the existing invalid-stack-pointer path does, but stop the walk before libunwind can dereference an unmapped address. Cover the failure with an ARM32 context whose initial IP is intentionally unmapped. --- src/unwinder/sentry_unwinder_libunwind.c | 6 ++++++ tests/unit/test_unwinder.c | 27 ++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 34 insertions(+) diff --git a/src/unwinder/sentry_unwinder_libunwind.c b/src/unwinder/sentry_unwinder_libunwind.c index 997c65e6e5..9a154608c8 100644 --- a/src/unwinder/sentry_unwinder_libunwind.c +++ b/src/unwinder/sentry_unwinder_libunwind.c @@ -191,6 +191,12 @@ sentry__unwind_stack_libunwind( if (n < max_frames) { ptrs[n++] = (void *)ip; } + + mem_range_t code = { 0, 0 }; + if (uctx && !find_mem_range((uintptr_t)ip, &code)) { + return n; + } + unw_word_t sp = 0; (void)unw_get_reg(&cursor, UNW_REG_SP, &sp); diff --git a/tests/unit/test_unwinder.c b/tests/unit/test_unwinder.c index 07fb2a0a7c..1e6bf0ae7d 100644 --- a/tests/unit/test_unwinder.c +++ b/tests/unit/test_unwinder.c @@ -7,6 +7,10 @@ # include "unwinder/sentry_unwinder.h" # include # include +# if defined(__arm__) +# define UNW_LOCAL_ONLY +# include +# endif extern bool find_mem_range_from_fd(int fd, uintptr_t ptr, mem_range_t *range); #endif @@ -103,6 +107,29 @@ SENTRY_TEST(unwinder) } } +SENTRY_TEST(unwinder_unmapped_ip) +{ +#if !defined(SENTRY_WITH_UNWINDER_LIBUNWIND) || !defined(__arm__) + SKIP_TEST(); +#else + int stack_var = 0; + // libunwind uses its own register-array context on ARM. + unw_context_t unwind_context = { 0 }; + unwind_context.regs[13] = (uintptr_t)&stack_var; + unwind_context.regs[15] = 0x10c; + + sentry_ucontext_t context = { 0 }; + context.user_context = (ucontext_t *)&unwind_context; + + void *backtrace[2] = { 0 }; + size_t frame_count + = sentry_unwind_stack_from_ucontext(&context, backtrace, 2); + + TEST_CHECK_INT_EQUAL(frame_count, 1); + TEST_CHECK_PTR_EQUAL(backtrace[0], (void *)(uintptr_t)0x10c); +#endif +} + SENTRY_TEST(find_mem_range) { #if !defined(SENTRY_WITH_UNWINDER_LIBUNWIND) diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index dbc00cae5c..a4a1b696d3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -414,6 +414,7 @@ XX(uint128_struct_size) XX(uninitialized) XX(unsampled_spans) XX(unwinder) +XX(unwinder_unmapped_ip) XX(update_from_header_no_sampled_flag) XX(update_from_header_null_ctx) XX(update_release_and_environment_late) From e5d9bca604c48cb94d89be81da077f0714e2fd1c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 08:32:42 +0200 Subject: [PATCH 2/3] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a275f801a5..e1ca1aac96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Crashpad: prevent external crash reporters from bypassing revoked user consent. ([#1972](https://github.com/getsentry/sentry-native/pull/1972), [crashpad#168](https://github.com/getsentry/crashpad/pull/168)) - Native/Linux: improve startup time by avoiding zeroing a large shmem module array during crash context initialization. ([#1966](https://github.com/getsentry/sentry-native/pull/1966)) +- Linux/ARM32: prevent recursive crashes when libunwind receives an unmapped initial instruction pointer during crash handling. ([#1977](https://github.com/getsentry/sentry-native/pull/1977)) ## 0.16.2 From 67c492c5cc7b13faf1444917499a63bc92c818e0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 13 Aug 2026 12:50:16 +0200 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1decc96dcc..083c0be152 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Fixes**: + +- Linux/ARM32: prevent recursive crashes when libunwind receives an unmapped initial instruction pointer during crash handling. ([#1977](https://github.com/getsentry/sentry-native/pull/1977)) + ## 0.16.3 **Features**: @@ -15,7 +21,6 @@ - Crashpad: avoid allocating while handling a crash. The backend built the attachment `base::FilePath` inside the crash handler; if the crash corrupted the heap (e.g. via an overridden `operator new`) that allocation faults again and the report is lost. The paths are now cached at startup. ([#1984](https://github.com/getsentry/sentry-native/pull/1984)) - Crashpad: prevent external crash reporters from bypassing revoked user consent. ([#1972](https://github.com/getsentry/sentry-native/pull/1972), [crashpad#168](https://github.com/getsentry/crashpad/pull/168)) - Native/Linux: improve startup time by avoiding zeroing a large shmem module array during crash context initialization. ([#1966](https://github.com/getsentry/sentry-native/pull/1966)) -- Linux/ARM32: prevent recursive crashes when libunwind receives an unmapped initial instruction pointer during crash handling. ([#1977](https://github.com/getsentry/sentry-native/pull/1977)) ## 0.16.2