diff --git a/CHANGELOG.md b/CHANGELOG.md index 2438d60fe..439635d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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**: diff --git a/CMakeLists.txt b/CMakeLists.txt index bacef2228..af50f3d5f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) @@ -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) diff --git a/examples/example.c b/examples/example.c index d3a083280..b209ce260 100644 --- a/examples/example.c +++ b/examples/example.c @@ -376,6 +376,7 @@ get_arg_value(int argc, char **argv, const char *arg) #if defined(SENTRY_PLATFORM_WINDOWS) && !defined(__MINGW32__) \ && !defined(__MINGW64__) +# include int call_rffe_many_times() @@ -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")) { diff --git a/include/sentry.h b/include/sentry.h index 25416813e..77b2daa75 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -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. */ @@ -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. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 317f7b3ea..9e3b3ad69 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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() diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 40f43b4f1..cdbcf8640 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -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 { @@ -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. diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 308dafeac..2329b8c8f 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -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 @@ -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; + } + + 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 * @@ -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")); @@ -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; } @@ -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; + } + } sentry_value_decref(event); } +#endif } // Open envelope file for writing @@ -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; } @@ -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) { @@ -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; @@ -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) diff --git a/src/backends/native/sentry_crash_handler.c b/src/backends/native/sentry_crash_handler.c index 3d6658a7c..e0bd4490d 100644 --- a/src/backends/native/sentry_crash_handler.c +++ b/src/backends/native/sentry_crash_handler.c @@ -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) { @@ -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); + } + // Fill crash context ctx->crashed_pid = GetCurrentProcessId(); ctx->crashed_tid = GetCurrentThreadId(); @@ -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; + } + bool swap_result = sentry__atomic_compare_swap( &ctx->state, SENTRY_CRASH_STATE_READY, SENTRY_CRASH_STATE_CRASHED); @@ -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) diff --git a/src/backends/native/sentry_wer.c b/src/backends/native/sentry_wer.c index 37542528c..798f1f3db 100644 --- a/src/backends/native/sentry_wer.c +++ b/src/backends/native/sentry_wer.c @@ -6,14 +6,6 @@ #include #include -#ifndef STATUS_FAIL_FAST_EXCEPTION -# define STATUS_FAIL_FAST_EXCEPTION ((DWORD)0xC0000602) -#endif - -#ifndef STATUS_STACK_BUFFER_OVERRUN -# define STATUS_STACK_BUFFER_OVERRUN ((DWORD)0xC0000409) -#endif - static BOOL is_fatal_wer_exception(const WER_RUNTIME_EXCEPTION_INFORMATION *info) { @@ -38,11 +30,27 @@ is_fatal_wer_exception(const WER_RUNTIME_EXCEPTION_INFORMATION *info) return ((const WER_RUNTIME_EXCEPTION_INFORMATION_19041 *)info)->bIsFatal; } -static BOOL -is_native_wer_exception(DWORD code) +static PCWSTR +get_report_id(const WER_RUNTIME_EXCEPTION_INFORMATION *info) { - return code == STATUS_FAIL_FAST_EXCEPTION - || code == STATUS_STACK_BUFFER_OVERRUN; + typedef struct { + DWORD dwSize; + HANDLE hProcess; + HANDLE hThread; + EXCEPTION_RECORD exceptionRecord; + CONTEXT context; + PCWSTR pwszReportId; + } WER_RUNTIME_EXCEPTION_INFORMATION_WITH_REPORT_ID; + + if (!info + || info->dwSize + <= offsetof(WER_RUNTIME_EXCEPTION_INFORMATION_WITH_REPORT_ID, + pwszReportId)) { + return NULL; + } + + return ((const WER_RUNTIME_EXCEPTION_INFORMATION_WITH_REPORT_ID *)info) + ->pwszReportId; } static BOOL @@ -113,9 +121,7 @@ static BOOL process_wer_exception( PVOID context, const WER_RUNTIME_EXCEPTION_INFORMATION *exception_info) { - if (!exception_info || !is_fatal_wer_exception(exception_info) - || !is_native_wer_exception( - exception_info->exceptionRecord.ExceptionCode)) { + if (!exception_info || !is_fatal_wer_exception(exception_info)) { return FALSE; } @@ -132,6 +138,16 @@ process_wer_exception( } BOOL claimed = FALSE; + + // Extract WER report ID regardless of who claims the crash, so the + // daemon can include it in the sentry event. + PCWSTR report_id = get_report_id(exception_info); + if (report_id) { + WideCharToMultiByte(CP_UTF8, 0, report_id, -1, + ctx->platform.wer_report_id, + (int)sizeof(ctx->platform.wer_report_id), NULL, NULL); + } + if (InterlockedCompareExchange(&ctx->state, SENTRY_CRASH_STATE_PROCESSING, SENTRY_CRASH_STATE_READY) == SENTRY_CRASH_STATE_READY) { @@ -147,23 +163,13 @@ process_wer_exception( ctx->platform.threads[0].context = exception_info->context; InterlockedExchange(&ctx->state, SENTRY_CRASH_STATE_CRASHED); - if (SetEvent(event)) { - claimed = TRUE; - uint64_t timeout_ms = ctx->shutdown_timeout - ? ctx->shutdown_timeout - : SENTRY_CRASH_HANDLER_WAIT_TIMEOUT_MS; - for (uint64_t waited_ms = 0; waited_ms < timeout_ms; - waited_ms += SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS) { - if (InterlockedCompareExchange(&ctx->state, - SENTRY_CRASH_STATE_DONE, SENTRY_CRASH_STATE_DONE) - >= SENTRY_CRASH_STATE_CAPTURED) { - break; - } - Sleep(SENTRY_CRASH_HANDLER_POLL_INTERVAL_MS); - } - TerminateProcess(exception_info->hProcess, - exception_info->exceptionRecord.ExceptionCode); - } + } + + // Always attempt to signal the daemon. If the crash handler claimed + // the crash first, it already SetEvent'd via sentry__crash_ipc_notify, + // but a redundant SetEvent on an auto-reset event is harmless. + if (SetEvent(event)) { + claimed = TRUE; } CloseHandle(event); @@ -191,10 +197,15 @@ OutOfProcessExceptionEventCallback(PVOID context, (void)event_name_size; (void)signature_count; - *ownership_claimed = FALSE; + if (ownership_claimed) { + *ownership_claimed = FALSE; + } if (process_wer_exception(context, exception_info)) { - *ownership_claimed = TRUE; + if (ownership_claimed) { + *ownership_claimed = TRUE; + } } + return S_OK; } diff --git a/src/backends/native/sentry_wer_report.c b/src/backends/native/sentry_wer_report.c new file mode 100644 index 000000000..1515521a4 --- /dev/null +++ b/src/backends/native/sentry_wer_report.c @@ -0,0 +1,453 @@ +#include "sentry_wer_report.h" + +#include "sentry_alloc.h" +#include "sentry_string.h" + +#include +#include +#include +#include +#include +#include + +static bool +format_wstring(wchar_t *buffer, size_t size, const wchar_t *format, ...) +{ + va_list args; + va_start(args, format); + int written = _vsnwprintf(buffer, size, format, args); + va_end(args); + + if (written < 0 || (size_t)written >= size) { + buffer[0] = L'\0'; + return false; + } + return true; +} + +static bool +read_file(const wchar_t *path, DWORD max_size, char **buffer, DWORD *read) +{ + *buffer = NULL; + *read = 0; + + HANDLE file = CreateFileW(path, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (file == INVALID_HANDLE_VALUE) { + return false; + } + + DWORD size = GetFileSize(file, NULL); + if (size == INVALID_FILE_SIZE || size > max_size) { + CloseHandle(file); + return false; + } + + *buffer = (char *)sentry_malloc((size_t)size); + if (!*buffer) { + CloseHandle(file); + return false; + } + + bool success = ReadFile(file, *buffer, size, read, NULL); + CloseHandle(file); + if (!success) { + sentry_free(*buffer); + *buffer = NULL; + *read = 0; + } + return success; +} + +static bool +decode_buffer( + const char *buffer, size_t size, wchar_t **decoded, const wchar_t **text) +{ + *decoded = NULL; + *text = NULL; + + if (!buffer || size > INT_MAX) { + return false; + } + + if (size >= 2 && (unsigned char)buffer[0] == 0xff + && (unsigned char)buffer[1] == 0xfe) { + size_t wchar_count = (size - 2) / sizeof(wchar_t); + *decoded + = (wchar_t *)sentry_malloc(sizeof(wchar_t) * (wchar_count + 1)); + if (!*decoded) { + return false; + } + memcpy(*decoded, buffer + 2, wchar_count * sizeof(wchar_t)); + (*decoded)[wchar_count] = L'\0'; + *text = *decoded; + return true; + } + + int len = MultiByteToWideChar( + CP_UTF8, MB_ERR_INVALID_CHARS, buffer, (int)size, NULL, 0); + UINT codepage = CP_UTF8; + DWORD flags = MB_ERR_INVALID_CHARS; + if (len <= 0) { + codepage = CP_ACP; + flags = 0; + len = MultiByteToWideChar(codepage, flags, buffer, (int)size, NULL, 0); + } + if (len <= 0) { + return false; + } + + *decoded = (wchar_t *)sentry_malloc(sizeof(wchar_t) * ((size_t)len + 1)); + if (!*decoded) { + return false; + } + + int written = MultiByteToWideChar( + codepage, flags, buffer, (int)size, *decoded, len); + if (written != len) { + sentry_free(*decoded); + *decoded = NULL; + return false; + } + (*decoded)[len] = L'\0'; + *text = *decoded; + return true; +} + +static const wchar_t * +find_wchar_before(const wchar_t *start, const wchar_t *end, wchar_t ch) +{ + while (start < end) { + if (*start == ch) { + return start; + } + start++; + } + return NULL; +} + +static const wchar_t * +wcsstr_before( + const wchar_t *haystack, const wchar_t *needle, const wchar_t *end) +{ + const wchar_t *found = wcsstr(haystack, needle); + size_t needle_len = wcslen(needle); + return found && (!end || found + needle_len <= end) ? found : NULL; +} + +static bool +find_xml_section(const wchar_t *xml, const wchar_t *tag, + const wchar_t **content_start, const wchar_t **content_end) +{ + wchar_t open_tag[64]; + wchar_t close_tag[64]; + if (!format_wstring( + open_tag, sizeof(open_tag) / sizeof(wchar_t), L"<%s>", tag) + || !format_wstring( + close_tag, sizeof(close_tag) / sizeof(wchar_t), L"", tag)) { + return false; + } + + const wchar_t *open = wcsstr(xml, open_tag); + if (!open) { + return false; + } + *content_start = open + wcslen(open_tag); + + const wchar_t *close = wcsstr(*content_start, close_tag); + if (!close) { + return false; + } + + *content_end = close; + return true; +} + +static bool +copy_xml_tag(const wchar_t *xml, const wchar_t *tag, wchar_t *out, size_t len) +{ + if (!xml || !tag || !out || !len) { + return false; + } + + out[0] = L'\0'; + + const wchar_t *start = NULL; + const wchar_t *end = NULL; + if (!find_xml_section(xml, tag, &start, &end) || end <= start) { + return false; + } + + size_t value_len = (size_t)(end - start); + if (value_len >= len) { + value_len = len - 1; + } + wcsncpy(out, start, value_len); + out[value_len] = L'\0'; + return true; +} + +static void +trim_wstr_range(const wchar_t **start, const wchar_t **end) +{ + while (*start < *end + && (**start == L' ' || **start == L'\t' || **start == L'\r' + || **start == L'\n')) { + (*start)++; + } + while (*end > *start + && ((*end)[-1] == L' ' || (*end)[-1] == L'\t' || (*end)[-1] == L'\r' + || (*end)[-1] == L'\n')) { + (*end)--; + } +} + +static void +set_wstr_range(sentry_value_t object, const wchar_t *key_start, size_t key_len, + const wchar_t *value_start, const wchar_t *value_end) +{ + if (key_len == (sizeof(SENTRY_WER_EVENT_ID_KEY_W) / sizeof(wchar_t)) - 1 + && wcsncmp(key_start, SENTRY_WER_EVENT_ID_KEY_W, key_len) == 0) { + return; + } + + trim_wstr_range(&value_start, &value_end); + + char *key = sentry__string_from_wstr_n(key_start, key_len); + char *value = sentry__string_from_wstr_n( + value_start, (size_t)(value_end - value_start)); + if (!sentry__string_empty(key) && value) { + sentry_value_set_by_key(object, key, sentry_value_new_string(value)); + } + sentry_free(key); + sentry_free(value); +} + +static void +set_wstring(sentry_value_t object, const char *key, const wchar_t *value) +{ + if (!value || !value[0]) { + return; + } + + sentry_value_t string = sentry__value_new_string_from_wstr(value); + if (!sentry_value_is_null(string)) { + sentry_value_set_by_key(object, key, string); + } +} + +static void +extract_xml_leaf_tags( + sentry_value_t metadata, const wchar_t *start, const wchar_t *end) +{ + const wchar_t *cursor = start; + while (cursor < end) { + const wchar_t *open = find_wchar_before(cursor, end, L'<'); + if (!open || open + 1 >= end) { + break; + } + + if (open[1] == L'/' || open[1] == L'!' || open[1] == L'?') { + cursor = open + 1; + continue; + } + + const wchar_t *tag_start = open + 1; + const wchar_t *tag_end = tag_start; + while (tag_end < end && *tag_end != L'>' && *tag_end != L' ' + && *tag_end != L'\t' && *tag_end != L'\r' && *tag_end != L'\n') { + tag_end++; + } + if (tag_end == tag_start || tag_end >= end) { + break; + } + + const wchar_t *open_end = find_wchar_before(tag_end, end, L'>'); + if (!open_end) { + break; + } + + wchar_t close_tag[128]; + wchar_t tag[64]; + size_t tag_len = (size_t)(tag_end - tag_start); + if (tag_len >= sizeof(tag) / sizeof(wchar_t)) { + cursor = open_end + 1; + continue; + } + wcsncpy(tag, tag_start, tag_len); + tag[tag_len] = L'\0'; + if (!format_wstring(close_tag, sizeof(close_tag) / sizeof(wchar_t), + L"", tag)) { + cursor = open_end + 1; + continue; + } + + const wchar_t *value_start = open_end + 1; + const wchar_t *close = wcsstr_before(value_start, close_tag, end); + if (!close) { + cursor = open_end + 1; + continue; + } + + if (!find_wchar_before(value_start, close, L'<')) { + set_wstr_range(metadata, tag_start, tag_len, value_start, close); + } + + cursor = close + wcslen(close_tag); + } +} + +static void +extract_xml_metadata_section( + sentry_value_t metadata, const wchar_t *text, const wchar_t *tag) +{ + const wchar_t *start = NULL; + const wchar_t *end = NULL; + if (find_xml_section(text, tag, &start, &end)) { + extract_xml_leaf_tags(metadata, start, end); + } +} + +static sentry_value_t +parse_internal_metadata(const wchar_t *text) +{ + if (!text || !wcsstr(text, L" 0) { + sentry_value_set_by_key(event, "tags", tags); + } else { + sentry_value_decref(tags); + } + + return event; +} + +static sentry_value_t +read_temp_metadata(const wchar_t *path, const wchar_t *event_id_w) +{ + char *buffer = NULL; + DWORD read = 0; + if (!read_file(path, 256 * 1024, &buffer, &read)) { + return sentry_value_new_null(); + } + + wchar_t *decoded = NULL; + const wchar_t *text = NULL; + bool decoded_buffer = decode_buffer(buffer, (size_t)read, &decoded, &text); + sentry_free(buffer); + if (!decoded_buffer) { + return sentry_value_new_null(); + } + + if (!wcsstr(text, L"= sizeof(program_data) / sizeof(wchar_t)) { + return sentry_value_new_null(); + } + + wchar_t temp_dir[MAX_PATH]; + if (!format_wstring(temp_dir, sizeof(temp_dir) / sizeof(wchar_t), + L"%s\\Microsoft\\Windows\\WER\\Temp", program_data)) { + return sentry_value_new_null(); + } + + sentry_value_t report = sentry_value_new_null(); + for (int i = 0; i < 20; i++) { + if (i > 0) { + Sleep(250); + } + + report = read_temp_metadata_with_marker(temp_dir, event_id); + if (!sentry_value_is_null(report)) { + break; + } + } + + return report; +} + +sentry_value_t +sentry__wer_report_from_buffer(const char *buffer, size_t size) +{ + wchar_t *decoded = NULL; + const wchar_t *text = NULL; + if (!decode_buffer(buffer, size, &decoded, &text)) { + return sentry_value_new_null(); + } + + sentry_value_t report = parse_internal_metadata(text); + sentry_free(decoded); + return report; +} diff --git a/src/backends/native/sentry_wer_report.h b/src/backends/native/sentry_wer_report.h new file mode 100644 index 000000000..c018a502f --- /dev/null +++ b/src/backends/native/sentry_wer_report.h @@ -0,0 +1,13 @@ +#ifndef SENTRY_WER_REPORT_H_INCLUDED +#define SENTRY_WER_REPORT_H_INCLUDED + +#include "sentry_boot.h" +#include "sentry_value.h" + +#define SENTRY_WER_EVENT_ID_KEY "SentryEventId" +#define SENTRY_WER_EVENT_ID_KEY_W L"SentryEventId" + +sentry_value_t sentry__wer_report_lookup(const char *event_id); +sentry_value_t sentry__wer_report_from_buffer(const char *buffer, size_t size); + +#endif diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 3c3e50843..6774e0811 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -33,15 +33,21 @@ #include "sentry_options.h" #include "sentry_os.h" #include "sentry_path.h" +#include "sentry_string.h" #include "sentry_scope.h" #include "sentry_session.h" #include "sentry_sync.h" #include "sentry_tracing.h" #include "sentry_transport.h" +#include "sentry_uuid.h" #include "sentry_value.h" #include "transports/sentry_disk_transport.h" +#if defined(SENTRY_PLATFORM_WINDOWS) +# include "sentry_wer_report.h" +#endif + // Global process-wide synchronization for IPC and shared memory access // This lives for the entire backend lifetime and is shared across all threads #if defined(SENTRY_PLATFORM_WINDOWS) @@ -116,6 +122,7 @@ wer_unregister_module(void) return; } + WerUnregisterCustomMetadata(SENTRY_WER_EVENT_ID_KEY_W); WerUnregisterRuntimeExceptionModule( g_wer_path->path_w, &g_wer_registration); wer_delete_registry_value(g_wer_path); @@ -124,21 +131,21 @@ wer_unregister_module(void) memset(&g_wer_registration, 0, sizeof(g_wer_registration)); } -static void -wer_register_module(uint64_t app_tid) +static bool +wer_register_module(uint64_t app_tid, const char *event_id) { windows_version_t win_ver; if (!sentry__get_windows_version(&win_ver) || win_ver.build < 19041) { SENTRY_WARN("Native WER module not registered, because Windows " "doesn't meet version requirements (build >= 19041)."); - return; + return false; } sentry_path_t *wer_path = wer_default_path(); if (!wer_path || !sentry__path_is_file(wer_path)) { SENTRY_WARN("Native WER module not found"); sentry__path_free(wer_path); - return; + return false; } const DWORD one = 1; @@ -146,25 +153,66 @@ wer_register_module(uint64_t app_tid) if (reg_res != ERROR_SUCCESS) { SENTRY_WARN("registering native WER module in registry failed"); sentry__path_free(wer_path); - return; + return false; } g_wer_registration.version = 1; g_wer_registration.app_pid = GetCurrentProcessId(); g_wer_registration.app_tid = app_tid; - HRESULT hr = WerRegisterRuntimeExceptionModule( + if (sentry__string_empty(event_id)) { + wer_delete_registry_value(wer_path); + sentry__path_free(wer_path); + memset(&g_wer_registration, 0, sizeof(g_wer_registration)); + return false; + } + + wchar_t *event_id_w = sentry__string_to_wstr(event_id); + if (!event_id_w) { + wer_delete_registry_value(wer_path); + sentry__path_free(wer_path); + memset(&g_wer_registration, 0, sizeof(g_wer_registration)); + return false; + } + + HRESULT hr + = WerRegisterCustomMetadata(SENTRY_WER_EVENT_ID_KEY_W, event_id_w); + sentry_free(event_id_w); + if (FAILED(hr)) { + SENTRY_WARN("registering native WER event ID failed"); + wer_delete_registry_value(wer_path); + sentry__path_free(wer_path); + memset(&g_wer_registration, 0, sizeof(g_wer_registration)); + return false; + } + SENTRY_DEBUGF("registered native WER event ID \"%s\"", event_id); + + hr = WerRegisterRuntimeExceptionModule( wer_path->path_w, &g_wer_registration); if (FAILED(hr)) { SENTRY_WARN("registering native WER module failed"); + WerUnregisterCustomMetadata(SENTRY_WER_EVENT_ID_KEY_W); wer_delete_registry_value(wer_path); sentry__path_free(wer_path); memset(&g_wer_registration, 0, sizeof(g_wer_registration)); - return; + return false; } SENTRY_DEBUGF("registered native WER module \"%s\"", wer_path->path); g_wer_path = wer_path; + return true; +} + +static void +wer_sync_tag(const char *key, sentry_value_t value, void *UNUSED(user_data)) +{ + wchar_t *wkey = sentry__string_to_wstr(key); + wchar_t *wvalue = sentry__string_to_wstr(sentry_value_as_string(value)); + if (!wkey || !wvalue || WerRegisterCustomMetadata(wkey, wvalue) != S_OK) { + SENTRY_WARNF("failed to sync tag \"%s\" to WER", key); + } + sentry_free(wkey); + sentry_free(wvalue); } #endif @@ -175,6 +223,7 @@ wer_register_module(uint64_t app_tid) typedef struct { sentry_crash_ipc_t *ipc; pid_t daemon_pid; + sentry_uuid_t crash_event_id; sentry_path_t *event_path; sentry_path_t *breadcrumb1_path; sentry_path_t *breadcrumb2_path; @@ -290,6 +339,9 @@ native_backend_startup( sentry_crash_context_t *ctx = state->ipc->shmem; + state->crash_event_id = sentry__new_event_id(); + sentry_uuid_as_string(&state->crash_event_id, ctx->crash_event_id); + // Set minidump mode from options ctx->minidump_mode = (sentry_minidump_mode_t)options->minidump_mode; @@ -297,6 +349,7 @@ native_backend_startup( ctx->crash_reporting_mode = options->crash_reporting_mode; ctx->system_crash_reporter_enabled = options->system_crash_reporter_enabled; ctx->crash_upload_mode = options->crash_upload_mode; + ctx->wer_sync_mode = options->wer_sync_mode; // Pass debug logging setting to daemon ctx->debug_enabled = options->debug; @@ -526,7 +579,8 @@ native_backend_startup( } # if defined(SENTRY_PLATFORM_WINDOWS) && !defined(SENTRY_PLATFORM_XBOX) - wer_register_module(tid); + bool module_registered = wer_register_module(tid, ctx->crash_event_id); + state->ipc->shmem->platform.wer_enabled = module_registered; # endif if (sentry__crash_handler_init(state->ipc) < 0) { @@ -769,7 +823,7 @@ native_backend_write_attachments(const sentry_path_t *event_path) static void native_backend_flush_scope( - sentry_backend_t *backend, const sentry_options_t *UNUSED(options)) + sentry_backend_t *backend, const sentry_options_t *options) { native_backend_state_t *state = (native_backend_state_t *)backend->data; if (!state || !state->event_path) { @@ -785,7 +839,8 @@ native_backend_flush_scope( } // Create event with current scope - sentry_value_t event = sentry_value_new_object(); + sentry_value_t event + = sentry__value_new_event_with_id(&state->crash_event_id); sentry_value_set_by_key( event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); @@ -833,6 +888,12 @@ native_backend_flush_scope( if (!sentry_value_is_null(tags)) { sentry_value_set_by_key(event, "tags", tags); sentry_value_incref(tags); + +#if defined(SENTRY_PLATFORM_WINDOWS) + if (options->wer_sync_mode & SENTRY_WER_SYNC_MODE_TO_WER) { + sentry__value_foreach_key_value(tags, wer_sync_tag, NULL); + } +#endif } sentry_value_t extra = scope->extra; @@ -999,7 +1060,9 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) = sentry__trace_finish(SENTRY_SPAN_STATUS_ABORTED); // Create crash event - sentry_value_t event = sentry_value_new_event(); + sentry_value_t event = state + ? sentry__value_new_event_with_id(&state->crash_event_id) + : sentry_value_new_event(); sentry_value_set_by_key( event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); diff --git a/src/sentry_options.c b/src/sentry_options.c index cb5bb936a..e2198ef15 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -99,6 +99,7 @@ sentry_options_new(void) = SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP; // Default: best of // both worlds opts->crash_upload_mode = SENTRY_CRASH_UPLOAD_MODE_SYNC; + opts->wer_sync_mode = SENTRY_WER_SYNC_MODE_NONE; opts->http_retry = false; opts->send_client_reports = true; opts->enable_large_attachments = false; @@ -636,6 +637,13 @@ sentry_options_get_crash_upload_mode(const sentry_options_t *opts) return (sentry_crash_upload_mode_t)opts->crash_upload_mode; } +void +sentry_options_set_wer_sync_mode( + sentry_options_t *opts, sentry_wer_sync_mode_t mode) +{ + opts->wer_sync_mode = (int)mode; +} + void sentry_options_set_crashpad_wait_for_upload( sentry_options_t *opts, int wait_for_upload) diff --git a/src/sentry_options.h b/src/sentry_options.h index 6f64bba43..33c97c022 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -103,6 +103,7 @@ struct sentry_options_s { int crash_reporting_mode; // 0=minidump, 1=native, 2=native_with_minidump // (see sentry_crash_reporting_mode_t) int crash_upload_mode; // 0=sync, 1=async (see sentry_crash_upload_mode_t) + int wer_sync_mode; // bitmask of sentry_wer_sync_mode_t #ifdef SENTRY_PLATFORM_NX void (*network_connect_func)(void); diff --git a/tests/assertions.py b/tests/assertions.py index 0f505d25f..251ed9b18 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -666,16 +666,21 @@ def assert_failed_proxy_auth_request(stdout): ) -def wait_for_file(path, timeout=10.0, poll_interval=0.1): - import glob +def wait_for(condition, timeout=10.0, interval=0.1): import time deadline = time.time() + timeout while time.time() < deadline: - if glob.glob(str(path)): + if condition(): return True - time.sleep(poll_interval) - return False + time.sleep(interval) + return condition() + + +def wait_for_file(path, timeout=10.0, interval=0.1): + import glob + + return wait_for(lambda: glob.glob(str(path)), timeout, interval) def wait_for_daemon(tmp_path, started_at, timeout=None): diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 3132aa4f5..68a778ee2 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -5,11 +5,14 @@ multi-thread capture, and FPU/SIMD register capture on all platforms. """ +from ast import pattern import os +from pydoc import text import subprocess import sys import time import struct +from pathlib import Path import pytest from . import ( @@ -26,6 +29,7 @@ assert_native_crash, assert_session, is_valid_hex, + wait_for, wait_for_file, assert_user_feedback, ) @@ -83,13 +87,37 @@ def test_native_capture_crash(cmake, httpserver): assert waiting.result +def wait_for_wer(event_id, timeout=10.0, interval=0.1): + program_data = os.environ.get("ProgramData") + if not program_data: + return False + + wer_dir = Path(program_data) / "Microsoft" / "Windows" / "WER" / "Temp" + + def find_wer_file(): + for path in wer_dir.glob("*.WERInternalMetadata.xml"): + try: + text = path.read_bytes().decode("utf-16", errors="replace") + if ( + f"{event_id}" in text + and "some value" in text + ): + return True + except Exception as e: + continue + return False + + return wait_for(find_wer_file, timeout=timeout, interval=interval) + + @pytest.mark.skipif( sys.platform != "win32" or bool(os.environ.get("TEST_MINGW")), reason="WER crash tests are only available in MSVC Windows builds", ) @pytest.mark.with_wer +@pytest.mark.parametrize("wer_sync_mode", ["none", "from-wer", "to-wer", "from-to-wer"]) @pytest.mark.parametrize("crash_arg", ["fastfail", "stack-buffer-overrun"]) -def test_native_wer(cmake, httpserver, crash_arg): +def test_native_wer(cmake, httpserver, crash_arg, wer_sync_mode): """Test WER crash capture with native backend""" tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "native"}) @@ -99,7 +127,7 @@ def test_native_wer(cmake, httpserver, crash_arg): run_crash( tmp_path, "sentry_example", - ["log", "stdout", crash_arg], + ["log", "stdout", "wer-sync-mode", wer_sync_mode, crash_arg], env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), ) assert waiting.result @@ -108,6 +136,24 @@ def test_native_wer(cmake, httpserver, crash_arg): envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) assert_native_crash(envelope, exception_code=0xC0000409) + event = envelope.get_event() + assert event is not None + contexts = event.get("contexts", {}) + tags = event.get("tags", {}) + + if "from" in wer_sync_mode: + assert "wer" in contexts + assert contexts["wer"].get("report_id") + assert tags.get("SentryWer") == "value from WER" + else: + assert "wer" not in contexts + assert "SentryWer" not in tags + + if "to" in wer_sync_mode: + event_id = event.get("event_id") + assert event_id + assert wait_for_wer(event_id) + @pytest.mark.skipif(not has_oom, reason="OOM test unreliable in this environment") def test_native_oom(cmake, httpserver): diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 132114fc4..e3debec88 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -13,6 +13,9 @@ // Include native backend headers # include "../../src/backends/native/minidump/sentry_minidump_format.h" # include "../../src/backends/native/sentry_crash_context.h" +# if defined(SENTRY_PLATFORM_WINDOWS) +# include "../../src/backends/native/sentry_wer_report.h" +# endif #endif #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) @@ -582,3 +585,51 @@ SENTRY_TEST(elf_header_entry_sizes) TEST_CHECK(!sentry__elf_has_phdr_size(e_ident, phdr_size)); #endif } + +SENTRY_TEST(wer_metadata) +{ +#if defined(SENTRY_BACKEND_NATIVE) && defined(SENTRY_PLATFORM_WINDOWS) + const char metadata[] + = "\r\n" + "\r\n" + " \r\n" + " 99926ddc-16e9-4b01-a13d-53dce7433e3f\r\n" + " 2026-05-12T20:17:11Z\r\n" + " \r\n" + " \r\n" + " APPCRASH\r\n" + " sentry-playground.exe\r\n" + " sentry-playground.exe\r\n" + " c0000005\r\n" + " 0000000000012345\r\n" + " \r\n" + " \r\n" + " zzz\r\n" + " aaa\r\n" + " ooo\r\n" + " internal-marker\r\n" + " \r\n" + "\r\n"; + + sentry_value_t wer_report + = sentry__wer_report_from_buffer(metadata, strlen(metadata)); + TEST_CHECK(!sentry_value_is_null(wer_report)); + TEST_CHECK(sentry_value_get_type(wer_report) == SENTRY_VALUE_TYPE_OBJECT); + + sentry_value_t tags = sentry_value_get_by_key(wer_report, "tags"); + TEST_CHECK(sentry_value_get_type(tags) == SENTRY_VALUE_TYPE_OBJECT); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(tags, "foo")), "ooo"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(tags, "bar")), "aaa"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(tags, "baz")), "zzz"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(tags, SENTRY_WER_EVENT_ID_KEY))); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(wer_report, "metadata"))); + sentry_value_decref(wer_report); +#else + SKIP_TEST(); +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index f08c777f2..38bda3bea 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -400,5 +400,6 @@ XX(value_uint64) XX(value_unicode) XX(value_user) XX(value_wrong_type) +XX(wer_metadata) XX(write_raw_envelope_to_file) XX(xmm_save_area_size)