Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a211f3e
feat(native): capture WER custom metadata
jpnurmi May 11, 2026
df51135
changelog
jpnurmi May 27, 2026
0f60503
review findings
jpnurmi May 27, 2026
bf8b0f7
make format
jpnurmi May 27, 2026
fa59321
re-remove wait-loop
jpnurmi May 27, 2026
d65d260
sentry__value_to_json
jpnurmi May 27, 2026
e8f51f6
fix test_native_abort
jpnurmi May 27, 2026
fc07b0b
fix review finding
jpnurmi May 27, 2026
315f250
fix: always notify daemon in crash handler WER path
jpnurmi May 28, 2026
765fc5d
fix: set exception_pointers to NULL in crash handler WER path
jpnurmi May 29, 2026
bf427a8
DEBUG
jpnurmi May 29, 2026
54afb7b
++
jpnurmi May 29, 2026
5430fe0
wait for daemon capture
jpnurmi May 29, 2026
3ab8f64
cleanup
jpnurmi May 29, 2026
f60abe1
round-trip
jpnurmi May 29, 2026
287eeb1
partial revert
jpnurmi May 29, 2026
7cbdb44
integration test
jpnurmi May 29, 2026
7f1687c
local wer_report_id
jpnurmi May 29, 2026
985abed
DEBUG
jpnurmi Jun 1, 2026
4d9c72e
use_wer -> SENTRY_CRASH_STATE_PROCESSING
jpnurmi Jun 1, 2026
1eebc98
earlier
jpnurmi Jun 1, 2026
eaa51dc
should_use_wer
jpnurmi Jun 1, 2026
57185c4
re-enable all tests
jpnurmi Jun 1, 2026
d535b7a
sentry_wer_sync_mode_t
jpnurmi Jun 1, 2026
e125ce5
WIP
jpnurmi Jun 1, 2026
678581b
WIP++
jpnurmi Jun 1, 2026
9dc23d7
Revert "DEBUG"
jpnurmi Jun 1, 2026
d137041
sync wer custom metadata as tags
jpnurmi Jun 1, 2026
b088def
debug--
jpnurmi Jun 1, 2026
f38c953
cas
jpnurmi Jun 1, 2026
b699bc9
!wkey || !wvalue
jpnurmi Jun 1, 2026
cfb5df9
update test
jpnurmi Jun 1, 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 @@ -9,6 +9,7 @@
- Add a `transfer_timeout` option for SDK-managed HTTP transports. ([#1741](https://github.com/getsentry/sentry-native/pull/1741))
- Apple: use `os_sync_wait_on_address` for the level-triggered waitable flag in the batcher on modern macOS(14.4+) and iOS(17.4+). ([#1765](https://github.com/getsentry/sentry-native/pull/1765))
- Native/macOS: add thread names. ([#1766](https://github.com/getsentry/sentry-native/pull/1766))
- Native/Windows: capture WER custom metadata. ([#1760](https://github.com/getsentry/sentry-native/pull/1760))

**Fixes**:

Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,7 @@ elseif(SENTRY_BACKEND_NATIVE)
${PROJECT_SOURCE_DIR}/src/backends/native
)
target_link_libraries(sentry-wer PRIVATE wer)
target_link_libraries(sentry PRIVATE wer)
set_property(TARGET sentry-wer PROPERTY PREFIX "") # ensure MINGW doesn't prefix "lib" to dll name
set_property(TARGET sentry-wer PROPERTY DEBUG_POSTFIX "") # prevent CMAKE_DEBUG_POSTFIX from being applied
if(SENTRY_BUILD_RUNTIMESTATIC AND MSVC)
Expand Down Expand Up @@ -868,6 +869,9 @@ elseif(SENTRY_BACKEND_NATIVE)

# Link same libraries as sentry
target_link_libraries(sentry-crash PRIVATE ${_SENTRY_PLATFORM_LIBS})
if(WIN32)
target_link_libraries(sentry-crash PRIVATE wer)
endif()
if(APPLE)
find_library(COREFOUNDATION_LIBRARY CoreFoundation REQUIRED)
find_library(SECURITY_LIBRARY Security REQUIRED)
Expand Down
20 changes: 20 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ get_arg_value(int argc, char **argv, const char *arg)

#if defined(SENTRY_PLATFORM_WINDOWS) && !defined(__MINGW32__) \
&& !defined(__MINGW64__)
# include <werapi.h>

int
call_rffe_many_times()
Expand Down Expand Up @@ -879,6 +880,25 @@ main(int argc, char **argv)
options, SENTRY_CRASH_UPLOAD_MODE_ASYNC);
}

#ifdef SENTRY_PLATFORM_WINDOWS
if (has_arg(argc, argv, "wer-sync-mode")) {
const char *arg = get_arg_value(argc, argv, "wer-sync-mode");
if (arg != NULL) {
sentry_wer_sync_mode_t mode = SENTRY_WER_SYNC_MODE_NONE;
if (strcmp(arg, "from-wer") == 0) {
mode = SENTRY_WER_SYNC_MODE_FROM_WER;
} else if (strcmp(arg, "to-wer") == 0) {
mode = SENTRY_WER_SYNC_MODE_TO_WER;
} else if (strcmp(arg, "from-to-wer") == 0) {
mode = (sentry_wer_sync_mode_t)(SENTRY_WER_SYNC_MODE_FROM_WER
| SENTRY_WER_SYNC_MODE_TO_WER);
}
sentry_options_set_wer_sync_mode(options, mode);
WerRegisterCustomMetadata(L"SentryWer", L"value from WER");
}
}
#endif

// E2E test mode: generate unique test ID for event correlation
char e2e_test_id[37] = { 0 };
if (has_arg(argc, argv, "e2e-test")) {
Expand Down
21 changes: 21 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,15 @@ typedef enum {
SENTRY_CRASH_UPLOAD_MODE_ASYNC = 1,
} sentry_crash_upload_mode_t;

/**
* Synchronization mode for Windows Error Reporting (WER) interop.
*/
typedef enum sentry_wer_sync_mode_t {
SENTRY_WER_SYNC_MODE_NONE = 0,
SENTRY_WER_SYNC_MODE_FROM_WER = 1 << 0,
SENTRY_WER_SYNC_MODE_TO_WER = 1 << 1,
} sentry_wer_sync_mode_t;

/**
* Controls if and when envelopes are kept in the persistent cache.
*/
Expand Down Expand Up @@ -1922,6 +1931,18 @@ SENTRY_API void sentry_options_set_crash_upload_mode(
SENTRY_API sentry_crash_upload_mode_t sentry_options_get_crash_upload_mode(
const sentry_options_t *opts);

/**
* Sets the Windows Error Reporting (WER) synchronization mode.
*
* This setting controls how data is synced between WER and Sentry. The value is
* bitmask and can combine flags from `sentry_wer_sync_mode_t`.
*
* This setting only has an effect when using the `native` backend on Windows.
* Default is `SENTRY_WER_SYNC_MODE_NONE`.
*/
SENTRY_API void sentry_options_set_wer_sync_mode(
sentry_options_t *opts, sentry_wer_sync_mode_t mode);

/**
* Enables a wait for the crash report upload to be finished before shutting
* down. This is disabled by default.
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ elseif(SENTRY_BACKEND_NATIVE)
elseif(WIN32)
sentry_target_sources_cwd(sentry
backends/native/minidump/sentry_minidump_windows.c
backends/native/sentry_wer_report.c
backends/native/sentry_wer_report.h
)
endif()

Expand Down
6 changes: 6 additions & 0 deletions src/backends/native/sentry_crash_context.h
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ typedef struct {
// Additional thread contexts
DWORD num_threads;
sentry_thread_context_windows_t threads[SENTRY_CRASH_MAX_THREADS];

bool wer_enabled;
char wer_report_id[64];
} sentry_crash_platform_windows_t;

typedef struct {
Expand Down Expand Up @@ -290,6 +293,9 @@ typedef struct {
uint64_t shutdown_timeout;
uint64_t transfer_timeout;
bool system_crash_reporter_enabled;
int wer_sync_mode; // sentry_wer_sync_mode_t

char crash_event_id[37];

// Atomic user consent (sentry_user_consent_t), updated whenever user
// consent changes so the daemon can honor it at crash time.
Expand Down
73 changes: 68 additions & 5 deletions src/backends/native/sentry_crash_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
#include "sentry_utils.h"
#include "sentry_uuid.h"
#include "sentry_value.h"
#if defined(SENTRY_PLATFORM_WINDOWS)
# include "sentry_wer_report.h"
#endif
#include "transports/sentry_disk_transport.h"

#include <limits.h>
Expand Down Expand Up @@ -2232,6 +2235,41 @@ build_stacktrace_from_ctx(const sentry_crash_context_t *ctx)
return build_stacktrace_for_thread(ctx, SIZE_MAX);
}

#if defined(SENTRY_PLATFORM_WINDOWS)
static bool
sync_wer_data(sentry_value_t event, const sentry_crash_context_t *ctx)
{
if (!ctx->platform.wer_enabled
|| sentry__string_empty(ctx->platform.wer_report_id)
|| (ctx->wer_sync_mode & SENTRY_WER_SYNC_MODE_FROM_WER) == 0) {
return false;
}
Comment thread
cursor[bot] marked this conversation as resolved.

char wer_report_id[sizeof(ctx->platform.wer_report_id)];
memcpy(
wer_report_id, ctx->platform.wer_report_id, sizeof(wer_report_id) - 1);
wer_report_id[sizeof(wer_report_id) - 1] = '\0';

sentry_value_t wer_report = sentry__wer_report_lookup(ctx->crash_event_id);
if (!sentry_value_is_null(wer_report)) {
sentry__value_merge_objects(event, wer_report);
sentry_value_decref(wer_report);
}

sentry_value_t wer_context = sentry_value_new_object();
sentry_value_set_by_key(
wer_context, "report_id", sentry_value_new_string(wer_report_id));

sentry_value_t contexts = sentry_value_get_by_key(event, "contexts");
if (sentry_value_get_type(contexts) != SENTRY_VALUE_TYPE_OBJECT) {
contexts = sentry_value_new_object();
sentry_value_set_by_key(event, "contexts", contexts);
}
sentry_value_set_by_key(contexts, "wer", wer_context);
return true;
}
#endif

/**
* Build native crash event with exception, mechanism, and debug_meta
*
Expand Down Expand Up @@ -2262,6 +2300,11 @@ build_native_crash_event(
event = sentry_value_new_event();
}

if (!sentry__string_empty(ctx->crash_event_id)) {
sentry_value_set_by_key(
event, "event_id", sentry_value_new_string(ctx->crash_event_id));
}

// Set platform to native
sentry_value_set_by_key(
event, "platform", sentry_value_new_string("native"));
Expand Down Expand Up @@ -2582,6 +2625,10 @@ build_native_crash_event(
SENTRY_WARN("No modules captured - debug_meta.images will be empty!");
}

#if defined(SENTRY_PLATFORM_WINDOWS)
sync_wer_data(event, ctx);
#endif

return event;
}

Expand Down Expand Up @@ -2850,18 +2897,28 @@ write_envelope_with_minidump(const sentry_options_t *options,
// Read event JSON data
size_t event_size = 0;
char *event_json = NULL;
char *event_id = NULL;
const char *event_id = ctx->crash_event_id;
sentry_path_t *ev_path = sentry__path_from_str(event_msgpack_path);
if (ev_path) {
event_json = sentry__path_read_to_buffer(ev_path, &event_size);
sentry__path_free(ev_path);
#if defined(SENTRY_PLATFORM_WINDOWS)
if (event_json && event_size > 0) {
sentry_value_t event
= sentry__value_from_json(event_json, event_size);
event_id = sentry__string_clone(sentry_value_as_string(
sentry_value_get_by_key(event, "event_id")));
if (!sentry_value_is_null(event) && sync_wer_data(event, ctx)) {
size_t new_event_size = 0;
char *new_event_json
= sentry__value_to_json(event, &new_event_size);
if (new_event_json) {
sentry_free(event_json);
event_json = new_event_json;
event_size = new_event_size;
}
}
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WER context dropped on JSON failure

Low Severity

In minidump-only envelope construction, after add_wer_context mutates the parsed event, a failed sentry__value_to_json leaves the original on-disk JSON unchanged, so the uploaded envelope can omit the WER context even though enrichment succeeded in memory.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9dc23d7. Configure here.

sentry_value_decref(event);
}
#endif
}

// Open envelope file for writing
Expand All @@ -2878,7 +2935,6 @@ write_envelope_with_minidump(const sentry_options_t *options,
if (fd < 0) {
SENTRY_WARN("Failed to open envelope file for writing");
sentry_free(event_json);
sentry_free(event_id);
return false;
}

Expand All @@ -2899,7 +2955,6 @@ write_envelope_with_minidump(const sentry_options_t *options,
} else {
header_len = snprintf(header_buf, sizeof(header_buf), "{}\n");
}
sentry_free(event_id);
if (header_len > 0 && header_len < (int)sizeof(header_buf)) {
#if defined(SENTRY_PLATFORM_UNIX)
if (write(fd, header_buf, header_len) != header_len) {
Expand Down Expand Up @@ -3106,6 +3161,12 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
// Mark as processing
sentry__atomic_store(&ctx->state, SENTRY_CRASH_STATE_PROCESSING);
SENTRY_DEBUG("Marked state as PROCESSING");
#if defined(SENTRY_PLATFORM_WINDOWS)
SENTRY_DEBUGF(
"crash-daemon: processing crash pid=%lu tid=%lu mode=%d wer=%d",
(unsigned long)ctx->crashed_pid, (unsigned long)ctx->crashed_tid,
ctx->crash_reporting_mode, ctx->platform.wer_enabled ? 1 : 0);
#endif

// Check crash reporting mode
int mode = ctx->crash_reporting_mode;
Expand All @@ -3117,6 +3178,8 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc)
// Mode 2 (NATIVE_WITH_MINIDUMP): Write minidump
bool need_minidump = (mode == SENTRY_CRASH_REPORTING_MODE_MINIDUMP
|| mode == SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP);
SENTRY_DEBUGF("crash-daemon: need_minidump=%d minidump_mode=%d",
need_minidump ? 1 : 0, ctx->minidump_mode);

// Determine if we use native stacktrace mode
// Mode 0: Use minidump-only envelope (existing behavior)
Expand Down
87 changes: 69 additions & 18 deletions src/backends/native/sentry_crash_handler.c
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,40 @@ static LPTOP_LEVEL_EXCEPTION_FILTER g_previous_filter = NULL;

static LONG WINAPI crash_exception_filter(EXCEPTION_POINTERS *exception_info);

static bool
should_use_wer(DWORD exception_code)
{
// WER callback path is primarily needed for fail-fast style crashes.
// Regular SEH crashes should stay on the direct path so the daemon can
// use live exception pointers for minidump capture.
if (exception_code == STATUS_STACK_BUFFER_OVERRUN) {
return true;
}
# ifdef STATUS_FAIL_FAST_EXCEPTION
if (exception_code == STATUS_FAIL_FAST_EXCEPTION) {
return true;
}
# endif
return false;
}

static void
wait_for_daemon_capture(sentry_crash_context_t *ctx)
{
bool processing_started = false;
int elapsed_ms = 0;
while (elapsed_ms < SENTRY_CRASH_HANDLER_WAIT_TIMEOUT_MS) {
long state = sentry__atomic_fetch(&ctx->state);
if (state == SENTRY_CRASH_STATE_PROCESSING && !processing_started) {
processing_started = true;
} else if (state >= SENTRY_CRASH_STATE_CAPTURED) {
break;
}
Sleep(SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS);
elapsed_ms += SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS;
}
}

static void
crash_sigabrt_handler(EXCEPTION_POINTERS *exception_pointers)
{
Expand Down Expand Up @@ -987,6 +1021,21 @@ crash_exception_filter(EXCEPTION_POINTERS *exception_info)

sentry_crash_context_t *ctx = ipc->shmem;

DWORD exception_code = exception_info->ExceptionRecord->ExceptionCode;
bool use_wer = ctx->platform.wer_enabled
&& exception_code != STATUS_FATAL_APP_EXIT
&& should_use_wer(exception_code);

// In WER mode, move out of READY as early as possible so the
// out-of-process WER callback cannot claim and wake the daemon before
// sentry_handle_exception writes the crash event.
// Use compare-and-swap to avoid overwriting CRASHED if the WER callback
// already claimed the crash.
if (use_wer) {
sentry__atomic_compare_swap(
&ctx->state, SENTRY_CRASH_STATE_READY, SENTRY_CRASH_STATE_PROCESSING);
}
Comment thread
cursor[bot] marked this conversation as resolved.

// Fill crash context
ctx->crashed_pid = GetCurrentProcessId();
ctx->crashed_tid = GetCurrentThreadId();
Expand Down Expand Up @@ -1017,6 +1066,25 @@ crash_exception_filter(EXCEPTION_POINTERS *exception_info)
sentry_uctx.exception_ptrs = *exception_info;
sentry_handle_exception(&sentry_uctx);

// STATUS_FATAL_APP_EXIT (from SIGABRT/abort()) goes through the signal
// handler, not SEH. WER's runtime exception module is never invoked, so
// EXCEPTION_CONTINUE_SEARCH would be ignored and the process terminated
// without notifying the daemon. Fall through to the direct claim path.
if (use_wer) {
// Claim crash and notify daemon. WER may or may not invoke
// the sentry-wer callback (e.g. CI runners without WER), so
// we must cover both cases. The sentry-wer callback will
// additionally write the WER report ID to shared memory.
// Set exception_pointers to NULL so the daemon uses the
// already-copied exception_record/context from shared memory
// (ClientPointers=FALSE), since the crashing process will be
// terminated before the daemon can read its address space.
ctx->platform.exception_pointers = NULL;
sentry__atomic_store(&ctx->state, SENTRY_CRASH_STATE_CRASHED);
sentry__crash_ipc_notify(ipc);
return EXCEPTION_CONTINUE_SEARCH;
Comment thread
cursor[bot] marked this conversation as resolved.
}

bool swap_result = sentry__atomic_compare_swap(
&ctx->state, SENTRY_CRASH_STATE_READY, SENTRY_CRASH_STATE_CRASHED);

Expand All @@ -1026,24 +1094,7 @@ crash_exception_filter(EXCEPTION_POINTERS *exception_info)

// Wait for daemon to finish processing (keep process alive for
// minidump)
bool processing_started = false;
int elapsed_ms = 0;
while (elapsed_ms < SENTRY_CRASH_HANDLER_WAIT_TIMEOUT_MS) {
long state = sentry__atomic_fetch(&ctx->state);
if (state == SENTRY_CRASH_STATE_PROCESSING && !processing_started) {
// Daemon started processing (no logging - exception filter
// context)
processing_started = true;
} else if (state >= SENTRY_CRASH_STATE_CAPTURED) {
// Daemon captured crash data (no logging - exception filter
// context)
break;
}
Sleep(SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS);
elapsed_ms += SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS;
}

// Timeout or completion (no logging - exception filter context)
wait_for_daemon_capture(ctx);
}

// Continue to default handler (which will terminate the process)
Expand Down
Loading
Loading