From 346f89cedbdad6b4c636b73d06b13bb7ddcfc384 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 12:15:16 +0200 Subject: [PATCH 01/12] Add an option to reserve memory for the crash handler Add the SENTRY_CRASH_MEMORY_RESERVE build setting (in KiB, 0 by default) and sentry_options_set_crash_memory_reserve() to override it at runtime. --- CMakeLists.txt | 6 ++++++ README.md | 4 ++++ include/sentry.h | 11 +++++++++++ src/sentry_options.c | 7 +++++++ src/sentry_options.h | 1 + 5 files changed, 29 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index b86d4b4da..d23b50f04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -289,6 +289,10 @@ endif() set(SENTRY_SDK_NAME "" CACHE STRING "The SDK name to report when sending events.") set(SENTRY_HANDLER_STACK_SIZE 64 CACHE STRING "The stack size (in KiB) that should be reserved for the crash handler.") +set(SENTRY_CRASH_MEMORY_RESERVE 0 CACHE STRING "The memory (in KiB) that is reserved on init and released when a crash is handled. 0 disables it.") +if(NOT SENTRY_CRASH_MEMORY_RESERVE MATCHES "^[0-9]+$") + message(FATAL_ERROR "SENTRY_CRASH_MEMORY_RESERVE must be a non-negative integer") +endif() set(SENTRY_BATCHER_BUFFER_COUNT 3 CACHE STRING "Number of 100-item telemetry buffers (2-10).") if(NOT SENTRY_BATCHER_BUFFER_COUNT MATCHES "^[0-9]+$" OR SENTRY_BATCHER_BUFFER_COUNT LESS 2 @@ -307,6 +311,7 @@ message(STATUS "SENTRY_LIBRARY_TYPE=${SENTRY_LIBRARY_TYPE}") message(STATUS "SENTRY_SDK_NAME=${SENTRY_SDK_NAME}") message(STATUS "SENTRY_INTEGRATION_PLATFORM=${SENTRY_INTEGRATION_PLATFORM}") message(STATUS "SENTRY_HANDLER_STACK_SIZE=${SENTRY_HANDLER_STACK_SIZE}") +message(STATUS "SENTRY_CRASH_MEMORY_RESERVE=${SENTRY_CRASH_MEMORY_RESERVE}") message(STATUS "SENTRY_BATCHER_BUFFER_COUNT=${SENTRY_BATCHER_BUFFER_COUNT}") if (WIN32) message(STATUS "SENTRY_THREAD_STACK_GUARANTEE_FACTOR=${SENTRY_THREAD_STACK_GUARANTEE_FACTOR}") @@ -432,6 +437,7 @@ if(SENTRY_INTEGRATION_PLATFORM) endif() target_compile_definitions(sentry PRIVATE SENTRY_HANDLER_STACK_SIZE=${SENTRY_HANDLER_STACK_SIZE}) +target_compile_definitions(sentry PRIVATE SENTRY_CRASH_MEMORY_RESERVE=${SENTRY_CRASH_MEMORY_RESERVE}) target_compile_definitions(sentry PRIVATE SENTRY_BATCHER_BUFFER_COUNT=${SENTRY_BATCHER_BUFFER_COUNT}) if(WIN32) target_compile_definitions(sentry PRIVATE SENTRY_THREAD_STACK_GUARANTEE_FACTOR=${SENTRY_THREAD_STACK_GUARANTEE_FACTOR}) diff --git a/README.md b/README.md index fc21a0ced..a75860bee 100644 --- a/README.md +++ b/README.md @@ -314,6 +314,10 @@ using `cmake -D BUILD_SHARED_LIBS=OFF ..`. this value can be as small as 16KiB with `breakpad` and 24KiB with `crashpad` and `native`, but we recommend 32KiB as the lower bound on 64-bit systems. **The value should be a multiple of the page size**. +- `SENTRY_CRASH_MEMORY_RESERVE` (Default: `0`): + The size in KiB of memory reserved on `sentry_init()` and released when a crash is handled, so the crash handler + can still allocate after the application has run out of memory. `0` disables it. + - `SENTRY_THREAD_STACK_GUARANTEE_FACTOR` (Default: `10`, only for Windows): Defines the factor by which the thread's stack reserve must exceed the specified guarantee for the handler. _Example_: if the `SENTRY_HANDLER_STACK_SIZE` is defined as 64KiB, then the thread's stack reserve must at least have diff --git a/include/sentry.h b/include/sentry.h index 14484bdfc..220b88d4a 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2458,6 +2458,17 @@ SENTRY_API void sentry_options_set_crashpad_limit_stack_capture_to_sp( SENTRY_API void sentry_options_set_max_stack_capture_size( sentry_options_t *opts, uint32_t max_stack_capture_size); +/** + * Sets the size (in bytes) of memory that is reserved on `sentry_init` and + * released when a crash is handled, so the crash handler can still allocate + * after the application has run out of memory. `0` disables it. + * + * Defaults to the `SENTRY_CRASH_MEMORY_RESERVE` build setting (in KiB), which + * is `0`. Custom backends must release the reserve from their crash handler. + */ +SENTRY_API void sentry_options_set_crash_memory_reserve( + sentry_options_t *opts, size_t bytes); + /** * Sets the maximum time (in milliseconds) to wait for the asynchronous * tasks to end on shutdown before attempting a forced termination. diff --git a/src/sentry_options.c b/src/sentry_options.c index bfb62146b..9fff22277 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -92,6 +92,7 @@ sentry_options_new(void) opts->strict_trace_continuation = false; opts->crashpad_limit_stack_capture_to_sp = false; opts->max_stack_capture_size = 0; + opts->crash_memory_reserve = (size_t)SENTRY_CRASH_MEMORY_RESERVE * 1024; opts->cache_keep = SENTRY_CACHE_KEEP_NONE; opts->cache_max_age = 0; opts->cache_max_size = 0; @@ -767,6 +768,12 @@ sentry_options_set_max_stack_capture_size( opts->max_stack_capture_size = max_stack_capture_size; } +void +sentry_options_set_crash_memory_reserve(sentry_options_t *opts, size_t bytes) +{ + opts->crash_memory_reserve = bytes; +} + void sentry_options_set_shutdown_timeout( sentry_options_t *opts, uint64_t shutdown_timeout) diff --git a/src/sentry_options.h b/src/sentry_options.h index 12548a79d..405f43e77 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -56,6 +56,7 @@ struct sentry_options_s { bool strict_trace_continuation; bool crashpad_limit_stack_capture_to_sp; uint32_t max_stack_capture_size; + size_t crash_memory_reserve; sentry_cache_keep_t cache_keep; time_t cache_max_age; From c17c7ec0d83d8b77261047356c0dae5a5279b02b Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 12:16:29 +0200 Subject: [PATCH 02/12] Reserve memory on init and release it on shutdown Map and touch the reserve in sentry_init() before the backend starts, so it is backed by memory when a crash occurs, and release it on close or when init fails. Windows uses VirtualAlloc and Unix uses mmap. Other platforms log a warning when a reserve is requested. --- src/CMakeLists.txt | 2 + src/sentry_core.c | 5 +++ src/sentry_crash_reserve.c | 90 ++++++++++++++++++++++++++++++++++++++ src/sentry_crash_reserve.h | 22 ++++++++++ 4 files changed, 119 insertions(+) create mode 100644 src/sentry_crash_reserve.c create mode 100644 src/sentry_crash_reserve.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b528429f9..c66ff0261 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,6 +16,8 @@ sentry_target_sources_cwd(sentry sentry_client_report.h sentry_core.c sentry_core.h + sentry_crash_reserve.c + sentry_crash_reserve.h sentry_cpu_relax.h sentry_database.c sentry_database.h diff --git a/src/sentry_core.c b/src/sentry_core.c index 5dd346a16..0365a8e2a 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -12,6 +12,7 @@ #include "sentry_backend.h" #include "sentry_client_report.h" #include "sentry_core.h" +#include "sentry_crash_reserve.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_hint.h" @@ -253,6 +254,8 @@ sentry_init(sentry_options_t *options) } initial_scope_prepared = true; + sentry__crash_reserve_alloc(options->crash_memory_reserve); + // and then we will start the backend, since it requires a valid run sentry_backend_t *backend = options->backend; if (backend && backend->startup_func) { @@ -313,6 +316,7 @@ sentry_init(sentry_options_t *options) fail: SENTRY_WARN("`sentry_init` failed"); + sentry__crash_reserve_release(); if (transport) { sentry__transport_shutdown(transport, 0); } @@ -384,6 +388,7 @@ sentry_close(void) SENTRY_DEBUG("shutting down backend"); options->backend->shutdown_func(options->backend); } + sentry__crash_reserve_release(); if (options->transport) { if (sentry__transport_shutdown( diff --git a/src/sentry_crash_reserve.c b/src/sentry_crash_reserve.c new file mode 100644 index 000000000..1103f9178 --- /dev/null +++ b/src/sentry_crash_reserve.c @@ -0,0 +1,90 @@ +#include "sentry_crash_reserve.h" +#include "sentry_logger.h" + +#include + +#if defined(SENTRY_PLATFORM_WINDOWS) +# include +# define SENTRY_CRASH_RESERVE_SUPPORTED +#elif defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_PS) +# include +# if !defined(MAP_ANONYMOUS) && defined(SENTRY_PLATFORM_DARWIN) +# define MAP_ANONYMOUS MAP_ANON +# endif +# define SENTRY_CRASH_RESERVE_SUPPORTED +#endif + +static void *volatile g_reserve = NULL; +static size_t g_reserve_size = 0; + +#ifdef SENTRY_CRASH_RESERVE_SUPPORTED +static void * +take_reserve(void) +{ +# ifdef SENTRY_PLATFORM_WINDOWS + return InterlockedExchangePointer((void *volatile *)&g_reserve, NULL); +# else + return __atomic_exchange_n(&g_reserve, NULL, __ATOMIC_SEQ_CST); +# endif +} + +static void * +map_memory(size_t size) +{ +# ifdef SENTRY_PLATFORM_WINDOWS + return VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); +# else + void *ptr = mmap( + NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + return ptr == MAP_FAILED ? NULL : ptr; +# endif +} +#endif + +void +sentry__crash_reserve_alloc(size_t size) +{ + sentry__crash_reserve_release(); + if (size == 0) { + return; + } + +#ifdef SENTRY_CRASH_RESERVE_SUPPORTED + void *ptr = map_memory(size); + if (!ptr) { + SENTRY_WARNF( + "failed to reserve %zu bytes of crash handler memory", size); + return; + } + // touch the pages so they are backed by memory + memset(ptr, 1, size); + + g_reserve_size = size; + g_reserve = ptr; + SENTRY_DEBUGF("reserved %zu bytes of crash handler memory", size); +#else + SENTRY_WARN("the crash memory reserve is not supported on this platform"); +#endif +} + +void +sentry__crash_reserve_release(void) +{ +#ifdef SENTRY_CRASH_RESERVE_SUPPORTED + void *ptr = take_reserve(); + if (!ptr) { + return; + } +# ifdef SENTRY_PLATFORM_WINDOWS + VirtualFree(ptr, 0, MEM_RELEASE); +# else + munmap(ptr, g_reserve_size); +# endif +#endif +} + +size_t +sentry__crash_reserve_size(void) +{ + return g_reserve ? g_reserve_size : 0; +} diff --git a/src/sentry_crash_reserve.h b/src/sentry_crash_reserve.h new file mode 100644 index 000000000..e3f96bafd --- /dev/null +++ b/src/sentry_crash_reserve.h @@ -0,0 +1,22 @@ +#ifndef SENTRY_CRASH_RESERVE_H_INCLUDED +#define SENTRY_CRASH_RESERVE_H_INCLUDED + +#include "sentry_boot.h" + +/** + * Reserves `size` bytes of committed memory for the crash handler, replacing + * any previous reserve. + */ +void sentry__crash_reserve_alloc(size_t size); + +/** + * Releases the reserve. Safe to call from a crash handler and more than once. + */ +void sentry__crash_reserve_release(void); + +/** + * Returns the size of the memory that is currently reserved, or `0`. + */ +size_t sentry__crash_reserve_size(void); + +#endif From 6523ecec7478d616bb5905d4e502745efc06f4c1 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 12:17:44 +0200 Subject: [PATCH 03/12] Release the crash memory reserve when a backend handles a crash Release it once each backend has decided to handle the crash, so that signals or exceptions that are handed back to a runtime keep it. Breakpad gets a filter callback to release it before the minidump is written, which covers unhandled exceptions, invalid parameters, pure calls and SIGABRT on Windows. The native backend releases it before signaling the daemon, which may share the application's memory budget. --- src/backends/native/sentry_crash_handler.c | 6 ++++ src/backends/sentry_backend_breakpad.cpp | 39 ++++++++++++++++------ src/backends/sentry_backend_crashpad.cpp | 2 ++ src/backends/sentry_backend_inproc.c | 3 ++ 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index a811712a0..a9cabf8ba 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -8,6 +8,7 @@ #include "sentry_alloc.h" #include "sentry_core.h" +#include "sentry_crash_reserve.h" #include "sentry_logger.h" #include "sentry_os.h" #include "sentry_sync.h" @@ -336,6 +337,8 @@ crash_signal_handler(int signum, siginfo_t *info, void *context) _exit(1); } + sentry__crash_reserve_release(); + sentry_crash_context_t *ctx = ipc->shmem; // Fill crash context @@ -1037,6 +1040,9 @@ crash_exception_filter(EXCEPTION_POINTERS *exception_info) return EXCEPTION_CONTINUE_SEARCH; } + // the daemon may share the application's memory budget + sentry__crash_reserve_release(); + sentry_crash_context_t *ctx = ipc->shmem; // Fill crash context diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 1662686e2..86878bafc 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -6,6 +6,7 @@ extern "C" { #include "sentry_backend.h" #include "sentry_client_report.h" #include "sentry_core.h" +#include "sentry_crash_reserve.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_hint.h" @@ -344,6 +345,20 @@ IsDebuggerActive() } #endif +// called before the minidump is written +#ifdef SENTRY_PLATFORM_WINDOWS +static bool +breakpad_backend_filter(void *UNUSED(context), + EXCEPTION_POINTERS *UNUSED(exinfo), MDRawAssertionInfo *UNUSED(assertion)) +#else +static bool +breakpad_backend_filter(void *UNUSED(context)) +#endif +{ + sentry__crash_reserve_release(); + return true; +} + static int breakpad_backend_startup( sentry_backend_t *backend, const sentry_options_t *options) @@ -355,26 +370,28 @@ breakpad_backend_startup( && defined(SENTRY_THREAD_STACK_GUARANTEE_AUTO_INIT) sentry__set_default_thread_stack_guarantee(); # endif - backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( - current_run_folder->path_w, nullptr, breakpad_backend_callback, nullptr, - google_breakpad::ExceptionHandler::HANDLER_EXCEPTION); + backend->data = new (std::nothrow) + google_breakpad::ExceptionHandler(current_run_folder->path_w, + breakpad_backend_filter, breakpad_backend_callback, nullptr, + google_breakpad::ExceptionHandler::HANDLER_EXCEPTION); if (backend->data) { sentry__win32_install_sigabrt_handler(handle_sigabrt); } #elif defined(SENTRY_PLATFORM_MACOS) // If process is being debugged and there are breakpoints set it will cause // task_set_exception_ports to crash the whole process and debugger - backend->data = new (std::nothrow) - google_breakpad::ExceptionHandler(current_run_folder->path, nullptr, - breakpad_backend_callback, nullptr, !IsDebuggerActive(), nullptr); + backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( + current_run_folder->path, breakpad_backend_filter, + breakpad_backend_callback, nullptr, !IsDebuggerActive(), nullptr); #elif defined(SENTRY_PLATFORM_IOS) - backend->data = new (std::nothrow) - google_breakpad::ExceptionHandler(current_run_folder->path, nullptr, - breakpad_backend_callback, nullptr, true, nullptr); + backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( + current_run_folder->path, breakpad_backend_filter, + breakpad_backend_callback, nullptr, true, nullptr); #else google_breakpad::MinidumpDescriptor descriptor(current_run_folder->path); - backend->data = new (std::nothrow) google_breakpad::ExceptionHandler( - descriptor, nullptr, breakpad_backend_callback, nullptr, true, -1); + backend->data = new (std::nothrow) + google_breakpad::ExceptionHandler(descriptor, breakpad_backend_filter, + breakpad_backend_callback, nullptr, true, -1); #endif return backend->data == nullptr; } diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index c31018583..a4693f7e4 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -6,6 +6,7 @@ extern "C" { #include "sentry_backend.h" #include "sentry_core.h" #include "sentry_cpu_relax.h" +#include "sentry_crash_reserve.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_hint.h" @@ -557,6 +558,7 @@ crashpad_handler(int signum, siginfo_t *info, ucontext_t *user_context) (void)!sentry__enter_signal_handler(); # endif sentry__enter_crash_handler(); + sentry__crash_reserve_release(); // Disable logging during crash handling if the option is set SENTRY_WITH_OPTIONS (options) { if (!options->enable_logging_when_crashed) { diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 0bc415b3f..33a27bd85 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -5,6 +5,7 @@ #include "sentry_backend.h" #include "sentry_core.h" #include "sentry_cpu_relax.h" +#include "sentry_crash_reserve.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_hint.h" @@ -1358,6 +1359,8 @@ process_ucontext(const sentry_ucontext_t *uctx) } #endif + sentry__crash_reserve_release(); + if (!g_backend_config.enable_logging_when_crashed) { sentry__logger_disable(); } From 865096810e0d19c5685964cc09ee666ee8d061b2 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 12:20:05 +0200 Subject: [PATCH 04/12] Add unit tests for the crash memory reserve --- tests/unit/CMakeLists.txt | 1 + tests/unit/test_crash_reserve.c | 67 +++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 4 ++ 3 files changed, 72 insertions(+) create mode 100644 tests/unit/test_crash_reserve.c diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 5c017bc0d..2869a2f01 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(sentry_test_unit test_cache.c test_client_report.c test_consent.c + test_crash_reserve.c test_concurrency.c test_embedded_info.c test_envelopes.c diff --git a/tests/unit/test_crash_reserve.c b/tests/unit/test_crash_reserve.c new file mode 100644 index 000000000..1598b47e9 --- /dev/null +++ b/tests/unit/test_crash_reserve.c @@ -0,0 +1,67 @@ +#include "sentry_crash_reserve.h" +#include "sentry_testsupport.h" + +#if defined(SENTRY_PLATFORM_WINDOWS) \ + || (defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_PS)) +# define CRASH_RESERVE_SUPPORTED +#endif + +SENTRY_TEST(crash_reserve_alloc_release) +{ +#ifndef CRASH_RESERVE_SUPPORTED + SKIP_TEST(); +#else + sentry__crash_reserve_alloc(64 * 1024); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 64 * 1024); + + sentry__crash_reserve_release(); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); + + // releasing again is a no-op + sentry__crash_reserve_release(); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); +#endif +} + +SENTRY_TEST(crash_reserve_disabled) +{ + sentry__crash_reserve_alloc(0); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); + sentry__crash_reserve_release(); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); +} + +SENTRY_TEST(crash_reserve_init_close) +{ +#ifndef CRASH_RESERVE_SUPPORTED + SKIP_TEST(); +#else + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_crash_memory_reserve(options, 128 * 1024); + sentry_init(options); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 128 * 1024); + + sentry_close(); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); + + // the build setting defaults to no reserve + SENTRY_TEST_OPTIONS_NEW(default_options); + sentry_init(default_options); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); + sentry_close(); +#endif +} + +SENTRY_TEST(crash_reserve_realloc) +{ +#ifndef CRASH_RESERVE_SUPPORTED + SKIP_TEST(); +#else + sentry__crash_reserve_alloc(64 * 1024); + sentry__crash_reserve_alloc(256 * 1024); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 256 * 1024); + + sentry__crash_reserve_release(); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index b9dcad661..417366f51 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -142,6 +142,10 @@ XX(crash_context_null_options) XX(crash_context_options_propagation) XX(crash_context_transport_fields) XX(crash_marker) +XX(crash_reserve_alloc_release) +XX(crash_reserve_disabled) +XX(crash_reserve_init_close) +XX(crash_reserve_realloc) XX(crashed_last_run) XX(custom_logger) XX(daemon_adopts_existing_run) From d560d2535560255738002474842aa28d44a4db79 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 12:23:01 +0200 Subject: [PATCH 05/12] Test crash capture after running out of memory Add an oom-exhaust fixture command that keeps allocating smaller blocks under a Job Object limit until almost no memory is left, and check that inproc and breakpad still capture the crash with a memory reserve. Without the reserve, breakpad writes an empty minidump. --- tests/fixtures/integration.c | 41 +++++++++++++++++++++++++++++++- tests/test_integration_stdout.py | 33 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/integration.c b/tests/fixtures/integration.c index 11d8dbb49..dc900f2ac 100644 --- a/tests/fixtures/integration.c +++ b/tests/fixtures/integration.c @@ -586,7 +586,7 @@ trigger_stack_overflow() } static void -trigger_oom(void) +limit_process_memory(void) { #ifdef SENTRY_PLATFORM_WINDOWS // Optionally limit process memory to trigger OOM faster. @@ -610,6 +610,12 @@ trigger_oom(void) } } #endif +} + +static void +trigger_oom(void) +{ + limit_process_memory(); size_t count = 1024; for (;;) { @@ -631,6 +637,32 @@ trigger_oom(void) } } +static void +trigger_oom_exhaust(void) +{ + limit_process_memory(); + + // keep allocating smaller blocks until almost no memory is left + for (size_t size = 64 * 1024 * 1024; size >= 64 * 1024; size /= 2) { + for (;;) { +#ifdef SENTRY_PLATFORM_WINDOWS + void *p = HeapAlloc(GetProcessHeap(), 0, size); +#else + void *p = malloc(size); +#endif + if (!p) { + break; + } + memset(p, 1, size); + } + } +#ifdef SENTRY_PLATFORM_WINDOWS + RaiseException(0xc000, 0, 0, NULL); +#else + *((volatile int *)3) = 1; +#endif +} + static sentry_value_t create_debug_crumb(const char *message) { @@ -838,6 +870,10 @@ main(int argc, char **argv) sentry_options_set_on_crash(options, on_crash_callback, NULL); } + if (has_arg(argc, argv, "crash-memory-reserve")) { + sentry_options_set_crash_memory_reserve(options, 4 * 1024 * 1024); + } + if (has_arg(argc, argv, "on-crashed-last-run")) { sentry_options_set_on_crashed_last_run( options, on_crashed_last_run_callback, NULL); @@ -1378,6 +1414,9 @@ main(int argc, char **argv) if (has_arg(argc, argv, "oom")) { trigger_oom(); } + if (has_arg(argc, argv, "oom-exhaust")) { + trigger_oom_exhaust(); + } #if defined(SENTRY_PLATFORM_WINDOWS) && !defined(__MINGW32__) \ && !defined(__MINGW64__) if (has_arg(argc, argv, "fastfail")) { diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index ba6326377..0d0900f81 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -321,6 +321,39 @@ def test_abort_stdout(cmake, backend): pytest.fail(f"unsupported backend: {backend}") +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="test needs breakpad backend" + ), + ), + ], +) +@pytest.mark.skipif( + sys.platform != "win32", reason="test needs a Job Object memory limit" +) +def test_oom_crash_memory_reserve_stdout(cmake, backend): + """Test that a crash is captured after the process ran out of memory.""" + tmp_path, output = run_stdout_for( + backend, + cmake, + ["crash-memory-reserve", "oom-exhaust"], + env=dict(os.environ, SENTRY_TEST_OOM_LIMIT="256"), + ) + + envelope = Envelope.deserialize(output) + + if backend == "inproc": + assert_inproc_crash(envelope) + else: + assert_minidump(envelope) + assert_breakpad_crash(envelope) + + @pytest.mark.skipif(is_qemu, reason="unreliable under qemu-user") def test_inproc_crash_stdout_before_send(cmake): tmp_path, output = run_crash_stdout_for("inproc", cmake, ["before-send"]) From 61b3277d58199a561efb427007ab10045ebddeec Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 12:32:14 +0200 Subject: [PATCH 06/12] Add changelog entry for the crash memory reserve --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9acb5ad1..7f9aeda80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Features**: + +- Add an opt-in memory reserve that is released when a crash is handled, so crashes are still captured after running out of memory. ([#2274](https://github.com/getsentry/sentry-native/pull/2274)) + **Fixes**: - Events captured while a crash is being handled, such as from the `on_crash` hook, are no longer dropped by `sample_rate`, matching the crash itself. ([#2269](https://github.com/getsentry/sentry-native/pull/2269)) From ef768e2df57b06bfd46dd719163367bda33e45e0 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 13:34:52 +0200 Subject: [PATCH 07/12] Check the reserve against the build default in unit tests --- external/libunwindstack-ndk | 2 +- tests/unit/test_crash_reserve.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external/libunwindstack-ndk b/external/libunwindstack-ndk index d81a291bf..284202fb1 160000 --- a/external/libunwindstack-ndk +++ b/external/libunwindstack-ndk @@ -1 +1 @@ -Subproject commit d81a291bf6487f5c00d256d0084242d1cc1df2fd +Subproject commit 284202fb1e42dbeba6598e26ced2e1ec404eecd1 diff --git a/tests/unit/test_crash_reserve.c b/tests/unit/test_crash_reserve.c index 1598b47e9..d221d4ed2 100644 --- a/tests/unit/test_crash_reserve.c +++ b/tests/unit/test_crash_reserve.c @@ -44,10 +44,10 @@ SENTRY_TEST(crash_reserve_init_close) sentry_close(); TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); - // the build setting defaults to no reserve SENTRY_TEST_OPTIONS_NEW(default_options); sentry_init(default_options); - TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), 0); + TEST_CHECK_INT_EQUAL(sentry__crash_reserve_size(), + (size_t)SENTRY_CRASH_MEMORY_RESERVE * 1024); sentry_close(); #endif } From c6643ac583e96574fd14b62199356d1829a61b72 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Fri, 30 Oct 2026 13:42:21 +0200 Subject: [PATCH 08/12] Limit the out-of-memory test to breakpad inproc walks the stack with dbghelp, which loads symbols on 32-bit and needs more memory than the test reserves in Debug builds. --- tests/test_integration_stdout.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/tests/test_integration_stdout.py b/tests/test_integration_stdout.py index 0d0900f81..cc7651dbb 100644 --- a/tests/test_integration_stdout.py +++ b/tests/test_integration_stdout.py @@ -321,25 +321,14 @@ def test_abort_stdout(cmake, backend): pytest.fail(f"unsupported backend: {backend}") -@pytest.mark.parametrize( - "backend", - [ - "inproc", - pytest.param( - "breakpad", - marks=pytest.mark.skipif( - not has_breakpad, reason="test needs breakpad backend" - ), - ), - ], -) +@pytest.mark.skipif(not has_breakpad, reason="test needs breakpad backend") @pytest.mark.skipif( sys.platform != "win32", reason="test needs a Job Object memory limit" ) -def test_oom_crash_memory_reserve_stdout(cmake, backend): +def test_breakpad_oom_crash_memory_reserve_stdout(cmake): """Test that a crash is captured after the process ran out of memory.""" tmp_path, output = run_stdout_for( - backend, + "breakpad", cmake, ["crash-memory-reserve", "oom-exhaust"], env=dict(os.environ, SENTRY_TEST_OOM_LIMIT="256"), @@ -347,11 +336,8 @@ def test_oom_crash_memory_reserve_stdout(cmake, backend): envelope = Envelope.deserialize(output) - if backend == "inproc": - assert_inproc_crash(envelope) - else: - assert_minidump(envelope) - assert_breakpad_crash(envelope) + assert_minidump(envelope) + assert_breakpad_crash(envelope) @pytest.mark.skipif(is_qemu, reason="unreliable under qemu-user") From 61fe0316bcc8e88a67d53da9bf4ed796d7cb404c Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 3 Nov 2026 12:21:25 +0200 Subject: [PATCH 09/12] Restore the libunwindstack-ndk submodule --- external/libunwindstack-ndk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/libunwindstack-ndk b/external/libunwindstack-ndk index 284202fb1..d81a291bf 160000 --- a/external/libunwindstack-ndk +++ b/external/libunwindstack-ndk @@ -1 +1 @@ -Subproject commit 284202fb1e42dbeba6598e26ced2e1ec404eecd1 +Subproject commit d81a291bf6487f5c00d256d0084242d1cc1df2fd From dda303ff33ac841cd08e9f28cdad2f40ee45a2a2 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 3 Nov 2026 12:21:26 +0200 Subject: [PATCH 10/12] Store the crash memory reserve size inside the mapping --- src/sentry_crash_reserve.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sentry_crash_reserve.c b/src/sentry_crash_reserve.c index 1103f9178..494a57c25 100644 --- a/src/sentry_crash_reserve.c +++ b/src/sentry_crash_reserve.c @@ -15,16 +15,15 @@ #endif static void *volatile g_reserve = NULL; -static size_t g_reserve_size = 0; #ifdef SENTRY_CRASH_RESERVE_SUPPORTED static void * -take_reserve(void) +swap_reserve(void *ptr) { # ifdef SENTRY_PLATFORM_WINDOWS - return InterlockedExchangePointer((void *volatile *)&g_reserve, NULL); + return InterlockedExchangePointer((void *volatile *)&g_reserve, ptr); # else - return __atomic_exchange_n(&g_reserve, NULL, __ATOMIC_SEQ_CST); + return __atomic_exchange_n(&g_reserve, ptr, __ATOMIC_SEQ_CST); # endif } @@ -58,9 +57,9 @@ sentry__crash_reserve_alloc(size_t size) } // touch the pages so they are backed by memory memset(ptr, 1, size); - - g_reserve_size = size; - g_reserve = ptr; + // the size travels with the mapping + *(size_t *)ptr = size; + swap_reserve(ptr); SENTRY_DEBUGF("reserved %zu bytes of crash handler memory", size); #else SENTRY_WARN("the crash memory reserve is not supported on this platform"); @@ -71,14 +70,14 @@ void sentry__crash_reserve_release(void) { #ifdef SENTRY_CRASH_RESERVE_SUPPORTED - void *ptr = take_reserve(); + void *ptr = swap_reserve(NULL); if (!ptr) { return; } # ifdef SENTRY_PLATFORM_WINDOWS VirtualFree(ptr, 0, MEM_RELEASE); # else - munmap(ptr, g_reserve_size); + munmap(ptr, *(size_t *)ptr); # endif #endif } @@ -86,5 +85,6 @@ sentry__crash_reserve_release(void) size_t sentry__crash_reserve_size(void) { - return g_reserve ? g_reserve_size : 0; + void *ptr = g_reserve; + return ptr ? *(size_t *)ptr : 0; } From 5aeb6dedfd14a214c046613ca4223e8129738217 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 3 Nov 2026 13:54:55 +0200 Subject: [PATCH 11/12] Update src/sentry_crash_reserve.c Co-authored-by: J-P Nurmi --- src/sentry_crash_reserve.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sentry_crash_reserve.c b/src/sentry_crash_reserve.c index 494a57c25..8b322ab04 100644 --- a/src/sentry_crash_reserve.c +++ b/src/sentry_crash_reserve.c @@ -58,7 +58,9 @@ sentry__crash_reserve_alloc(size_t size) // touch the pages so they are backed by memory memset(ptr, 1, size); // the size travels with the mapping - *(size_t *)ptr = size; + size_t *hdr = ptr; + hdr[0] = size; + hdr[1] = ~size; swap_reserve(ptr); SENTRY_DEBUGF("reserved %zu bytes of crash handler memory", size); #else From cd2996e7d1a70bcd23bc32446c726e099e540c26 Mon Sep 17 00:00:00 2001 From: Ivan Tustanivskyi Date: Tue, 3 Nov 2026 13:55:07 +0200 Subject: [PATCH 12/12] Update src/sentry_crash_reserve.c Co-authored-by: J-P Nurmi --- src/sentry_crash_reserve.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sentry_crash_reserve.c b/src/sentry_crash_reserve.c index 8b322ab04..a9b7e7dd0 100644 --- a/src/sentry_crash_reserve.c +++ b/src/sentry_crash_reserve.c @@ -79,7 +79,11 @@ sentry__crash_reserve_release(void) # ifdef SENTRY_PLATFORM_WINDOWS VirtualFree(ptr, 0, MEM_RELEASE); # else - munmap(ptr, *(size_t *)ptr); + size_t *hdr = ptr; + // don't let a corrupted size unmap unrelated memory + if (hdr[1] == ~hdr[0]) { + munmap(ptr, hdr[0]); + } # endif #endif }