From 616176bdb0c0390be1daf60bf8740fa55cd16ea2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 15 Sep 2026 14:11:20 +0200 Subject: [PATCH 1/3] perf(native): Sync scope over IPC Instead of flushing the whole scope/event on disk on every scope change, send an initial scope snapshot and then minimal incremental updates to the crash daemon over IPC. Keep the latest complete state in daemon memory for crash reports. Discard interrupted messages and preserve crash callback changes through a final event and attachment snapshot. Close: #1834 --- CHANGELOG.md | 1 + src/backends/native/sentry_crash_context.h | 4 +- src/backends/native/sentry_crash_daemon.c | 392 ++++++-------- src/backends/native/sentry_crash_ipc.c | 485 ++++++++++++++++- src/backends/native/sentry_crash_ipc.h | 54 +- src/backends/sentry_backend_native.c | 600 ++++++++++++--------- src/integrations/sentry_integration_wer.c | 5 +- src/sentry_scope.c | 97 +++- src/sentry_scope.h | 16 +- tests/test_integration_native.py | 206 +++---- tests/unit/test_native_backend.c | 490 ++++++++++++++++- tests/unit/test_scope.c | 166 +++++- tests/unit/tests.inc | 12 + 13 files changed, 1917 insertions(+), 611 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26067c939c..8cb9fdc9cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add `sentry_scope_begin_read`/`sentry_scope_end_read` and `sentry_scope_begin_write`/`sentry_scope_end_write` for consistent reads of multiple scope properties and applying multiple scope changes with a single scope flush. ([#2107](https://github.com/getsentry/sentry-native/pull/2107)) - Add `sentry_scope_acquire` and `sentry_scope_release` to access the isolation scope. ([#2160](https://github.com/getsentry/sentry-native/pull/2160)) - Native: Provide exception metadata, registers, and a basic crashing-thread stack to `on_crash` for crash identification. ([#2174](https://github.com/getsentry/sentry-native/pull/2174)) +- Native: reduce disk I/O on scope changes by sending incremental updates to the crash daemon over IPC. ([#2155](https://github.com/getsentry/sentry-native/pull/2155), [#2157](https://github.com/getsentry/sentry-native/pull/2157)) **Fixes**: diff --git a/src/backends/native/sentry_crash_context.h b/src/backends/native/sentry_crash_context.h index 627c415d40..4f5e30e17c 100644 --- a/src/backends/native/sentry_crash_context.h +++ b/src/backends/native/sentry_crash_context.h @@ -330,13 +330,11 @@ typedef struct { // On Linux, libunwind captures using DWARF without frame pointers. // The daemon prefers captures over its own stack walk. sentry_crash_capture_t capture; + bool event_discarded; // Sentry-specific metadata paths char database_path[SENTRY_CRASH_MAX_PATH]; // Shared across runs char run_path[SENTRY_CRASH_MAX_PATH]; // For current run - char event_path[SENTRY_CRASH_MAX_PATH]; - char breadcrumb1_path[SENTRY_CRASH_MAX_PATH]; - char breadcrumb2_path[SENTRY_CRASH_MAX_PATH]; char envelope_path[SENTRY_CRASH_MAX_PATH]; char external_reporter_path[SENTRY_CRASH_MAX_PATH]; char dsn[SENTRY_CRASH_MAX_PATH]; // Sentry DSN for uploading crashes diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index ce9437baea..e9b9877e8d 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -374,7 +374,7 @@ read_legacy_manifest(const sentry_path_t *manifest_path) size_t buf_len = 0; char *buf = sentry__path_read_to_buffer(manifest_path, &buf_len); if (!buf) { - return sentry_value_new_list(); + return sentry_value_new_null(); } const char *start = buf; @@ -444,42 +444,37 @@ read_attachment_manifest(const sentry_path_t *run_folder) return attachments; } -static void -write_attachments_from_manifest( - int fd, const sentry_options_t *options, const sentry_path_t *run_folder) +static sentry_value_t +read_crash_event(const sentry_path_t *run_folder) { - sentry_value_t attachments = read_attachment_manifest(run_folder); - size_t len = sentry_value_get_length(attachments); - for (size_t i = 0; i < len; i++) { - sentry_value_t attachment = sentry_value_get_by_index(attachments, i); - const char *path = sentry__attachment_get_path(attachment); - if (!attachment_is_placeholder(options, path)) { - write_attachment_to_envelope(fd, path, - sentry__attachment_get_filename(attachment), - sentry__attachment_get_type(attachment), - sentry__attachment_get_content_type(attachment)); - } + sentry_path_t *path = sentry__path_join_str(run_folder, "__sentry-event"); + if (!path) { + return sentry_value_new_null(); } - sentry_value_decref(attachments); + size_t len = 0; + char *buf = sentry__path_read_to_buffer(path, &len); + sentry__path_free(path); + if (!buf) { + return sentry_value_new_null(); + } + sentry_value_t event = sentry_value_from_json(buf, len); + sentry_free(buf); + if (sentry_value_get_type(event) != SENTRY_VALUE_TYPE_OBJECT) { + sentry_value_decref(event); + return sentry_value_new_null(); + } + return event; } -// For each large attachment listed in `/__sentry-attachments`, -// cache it as an attachment-ref item. Small attachments were already inlined -// during envelope writing. +// cache large staged attachments as attachment-ref items static void add_attachment_refs(sentry_envelope_t *envelope, - const sentry_options_t *options, const sentry_path_t *run_folder) + const sentry_options_t *options, sentry_value_t list) { if (!envelope || !options || !options->run - || !options->enable_large_attachments || !run_folder) { - return; - } - sentry_value_t list = read_attachment_manifest(run_folder); - if (sentry_value_is_null(list)) { - SENTRY_WARN("Failed to parse attachment manifest"); + || !options->enable_large_attachments) { return; } - bool materialized = false; size_t len = sentry_value_get_length(list); for (size_t i = 0; i < len; i++) { @@ -497,7 +492,6 @@ add_attachment_refs(sentry_envelope_t *envelope, SENTRY_WARN("failed to cache attachment-ref"); } } - sentry_value_decref(list); } /** @@ -3087,64 +3081,6 @@ build_stacktrace_from_ctx(const sentry_crash_context_t *ctx) return build_stacktrace_for_thread(ctx, SIZE_MAX); } -/** - * Reads one breadcrumb ring file the crashing process appended on its hot path - * into a breadcrumb list. Returns null if the file is absent or empty. - */ -static sentry_value_t -read_breadcrumb_ring_file(const sentry_path_t *run_folder, const char *name) -{ - if (!run_folder) { - return sentry_value_new_null(); - } - sentry_path_t *path = sentry__path_join_str(run_folder, name); - if (!path) { - return sentry_value_new_null(); - } - size_t size = 0; - char *buf = sentry__path_read_to_buffer(path, &size); - sentry__path_free(path); - if (!buf || size == 0) { - sentry_free(buf); - return sentry_value_new_null(); - } - sentry_value_t list = sentry_value_from_msgpack_stream(buf, size); - sentry_free(buf); - return list; -} - -/** - * Assembles the crash event's breadcrumbs from the two ring files the crashing - * process appended one-at-a-time, merges them in timestamp order, keeps the - * newest `max_breadcrumbs`, and attaches them to `event`. - * Mirrors the crashpad backend's `report_to_envelope`. - */ -static void -apply_breadcrumbs_from_ring_files(sentry_value_t event, - const sentry_path_t *run_folder, const sentry_crash_context_t *ctx) -{ - if (ctx && ctx->max_breadcrumbs == 0) { - return; - } - - sentry_value_t b1 - = read_breadcrumb_ring_file(run_folder, "__sentry-breadcrumb1"); - sentry_value_t b2 - = read_breadcrumb_ring_file(run_folder, "__sentry-breadcrumb2"); - size_t max = ctx && ctx->max_breadcrumbs ? ctx->max_breadcrumbs - : SENTRY_BREADCRUMBS_MAX; - sentry_value_t merged = sentry__value_merge_breadcrumbs(b1, b2, max); - sentry_value_decref(b1); - sentry_value_decref(b2); - // Overwrite any breadcrumbs the base event may carry: the ring files are - // the single source of truth, so this is idempotent and never duplicates. - if (sentry_value_get_type(merged) == SENTRY_VALUE_TYPE_LIST) { - sentry_value_set_by_key(event, "breadcrumbs", merged); - } else { - sentry_value_decref(merged); - } -} - #if defined(SENTRY_PLATFORM_WINDOWS) static bool is_wer_done(const sentry_crash_context_t *ctx) @@ -3215,40 +3151,18 @@ enrich_stacktrace(const sentry_crash_context_t *ctx, sentry_value_t stacktrace) * Build a native event and set the level, mechanism, and handled state * * @param ctx Crash context - * @param event_file_path Path to base event file from parent process - * @param run_folder Run directory holding the breadcrumb ring files + * @param base Staged event received from the parent process * @param level Event level (e.g. "fatal") * @param mechanism_type Exception mechanism type (e.g. "signalhandler") * @param handled Whether the mechanism was handled */ static sentry_value_t -build_native_event(const sentry_crash_context_t *ctx, - const char *event_file_path, const sentry_path_t *run_folder, +build_native_event(const sentry_crash_context_t *ctx, sentry_value_t base, const char *level, const char *mechanism_type, bool handled) { - // Read base event from parent's file - sentry_value_t event = sentry_value_new_null(); - if (!sentry__string_empty(event_file_path)) { - sentry_path_t *ev_path = sentry__path_from_str(event_file_path); - if (ev_path) { - size_t event_size = 0; - char *event_json - = sentry__path_read_to_buffer(ev_path, &event_size); - sentry__path_free(ev_path); - if (event_json && event_size > 0) { - event = sentry_value_from_json(event_json, event_size); - sentry_free(event_json); - } - } - } + sentry_value_t event = sentry__value_clone(base); + sentry__ensure_event_id(event, NULL); - if (sentry_value_is_null(event)) { - event = sentry_value_new_event(); - } else { - sentry__ensure_event_id(event, NULL); - } - - apply_breadcrumbs_from_ring_files(event, run_folder, ctx); #if defined(SENTRY_PLATFORM_WINDOWS) apply_wer_context(event, ctx); #endif @@ -3583,14 +3497,14 @@ build_native_event(const sentry_crash_context_t *ctx, static bool write_envelope_with_native_stacktrace(const sentry_options_t *options, const char *envelope_path, const sentry_crash_context_t *ctx, - const char *event_file_path, const char *minidump_path, - sentry_path_t *run_folder) + sentry_value_t base, const char *minidump_path, sentry_path_t *run_folder, + sentry_value_t attachments) { // Build native crash event (always include threads with names) SENTRY_DEBUGF("write_envelope_with_native_stacktrace: minidump_path=%s", minidump_path ? minidump_path : "(null)"); - sentry_value_t event = build_native_event( - ctx, event_file_path, run_folder, "fatal", "signalhandler", false); + sentry_value_t event + = build_native_event(ctx, base, "fatal", "signalhandler", false); // Serialize event to JSON size_t event_size = 0; @@ -3746,9 +3660,27 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options, } } - // Add scope attachments using metadata file - if (run_folder) { - write_attachments_from_manifest(fd, options, run_folder); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + sentry_value_t attach_info = sentry_value_get_by_index(attachments, i); + sentry_value_t path_val = sentry_value_get_by_key(attach_info, "path"); + sentry_value_t filename_val + = sentry_value_get_by_key(attach_info, "filename"); + sentry_value_t attachment_type_val + = sentry_value_get_by_key(attach_info, "attachment_type"); + sentry_value_t content_type_val + = sentry_value_get_by_key(attach_info, "content_type"); + + const char *path = sentry_value_as_string(path_val); + const char *filename = sentry_value_as_string(filename_val); + const char *attachment_type + = sentry_value_as_string(attachment_type_val); + const char *content_type = sentry_value_as_string(content_type_val); + + if (path && filename && !attachment_is_placeholder(options, path)) { + write_attachment_to_envelope( + fd, path, filename, attachment_type, content_type); + } } // Add screenshot attachment if captured by the daemon @@ -3789,48 +3721,21 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options, static bool write_envelope_with_minidump(const sentry_options_t *options, const sentry_crash_context_t *ctx, const char *envelope_path, - const char *event_msgpack_path, const char *minidump_path, - sentry_path_t *run_folder) + sentry_value_t base, const char *minidump_path, sentry_path_t *run_folder, + sentry_value_t attachments) { - // Read the base event, merge in the breadcrumbs from the ring files, - // re-serialize. - size_t event_size = 0; - char *event_json = NULL; - char *event_id = NULL; - sentry_path_t *ev_path = sentry__path_from_str(event_msgpack_path); - if (ev_path) { - size_t base_size = 0; - char *base_json = sentry__path_read_to_buffer(ev_path, &base_size); - sentry__path_free(ev_path); - if (base_json && base_size > 0) { - sentry_value_t event = sentry_value_from_json(base_json, base_size); - if (sentry_value_is_null(event)) { - // Parsing the base event failed (e.g. truncated buffer or - // OOM). Don't serialize the null into "null" and ship an - // invalid payload - fall back to streaming the raw event - // bytes verbatim so the crash report is preserved. - sentry_value_decref(event); - event_json = sentry__string_clone_n(base_json, base_size); - event_size = event_json ? base_size : 0; - } else { - apply_breadcrumbs_from_ring_files(event, run_folder, ctx); + sentry_value_t event = sentry__value_clone(base); #if defined(SENTRY_PLATFORM_WINDOWS) - apply_wer_context(event, ctx); + apply_wer_context(event, ctx); #endif - event_id = sentry__string_clone(sentry_value_as_string( - sentry_value_get_by_key(event, "event_id"))); - event_json = sentry__value_to_json(event, &event_size); - sentry_value_decref(event); - if (!event_json) { - // Re-serialization failed (e.g. OOM). Fall back to the raw - // event bytes so the crash report is preserved, losing only - // the merged breadcrumbs rather than the whole event. - event_json = sentry__string_clone_n(base_json, base_size); - event_size = event_json ? base_size : 0; - } - } - } - sentry_free(base_json); + size_t event_size = 0; + char *event_json = sentry__value_to_json(event, &event_size); + char *event_id = sentry__string_clone( + sentry_value_as_string(sentry_value_get_by_key(event, "event_id"))); + sentry_value_decref(event); + if (!event_json) { + sentry_free(event_id); + return false; } // Open envelope file for writing @@ -3975,9 +3880,27 @@ write_envelope_with_minidump(const sentry_options_t *options, #endif } - // Add scope attachments using metadata file - if (run_folder) { - write_attachments_from_manifest(fd, options, run_folder); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + sentry_value_t attach_info = sentry_value_get_by_index(attachments, i); + sentry_value_t path_val = sentry_value_get_by_key(attach_info, "path"); + sentry_value_t filename_val + = sentry_value_get_by_key(attach_info, "filename"); + sentry_value_t attachment_type_val + = sentry_value_get_by_key(attach_info, "attachment_type"); + sentry_value_t content_type_val + = sentry_value_get_by_key(attach_info, "content_type"); + + const char *path = sentry_value_as_string(path_val); + const char *filename = sentry_value_as_string(filename_val); + const char *attachment_type + = sentry_value_as_string(attachment_type_val); + const char *content_type = sentry_value_as_string(content_type_val); + + if (path && filename && !attachment_is_placeholder(options, path)) { + write_attachment_to_envelope( + fd, path, filename, attachment_type, content_type); + } } // Add screenshot attachment if captured by the daemon @@ -4017,6 +3940,39 @@ write_envelope_with_minidump(const sentry_options_t *options, * * Called by the crash daemon (out-of-process on Linux/macOS). */ +#if defined(SENTRY_PLATFORM_WINDOWS) +// Sentry's symbolicator needs `contexts.device.arch` to process PE modules. If +// the scope already carries a device context with arch (host SDKs like Unity +// provide one), leave it; otherwise synthesize a minimal one so native-only +// consumers still symbolicate. +static void +ensure_device_arch(sentry_value_t event) +{ + sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); + if (sentry_value_is_null(contexts)) { + contexts = sentry_value_new_object(); + sentry_value_set_by_key(event, "contexts", contexts); + } + sentry_value_t device = sentry_value_get_by_key(contexts, "device"); + if (sentry_value_is_null(device)) { + device = sentry_value_new_object(); + sentry_value_set_by_key( + device, "type", sentry_value_new_string("device")); + sentry_value_set_by_key(contexts, "device", device); + } + if (!sentry_value_is_null(sentry_value_get_by_key(device, "arch"))) { + return; + } +# if defined(_M_AMD64) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86_64")); +# elif defined(_M_IX86) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86")); +# elif defined(_M_ARM64) + sentry_value_set_by_key(device, "arch", sentry_value_new_string("arm64")); +# endif +} +#endif + bool sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) { @@ -4028,7 +3984,7 @@ 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 (!ctx->event_path[0]) { + if (ctx->event_discarded) { SENTRY_DEBUG("Crash event discarded by local callback"); goto done; } @@ -4100,33 +4056,23 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) goto done; } - // Get event file path from context - const char *event_path = ctx->event_path[0] ? ctx->event_path : NULL; - SENTRY_DEBUGF( - "Event path from context: %s", event_path ? event_path : "(null)"); - if (!event_path) { - SENTRY_WARN("No event file from parent"); - if (minidump_path[0]) { - // Delete the orphaned minidump to prevent disk space leaks -#if defined(SENTRY_PLATFORM_UNIX) - unlink(minidump_path); -#elif defined(SENTRY_PLATFORM_WINDOWS) - wchar_t *wpath = sentry__string_to_wstr(minidump_path); - if (wpath) { - _wunlink(wpath); - sentry_free(wpath); - } -#endif - } - ctx->minidump_path[0] = '\0'; - goto done; + sentry_value_t base = sentry__crash_scope_event(&ipc->scope); + sentry_path_t *run_folder = sentry__path_from_str(ctx->run_path); + sentry_value_t crash_event + = run_folder ? read_crash_event(run_folder) : sentry_value_new_null(); + if (!sentry_value_is_null(crash_event)) { + sentry_value_decref(base); + base = crash_event; } - - // Extract run folder path from event path (event is at - // run_folder/__sentry-event) - SENTRY_DEBUG("Extracting run folder from event path"); - sentry_path_t *ev_path = sentry__path_from_str(event_path); - sentry_path_t *run_folder = ev_path ? sentry__path_dir(ev_path) : NULL; + sentry_value_t attachments = run_folder + ? read_attachment_manifest(run_folder) + : sentry_value_new_null(); + if (sentry_value_is_null(attachments)) { + attachments = sentry_value_incref(ipc->scope.attachments); + } +#if defined(SENTRY_PLATFORM_WINDOWS) + ensure_device_arch(base); +#endif // The crashing process dumps its pending logs, sessions, and transactions // before notifying the daemon. Queue those before writing the crash @@ -4144,7 +4090,8 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) if (path_len < 0 || path_len >= (int)sizeof(envelope_path)) { SENTRY_WARN("Envelope path truncated or invalid"); - sentry__path_free(ev_path); + sentry_value_decref(base); + sentry_value_decref(attachments); if (run_folder) { sentry__path_free(run_folder); } @@ -4253,18 +4200,19 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) "minidump_path=%s", minidump_path[0] ? minidump_path : "NULL"); envelope_written = write_envelope_with_native_stacktrace(options, - envelope_path, ctx, event_path, - minidump_path[0] ? minidump_path : NULL, run_folder); + envelope_path, ctx, base, minidump_path[0] ? minidump_path : NULL, + run_folder, attachments); } else { // Mode 0 (MINIDUMP only) SENTRY_DEBUG("Writing envelope with minidump"); - envelope_written = write_envelope_with_minidump( - options, ctx, envelope_path, event_path, minidump_path, run_folder); + envelope_written = write_envelope_with_minidump(options, ctx, + envelope_path, base, minidump_path, run_folder, attachments); } if (!envelope_written) { SENTRY_WARN("Failed to write envelope"); - sentry__path_free(ev_path); + sentry_value_decref(base); + sentry_value_decref(attachments); if (run_folder) { sentry__path_free(run_folder); } @@ -4314,7 +4262,7 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) goto cleanup; } - add_attachment_refs(envelope, options, run_folder); + add_attachment_refs(envelope, options, attachments); bool has_attachment_refs = sentry__envelope_has_content_type( envelope, SENTRY_ATTACHMENT_REF_MIME); @@ -4356,36 +4304,15 @@ sentry__process_crash(const sentry_options_t *options, sentry_crash_ipc_t *ipc) } cleanup: - // Send the staged session-replay envelope same-session, enriched from the - // crash event (`/__sentry-event`) so it shares the crash's - // tags/contexts/trace and embeds its breadcrumbs. Only flush when the - // crash itself was delivered: `cleanup` is also reached via `goto` on - // error paths where the crash was never captured, and flushing there - // would consume (and delete) the staged replay for a crash that never - // arrived. + // flush replay only after the crash itself was captured if (crash_captured && options && options->transport && sentry__session_replay_has_pending(options)) { - sentry_value_t crash_event = sentry_value_new_null(); - if (ev_path) { - size_t ev_len = 0; - char *ev_json = sentry__path_read_to_buffer(ev_path, &ev_len); - if (ev_json) { - crash_event = sentry_value_from_json(ev_json, ev_len); - sentry_free(ev_json); - } - } - if (!sentry_value_is_null(crash_event)) { - // `__sentry-event` is scope-applied without breadcrumbs; merge - // the ring files so the replay recording can embed them - apply_breadcrumbs_from_ring_files(crash_event, run_folder, ctx); - } - sentry__session_replay_flush_pending( - options, options->transport, crash_event); - sentry_value_decref(crash_event); + sentry__session_replay_flush_pending(options, options->transport, base); } sentry__path_free(run_folder); - sentry__path_free(ev_path); + sentry_value_decref(base); + sentry_value_decref(attachments); SENTRY_DEBUG("Crash processing completed successfully"); @@ -4662,6 +4589,16 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, ipc->parent_handle = app_pid; #endif + if (!sentry__crash_scope_init(&ipc->scope, ipc->shmem->max_breadcrumbs)) { + sentry__crash_ipc_free(ipc); + sentry_options_free(options); + if (log_file) { + sentry__logger_disable(); + fclose(log_file); + } + return 1; + } + // Signal to parent that daemon is ready SENTRY_DEBUG("Signaling ready to parent"); sentry__crash_ipc_signal_ready(ipc); @@ -4690,8 +4627,8 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, uint64_t crash_deadline = 0; while (true) { // Wait for crash notification (with timeout to check parent health) - bool wait_result - = sentry__crash_ipc_wait(ipc, SENTRY_CRASH_DAEMON_WAIT_TIMEOUT_MS); + bool wait_result = sentry__crash_ipc_receive( + ipc, SENTRY_CRASH_DAEMON_WAIT_TIMEOUT_MS); if (wait_result) { SENTRY_DEBUG("Event signaled, checking crash state"); } @@ -4746,8 +4683,17 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, SENTRY_DEBUG("Spurious notification or already processed"); } + if (ipc->scope.stopped) { + break; + } + if (ipc->message_closed && parent_alive) { + // retain legacy WER notifications after the app closes its stream + ipc->notify_pending = sentry__crash_ipc_wait( + ipc, SENTRY_CRASH_DAEMON_WAIT_TIMEOUT_MS); + } + // Check if parent is still alive (only if no crash processed yet) - if (!parent_alive) { + if (!parent_alive && !ipc->notify_pending) { SENTRY_DEBUG("Parent process exited without crash"); break; } diff --git a/src/backends/native/sentry_crash_ipc.c b/src/backends/native/sentry_crash_ipc.c index 94cdd0eef4..fb123ffd21 100644 --- a/src/backends/native/sentry_crash_ipc.c +++ b/src/backends/native/sentry_crash_ipc.c @@ -1,12 +1,18 @@ #include "sentry_crash_ipc.h" #include "sentry_alloc.h" +#include "sentry_core.h" #include "sentry_logger.h" #include "sentry_sync.h" +#include "sentry_value.h" #include #include +#if defined(SENTRY_PLATFORM_UNIX) +# include +#endif + static void write_u16_le(char *buf, uint16_t value) { @@ -114,6 +120,9 @@ sentry__crash_ipc_message_decode( } uint32_t message_len = read_u32_le(buf); + if (message_len > SENTRY_CRASH_IPC_MESSAGE_MAX_LEN) { + return SENTRY_CRASH_IPC_MESSAGE_OVERSIZED; + } if (message_len < SENTRY_CRASH_IPC_MESSAGE_MIN_LEN) { return SENTRY_CRASH_IPC_MESSAGE_INVALID; } @@ -309,6 +318,10 @@ sentry__crash_ipc_init_app(sem_t *init_sem) return NULL; } + struct timeval send_timeout = { 1, 0 }; + setsockopt(ipc->message_fd[0], SOL_SOCKET, SO_SNDTIMEO, &send_timeout, + sizeof(send_timeout)); + // Initialize shared memory only if newly created if (!shm_exists) { sentry__crash_context_init(ipc->shmem); @@ -336,6 +349,7 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, ipc->is_daemon = true; ipc->message_fd[0] = -1; ipc->message_fd[1] = message_fd; + fcntl(message_fd, F_SETFD, FD_CLOEXEC); // Open existing shared memory created by app (using PID and thread ID) // Must match the format in sentry__crash_ipc_init_app @@ -381,8 +395,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return ipc; } -void -sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +static void +notify_fallback(sentry_crash_ipc_t *ipc) { if (!ipc || ipc->notify_fd < 0) { return; @@ -440,6 +454,9 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) return; } + sentry_free(ipc->message_buf); + sentry__crash_scope_free(&ipc->scope); + if (ipc->shmem && ipc->shmem != MAP_FAILED) { munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); } @@ -634,6 +651,12 @@ sentry__crash_ipc_init_app(sentry_mutex_t *init_mutex) sentry_free(ipc); return NULL; } + int no_sigpipe = 1; + setsockopt(ipc->message_fd[0], SOL_SOCKET, SO_NOSIGPIPE, &no_sigpipe, + sizeof(no_sigpipe)); + struct timeval send_timeout = { 1, 0 }; + setsockopt(ipc->message_fd[0], SOL_SOCKET, SO_SNDTIMEO, &send_timeout, + sizeof(send_timeout)); fcntl(ipc->message_fd[0], F_SETFD, FD_CLOEXEC); fcntl(ipc->message_fd[1], F_SETFD, FD_CLOEXEC); @@ -665,6 +688,7 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, ipc->is_daemon = true; ipc->message_fd[0] = -1; ipc->message_fd[1] = message_fd; + fcntl(message_fd, F_SETFD, FD_CLOEXEC); // Use the inherited shm_fd directly (no shm_open needed, sandbox-safe) ipc->shm_fd = shm_fd; @@ -699,8 +723,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return ipc; } -void -sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +static void +notify_fallback(sentry_crash_ipc_t *ipc) { if (!ipc) { return; @@ -754,6 +778,9 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) return; } + sentry_free(ipc->message_buf); + sentry__crash_scope_free(&ipc->scope); + if (ipc->shmem && ipc->shmem != MAP_FAILED) { munmap(ipc->shmem, SENTRY_CRASH_SHM_SIZE); } @@ -947,6 +974,7 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, } ipc->is_daemon = true; ipc->message_read_handle = message_read_handle; + SetHandleInformation(message_read_handle, HANDLE_FLAG_INHERIT, 0); // Open existing shared memory (using PID and thread ID) swprintf(ipc->shm_name, SENTRY_CRASH_IPC_NAME_SIZE, @@ -1012,8 +1040,8 @@ sentry__crash_ipc_init_daemon(pid_t app_pid, uint64_t app_tid, return ipc; } -void -sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +static void +notify_fallback(sentry_crash_ipc_t *ipc) { if (!ipc || !ipc->event_handle) { // No logging - called from signal handler/exception filter @@ -1060,6 +1088,9 @@ sentry__crash_ipc_free(sentry_crash_ipc_t *ipc) return; } + sentry_free(ipc->message_buf); + sentry__crash_scope_free(&ipc->scope); + if (ipc->shmem) { UnmapViewOfFile(ipc->shmem); } @@ -1215,3 +1246,445 @@ sentry__crash_ipc_wait_for_ready(sentry_crash_ipc_t *ipc, int timeout_ms) return false; #endif } + +bool +sentry__crash_scope_init(sentry_crash_scope_t *scope, size_t max_breadcrumbs) +{ + memset(scope, 0, sizeof(*scope)); + scope->event = sentry_value_new_null(); + scope->attachments = sentry_value_new_list(); + scope->breadcrumbs = sentry__ringbuffer_new(max_breadcrumbs); + return scope->breadcrumbs && !sentry_value_is_null(scope->attachments); +} + +void +sentry__crash_scope_free(sentry_crash_scope_t *scope) +{ + sentry_value_decref(scope->event); + sentry_value_decref(scope->attachments); + sentry__ringbuffer_free(scope->breadcrumbs); +} + +bool +sentry__crash_scope_apply( + sentry_crash_scope_t *scope, const sentry_crash_ipc_message_t *message) +{ + if (scope->stopped || message->flags + || message->sequence != scope->sequence + 1 + || (!scope->initialized + && message->type != SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT)) { + return false; + } + if (message->type == SENTRY_CRASH_IPC_MESSAGE_CRASH + || message->type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN) { + if (message->payload_len) { + return false; + } + scope->sequence = message->sequence; + scope->stopped = true; + return true; + } + + sentry_value_t value + = sentry_value_from_msgpack(message->payload, message->payload_len); + sentry_value_type_t type = sentry_value_get_type(value); + if (sentry_value_is_null(value) + && (message->payload_len != 1 + || (unsigned char)message->payload[0] != 0xc0)) { + return false; + } + bool valid = false; + const char *field = NULL; + switch (message->type) { + case SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT: { + sentry_value_t event = sentry_value_get_by_key(value, "event"); + sentry_value_t attachments + = sentry_value_get_by_key(value, "attachments"); + if (sentry_value_get_type(event) != SENTRY_VALUE_TYPE_OBJECT + || sentry_value_get_type(attachments) != SENTRY_VALUE_TYPE_LIST) { + break; + } + sentry_ringbuffer_t *breadcrumbs + = sentry__ringbuffer_new(scope->breadcrumbs->max_size); + if (!breadcrumbs) { + break; + } + sentry_value_t list = sentry_value_get_by_key(event, "breadcrumbs"); + for (size_t i = 0; i < sentry_value_get_length(list); i++) { + sentry__ringbuffer_append(breadcrumbs, + sentry_value_incref(sentry_value_get_by_index(list, i))); + } + sentry_value_remove_by_key(event, "breadcrumbs"); + sentry__value_replace(&scope->event, sentry_value_incref(event)); + sentry__value_replace( + &scope->attachments, sentry_value_incref(attachments)); + sentry__ringbuffer_free(scope->breadcrumbs); + scope->breadcrumbs = breadcrumbs; + scope->initialized = true; + valid = true; + break; + } + case SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE: + field = "release"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_ENVIRONMENT: + field = "environment"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_TRANSACTION: + field = "transaction"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT: + field = "fingerprint"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_LEVEL: + field = "level"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_USER: + field = "user"; + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_TAG: + case SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA: + case SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT: { + const char *key + = sentry_value_as_string(sentry_value_get_by_index(value, 0)); + if (type != SENTRY_VALUE_TYPE_LIST + || sentry_value_get_length(value) != 2 + || sentry_value_get_type(sentry_value_get_by_index(value, 0)) + != SENTRY_VALUE_TYPE_STRING) { + break; + } + const char *name = message->type == SENTRY_CRASH_IPC_MESSAGE_SET_TAG + ? "tags" + : message->type == SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA ? "extra" + : "contexts"; + sentry_value_t object = sentry_value_get_by_key(scope->event, name); + if (sentry_value_is_null(object)) { + object = sentry_value_new_object(); + sentry_value_set_by_key(scope->event, name, object); + } + valid + = sentry_value_set_by_key_n(object, key, + sentry_value_get_length(sentry_value_get_by_index(value, 0)), + sentry_value_incref(sentry_value_get_by_index(value, 1))) + == 0; + break; + } + case SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG: + case SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA: + case SENTRY_CRASH_IPC_MESSAGE_REMOVE_CONTEXT: { + if (type != SENTRY_VALUE_TYPE_STRING) { + break; + } + const char *name = message->type == SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG + ? "tags" + : message->type == SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA + ? "extra" + : "contexts"; + sentry_value_remove_by_key_n( + sentry_value_get_by_key(scope->event, name), + sentry_value_as_string(value), sentry_value_get_length(value)); + valid = true; + break; + } + case SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB: + if (type == SENTRY_VALUE_TYPE_OBJECT) { + valid = !scope->breadcrumbs->max_size + || sentry__ringbuffer_append( + scope->breadcrumbs, sentry_value_incref(value)) + == 0; + } + break; + case SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST: + if (type == SENTRY_VALUE_TYPE_LIST) { + sentry__value_replace( + &scope->attachments, sentry_value_incref(value)); + valid = true; + } + break; + default: + break; + } + if (field) { + sentry_value_type_t expected + = message->type == SENTRY_CRASH_IPC_MESSAGE_SET_USER + ? SENTRY_VALUE_TYPE_OBJECT + : message->type == SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT + ? SENTRY_VALUE_TYPE_LIST + : SENTRY_VALUE_TYPE_STRING; + if (type == expected || sentry_value_is_null(value)) { + if (sentry_value_is_null(value) + || (type == SENTRY_VALUE_TYPE_STRING + && !sentry_value_get_length(value))) { + sentry_value_remove_by_key(scope->event, field); + valid = true; + } else { + valid = sentry_value_set_by_key( + scope->event, field, sentry_value_incref(value)) + == 0; + } + } + } + sentry_value_decref(value); + if (valid) { + scope->sequence = message->sequence; + } + return valid; +} + +sentry_value_t +sentry__crash_scope_event(const sentry_crash_scope_t *scope) +{ + sentry_value_t event = scope->initialized + ? sentry__value_clone(scope->event) + : sentry_value_new_event(); + sentry_value_set_by_key( + event, "breadcrumbs", sentry__ringbuffer_to_list(scope->breadcrumbs)); + sentry_value_set_by_key( + event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + sentry__ensure_event_id(event, NULL); + return event; +} + +static bool +write_message( + sentry_crash_ipc_t *ipc, const char *buf, size_t len, bool nonblocking) +{ + while (len) { +#if defined(SENTRY_PLATFORM_WINDOWS) + (void)nonblocking; + DWORD written = 0; + if (!WriteFile( + ipc->message_write_handle, buf, (DWORD)len, &written, NULL) + || !written) { + return false; + } +#else + int flags = nonblocking ? MSG_DONTWAIT : 0; +# if defined(MSG_NOSIGNAL) + flags |= MSG_NOSIGNAL; +# endif + ssize_t written = send(ipc->message_fd[0], buf, len, flags); + if (written < 0 && errno == EINTR) { + continue; + } + if (written <= 0) { + return false; + } +#endif + buf += written; + len -= (size_t)written; + } + return true; +} + +bool +sentry__crash_ipc_send( + sentry_crash_ipc_t *ipc, uint16_t type, sentry_value_t value) +{ + if (!ipc || sentry__atomic_fetch(&ipc->stopped) + || sentry__atomic_fetch(&ipc->message_failed) + || !sentry__atomic_compare_swap(&ipc->writing, 0, 1)) { + return false; + } + size_t payload_len = 0; + char *payload = type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN + ? NULL + : sentry_value_to_msgpack(value, &payload_len); + char *buf = NULL; + size_t len = 0; + bool sent = (payload || type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN) + && sentry__crash_ipc_message_encode( + type, 0, ipc->sequence + 1, payload, payload_len, &buf, &len) + == SENTRY_CRASH_IPC_MESSAGE_OK; + if (sent) { + sent = write_message(ipc, buf, len, false); + if (sent) { + ipc->sequence++; + } else { + // a partial write makes this stream unusable for further frames + sentry__atomic_store(&ipc->message_failed, 1); + } + } + sentry_free(buf); + sentry_free(payload); + sentry__atomic_store(&ipc->writing, 0); + return sent; +} + +void +sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc) +{ + if (!ipc) { + return; + } + if (sentry__atomic_fetch(&ipc->shmem->state) + == SENTRY_CRASH_STATE_CRASHING) { + notify_fallback(ipc); + return; + } + sentry__atomic_store(&ipc->stopped, 1); +#if !defined(SENTRY_PLATFORM_WINDOWS) + if (!sentry__atomic_fetch(&ipc->message_failed) + && sentry__atomic_compare_swap(&ipc->writing, 0, 1)) { + char frame[SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE] = { 0 }; + write_u32_le(frame, SENTRY_CRASH_IPC_MESSAGE_MIN_LEN); + write_u16_le(frame + 4, SENTRY_CRASH_IPC_MESSAGE_CRASH); + write_u64_le(frame + 8, ipc->sequence + 1); + bool sent = write_message(ipc, frame, sizeof(frame), true); + sentry__atomic_store(&ipc->writing, 0); + if (sent) { + return; + } + } +#endif + // also used by WER; the daemon drains complete frames before freezing + notify_fallback(ipc); +} + +static void +close_message_stream(sentry_crash_ipc_t *ipc) +{ + ipc->message_closed = true; +#if defined(SENTRY_PLATFORM_WINDOWS) + CloseHandle(ipc->message_read_handle); + ipc->message_read_handle = NULL; +#else + close(ipc->message_fd[1]); + ipc->message_fd[1] = -1; +#endif +} + +bool +sentry__crash_ipc_receive(sentry_crash_ipc_t *ipc, int timeout_ms) +{ + // a bounded poll also checks legacy notifications without blocking a writer + int remaining = timeout_ms; + do { + ipc->notify_pending + = ipc->notify_pending || sentry__crash_ipc_wait(ipc, 0); + bool available = false; + if (!ipc->message_closed) { +#if defined(SENTRY_PLATFORM_WINDOWS) + DWORD bytes = 0; + if (!PeekNamedPipe( + ipc->message_read_handle, NULL, 0, NULL, &bytes, NULL)) { + close_message_stream(ipc); + } + available = bytes > 0; +#else + struct pollfd fd = { ipc->message_fd[1], POLLIN, 0 }; + available = poll(&fd, 1, 0) > 0; +#endif + } + if (!available) { + if (ipc->notify_pending) { + if (sentry__atomic_fetch(&ipc->shmem->state) + == SENTRY_CRASH_STATE_CRASHING) { + ipc->notify_pending = false; + } else { + ipc->scope.stopped = true; + } + return true; + } + if (ipc->message_closed || remaining <= 0) { + return false; + } +#if defined(SENTRY_PLATFORM_WINDOWS) + int delay = remaining < 10 ? remaining : 10; + ipc->notify_pending = sentry__crash_ipc_wait(ipc, delay); + remaining -= delay; +#else +# if defined(SENTRY_PLATFORM_MACOS) + int notify_fd = ipc->notify_pipe[0]; +# else + int notify_fd = ipc->notify_fd; +# endif + struct pollfd fds[] = { + { ipc->message_fd[1], POLLIN, 0 }, + { notify_fd, POLLIN, 0 }, + }; + int rv = poll(fds, 2, remaining); + if (!rv || (rv < 0 && errno != EINTR)) { + return false; + } +#endif + continue; + } + if (!ipc->message_buf) { + ipc->message_size = SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE; + ipc->message_buf = sentry_malloc(ipc->message_size); + if (!ipc->message_buf) { + close_message_stream(ipc); + continue; + } + } + size_t len = ipc->message_size - ipc->message_len; +#if defined(SENTRY_PLATFORM_WINDOWS) + DWORD bytes = 0, received = 0; + if (!PeekNamedPipe( + ipc->message_read_handle, NULL, 0, NULL, &bytes, NULL) + || !ReadFile(ipc->message_read_handle, + ipc->message_buf + ipc->message_len, + (DWORD)(len < bytes ? len : bytes), &received, NULL) + || !received) { + close_message_stream(ipc); + continue; + } +#else + ssize_t received = recv(ipc->message_fd[1], + ipc->message_buf + ipc->message_len, len, MSG_DONTWAIT); + if (received < 0 && (errno == EINTR || errno == EAGAIN)) { + continue; + } + if (received <= 0) { + close_message_stream(ipc); + continue; + } +#endif + ipc->message_len += (size_t)received; + if (ipc->message_len < ipc->message_size) { + continue; + } + sentry_crash_ipc_message_t message; + sentry_crash_ipc_message_result_t result + = sentry__crash_ipc_message_decode( + ipc->message_buf, ipc->message_len, &message); + if (result == SENTRY_CRASH_IPC_MESSAGE_PARTIAL) { + if (!sentry__crash_ipc_message_type_is_known( + read_u16_le(ipc->message_buf + 4)) + || read_u16_le(ipc->message_buf + 6) + || read_u64_le(ipc->message_buf + 8) + != ipc->scope.sequence + 1) { + close_message_stream(ipc); + continue; + } + ipc->message_size = (size_t)read_u32_le(ipc->message_buf) + + SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE; + char *buf = sentry_malloc(ipc->message_size); + if (!buf) { + close_message_stream(ipc); + continue; + } + memcpy(buf, ipc->message_buf, ipc->message_len); + sentry_free(ipc->message_buf); + ipc->message_buf = buf; + continue; + } + if (result != SENTRY_CRASH_IPC_MESSAGE_OK + || !sentry__crash_scope_apply(&ipc->scope, &message)) { + SENTRY_WARN("daemon: invalid scope IPC frame"); + close_message_stream(ipc); + continue; + } + uint16_t type = message.type; + sentry_free(ipc->message_buf); + ipc->message_buf = NULL; + ipc->message_len = 0; + if (type == SENTRY_CRASH_IPC_MESSAGE_CRASH) { + return true; + } + if (type == SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN) { + return false; + } + } while (true); +} diff --git a/src/backends/native/sentry_crash_ipc.h b/src/backends/native/sentry_crash_ipc.h index dbefd8b3b8..6704dbc600 100644 --- a/src/backends/native/sentry_crash_ipc.h +++ b/src/backends/native/sentry_crash_ipc.h @@ -3,6 +3,7 @@ #include "sentry_boot.h" #include "sentry_crash_context.h" +#include "sentry_ringbuffer.h" #include #include @@ -22,8 +23,14 @@ #define SENTRY_CRASH_IPC_MESSAGE_PREFIX_SIZE 4u #define SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE 16u #define SENTRY_CRASH_IPC_MESSAGE_MIN_LEN 12u -#define SENTRY_CRASH_IPC_MESSAGE_MAX_LEN UINT32_MAX +#define SENTRY_CRASH_IPC_MESSAGE_MAX_LEN (256u * 1024u * 1024u) +/** + * Frames use little-endian length/type/flags/sequence, followed by one msgpack + * value. Flags are reserved (zero). Snapshots carry {event, attachments}; keyed + * setters carry [key, value], removals carry the key, and other updates carry + * the new value. Crash and shutdown frames have no payload. + */ typedef enum { SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT = 1, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE = 2, @@ -70,6 +77,22 @@ sentry_crash_ipc_message_result_t sentry__crash_ipc_message_encode( sentry_crash_ipc_message_result_t sentry__crash_ipc_message_decode( const char *buf, size_t buf_len, sentry_crash_ipc_message_t *message); +typedef struct { + sentry_value_t event; + sentry_value_t attachments; + sentry_ringbuffer_t *breadcrumbs; + uint64_t sequence; + bool initialized; + bool stopped; +} sentry_crash_scope_t; + +bool sentry__crash_scope_init( + sentry_crash_scope_t *scope, size_t max_breadcrumbs); +void sentry__crash_scope_free(sentry_crash_scope_t *scope); +bool sentry__crash_scope_apply( + sentry_crash_scope_t *scope, const sentry_crash_ipc_message_t *message); +sentry_value_t sentry__crash_scope_event(const sentry_crash_scope_t *scope); + #if defined(SENTRY_PLATFORM_WINDOWS) typedef HANDLE sentry_process_handle_t; #else @@ -82,6 +105,20 @@ typedef pid_t sentry_process_handle_t; typedef struct { sentry_crash_context_t *shmem; + // app-only writer guard; crash handling must never wait for its owner + volatile long writing; + volatile long message_failed; + volatile long stopped; + uint64_t sequence; + + // daemon-only staged state and incremental frame reader + sentry_crash_scope_t scope; + char *message_buf; + size_t message_len; + size_t message_size; + bool message_closed; + bool notify_pending; + #if defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) int shm_fd; int notify_fd; // Eventfd for crash notifications @@ -182,6 +219,21 @@ void sentry__crash_ipc_notify(sentry_crash_ipc_t *ipc); */ bool sentry__crash_ipc_wait(sentry_crash_ipc_t *ipc, int timeout_ms); +/** + * Send a borrowed value. Scope updates must hold the scope observer lock. + * Returns false on failure or if a crash interrupted another writer. + */ +bool sentry__crash_ipc_send( + sentry_crash_ipc_t *ipc, uint16_t type, sentry_value_t value); + +/** + * Receive and apply ordered frames, returning true on a crash notification. + * Early crash notifications keep the staged scope mutable. + * An out-of-band notification drains complete available frames first; an + * interrupted partial frame is discarded without changing staged state. + */ +bool sentry__crash_ipc_receive(sentry_crash_ipc_t *ipc, int timeout_ms); + /** * Unlink the shared memory. */ diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 64dc467dc6..c3da50cc3f 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -207,44 +207,17 @@ typedef struct { sentry_crash_ipc_t *ipc; pid_t daemon_pid; sentry_path_t *run_path; - sentry_path_t *event_path; - sentry_path_t *breadcrumb1_path; - sentry_path_t *breadcrumb2_path; sentry_path_t *envelope_path; - size_t max_breadcrumbs; - size_t num_breadcrumbs; - volatile long crashed; + const sentry_options_t *options; sentry_scope_observer_t *scope_observer; + volatile long crashed; } native_backend_state_t; -static void native_backend_flush_scope( - sentry_backend_t *backend, const sentry_options_t *options); -static void native_backend_add_breadcrumb( - void *data, sentry_value_t breadcrumb); -static void add_attachment(void *data, sentry_value_t attachment); - -static void -native_backend_preload_scope(sentry_backend_t *backend) -{ - sentry_value_t breadcrumbs = sentry_value_new_null(); - SENTRY_WITH_SCOPE (scope) { - sentry_value_t attachments = sentry__scope_load_attachments(scope); - size_t attachment_count = sentry_value_get_length(attachments); - for (size_t i = 0; i < attachment_count; i++) { - add_attachment( - backend->data, sentry_value_get_by_index(attachments, i)); - } - sentry_value_decref(attachments); - breadcrumbs = sentry__scope_breadcrumbs_to_list(scope); - } - - size_t breadcrumb_count = sentry_value_get_length(breadcrumbs); - for (size_t i = 0; i < breadcrumb_count; i++) { - native_backend_add_breadcrumb( - backend->data, sentry_value_get_by_index(breadcrumbs, i)); - } - sentry_value_decref(breadcrumbs); -} +static sentry_value_t attachment_list( + native_backend_state_t *state, const sentry_scope_t *scope); +static void sync_attachments(void *data, sentry_value_t attachment); +static bool observe_scope(native_backend_state_t *state); +static void native_backend_shutdown(sentry_backend_t *backend); static void add_attachment(void *data, sentry_value_t attachment) @@ -568,27 +541,48 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, } // Prepare process creation structures - STARTUPINFOW si; + STARTUPINFOEXW si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); + si.StartupInfo.cb = sizeof(si); // Hide console window for daemon - si.dwFlags = STARTF_USESHOWWINDOW; - si.wShowWindow = SW_HIDE; + si.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; + si.StartupInfo.wShowWindow = SW_HIDE; ZeroMemory(&pi, sizeof(pi)); + SIZE_T attr_size = 0; + InitializeProcThreadAttributeList(NULL, 1, 0, &attr_size); + si.lpAttributeList = sentry_malloc(attr_size); + if (!si.lpAttributeList) { + return (pid_t)-1; + } + if (!InitializeProcThreadAttributeList( + si.lpAttributeList, 1, 0, &attr_size)) { + sentry_free(si.lpAttributeList); + return (pid_t)-1; + } + if (!UpdateProcThreadAttribute(si.lpAttributeList, 0, + PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &message_read_handle, + sizeof(message_read_handle), NULL, NULL)) { + DeleteProcThreadAttributeList(si.lpAttributeList); + sentry_free(si.lpAttributeList); + return (pid_t)-1; + } + // Create the daemon process if (!CreateProcessW(NULL, // Application name (use command line) cmd_line, // Command line NULL, // Process security attributes NULL, // Thread security attributes - TRUE, // Inherit handles (for event_handle) - CREATE_NO_WINDOW | DETACHED_PROCESS, // Creation flags + TRUE, // inherit the control-stream read handle + CREATE_NO_WINDOW | DETACHED_PROCESS | EXTENDED_STARTUPINFO_PRESENT, NULL, // Environment NULL, // Current directory - &si, // Startup info + &si.StartupInfo, // Startup info &pi)) { // Process information DWORD error = GetLastError(); + DeleteProcThreadAttributeList(si.lpAttributeList); + sentry_free(si.lpAttributeList); char *daemon_path_err = sentry__string_from_wstr(daemon_path_w); if (daemon_path_err) { SENTRY_WARNF("Failed to create daemon process at '%s': Error %lu%s", @@ -603,6 +597,9 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, return (pid_t)-1; } + DeleteProcThreadAttributeList(si.lpAttributeList); + sentry_free(si.lpAttributeList); + // Close thread handle (we don't need it) CloseHandle(pi.hThread); @@ -614,31 +611,6 @@ daemon_start(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, #endif } -static void -clear_scope(void *data) -{ - native_backend_state_t *state = (native_backend_state_t *)data; - if (!state) { - return; - } - - const sentry_path_t *paths[] - = { state->breadcrumb1_path, state->breadcrumb2_path }; - for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) { - if (paths[i] && sentry__path_write_buffer(paths[i], "", 0) != 0) { - SENTRY_WARN("failed to clear persisted breadcrumbs"); - } - } - state->num_breadcrumbs = 0; - - sentry_path_t *path - = sentry__path_join_str(state->run_path, "__sentry-attachments"); - if (path && sentry__path_is_file(path) && sentry__path_remove(path) != 0) { - SENTRY_WARN("failed to clear attachment manifest"); - } - sentry__path_free(path); -} - static int native_backend_startup( sentry_backend_t *backend, const sentry_options_t *options) @@ -702,7 +674,7 @@ native_backend_startup( } backend->data = state; state->run_path = sentry__path_clone(options->run->run_path); - state->max_breadcrumbs = options->max_breadcrumbs; + state->options = options; // Initialize IPC (protected by global synchronization for concurrent // access) @@ -852,36 +824,6 @@ native_backend_startup( #endif } - state->event_path = sentry__path_join_str(run_path, "__sentry-event"); - state->breadcrumb1_path - = sentry__path_join_str(run_path, "__sentry-breadcrumb1"); - state->breadcrumb2_path - = sentry__path_join_str(run_path, "__sentry-breadcrumb2"); - - sentry__path_touch(state->event_path); - sentry__path_touch(state->breadcrumb1_path); - sentry__path_touch(state->breadcrumb2_path); - - // Copy paths to crash context -#ifdef _WIN32 - strncpy_s(ctx->event_path, sizeof(ctx->event_path), state->event_path->path, - _TRUNCATE); - strncpy_s(ctx->breadcrumb1_path, sizeof(ctx->breadcrumb1_path), - state->breadcrumb1_path->path, _TRUNCATE); - strncpy_s(ctx->breadcrumb2_path, sizeof(ctx->breadcrumb2_path), - state->breadcrumb2_path->path, _TRUNCATE); -#else - strncpy( - ctx->event_path, state->event_path->path, sizeof(ctx->event_path) - 1); - ctx->event_path[sizeof(ctx->event_path) - 1] = '\0'; - strncpy(ctx->breadcrumb1_path, state->breadcrumb1_path->path, - sizeof(ctx->breadcrumb1_path) - 1); - ctx->breadcrumb1_path[sizeof(ctx->breadcrumb1_path) - 1] = '\0'; - strncpy(ctx->breadcrumb2_path, state->breadcrumb2_path->path, - sizeof(ctx->breadcrumb2_path) - 1); - ctx->breadcrumb2_path[sizeof(ctx->breadcrumb2_path) - 1] = '\0'; -#endif - // Set up crash envelope path state->envelope_path = sentry__path_join_str( options->run->run_path, "__sentry-crash.envelope"); @@ -925,10 +867,6 @@ native_backend_startup( } #endif - // Persist the preloaded scope before any crash handler becomes active. - native_backend_preload_scope(backend); - native_backend_flush_scope(backend, options); - // Install crash handlers (signal handlers on Linux/macOS, Mach exception // handler on iOS) #if defined(SENTRY_PLATFORM_IOS) @@ -1029,6 +967,12 @@ native_backend_startup( state->ipc->shmem->platform.wer_enabled = wer_register_module(tid, options); # endif + if (!observe_scope(state)) { + SENTRY_WARN("failed to synchronize native crash scope"); + native_backend_shutdown(backend); + return 1; + } + sentry_handler_strategy_t strategy = # if defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) options ? sentry_options_get_handler_strategy(options) : @@ -1050,6 +994,10 @@ native_backend_startup( CloseHandle(hDaemon); } # endif + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_remove_observer(scope, state->scope_observer); + } + state->scope_observer = NULL; sentry__crash_ipc_free(state->ipc); sentry__path_free(state->run_path); sentry_free(state); @@ -1059,18 +1007,6 @@ native_backend_startup( #endif SENTRY_DEBUG("native backend started successfully"); - sentry_scope_observer_t *observer = sentry__scope_observer_new(); - if (observer) { - observer->data = state; - observer->clear = clear_scope; - observer->add_breadcrumb = native_backend_add_breadcrumb; - observer->add_attachment = add_attachment; - SENTRY_WITH_SCOPE_MUT (scope) { - if (sentry__scope_add_observer(scope, observer)) { - state->scope_observer = observer; - } - } - } return 0; } @@ -1095,20 +1031,30 @@ native_backend_shutdown(sentry_backend_t *backend) wer_unregister_module(); #endif + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null()); + // Shutdown crash handlers (signal handlers on Linux/macOS, Mach exception // handler on iOS) sentry__crash_handler_shutdown(); bool daemon_stopped = false; #if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_IOS) - // Terminate daemon (Unix) if (state->daemon_pid > 0) { - kill(state->daemon_pid, SIGTERM); - // Wait for daemon to exit - pid_t wait_result; - do { - wait_result = waitpid(state->daemon_pid, NULL, 0); - } while (wait_result < 0 && errno == EINTR); + pid_t wait_result = 0; + for (int i = 0; i < 500; i++) { + wait_result = waitpid(state->daemon_pid, NULL, WNOHANG); + if (wait_result > 0 || (wait_result < 0 && errno != EINTR)) { + break; + } + usleep(10000); + } + if (wait_result == 0) { + kill(state->daemon_pid, SIGTERM); + do { + wait_result = waitpid(state->daemon_pid, NULL, 0); + } while (wait_result < 0 && errno == EINTR); + } daemon_stopped = wait_result == state->daemon_pid; } #elif defined(SENTRY_PLATFORM_WINDOWS) @@ -1117,10 +1063,13 @@ native_backend_shutdown(sentry_backend_t *backend) HANDLE hDaemon = OpenProcess( PROCESS_TERMINATE | SYNCHRONIZE, FALSE, state->daemon_pid); if (hDaemon) { - TerminateProcess(hDaemon, 0); - // Wait for daemon to exit (with timeout) daemon_stopped = WaitForSingleObject(hDaemon, 5000) == WAIT_OBJECT_0; + if (!daemon_stopped) { + TerminateProcess(hDaemon, 0); + daemon_stopped + = WaitForSingleObject(hDaemon, 5000) == WAIT_OBJECT_0; + } CloseHandle(hDaemon); } } @@ -1213,162 +1162,321 @@ native_backend_free(sentry_backend_t *backend) return; } - sentry__path_free(state->event_path); - sentry__path_free(state->breadcrumb1_path); - sentry__path_free(state->breadcrumb2_path); sentry__path_free(state->envelope_path); sentry__path_free(state->run_path); sentry_free(state); } -// Writes the attachment list to /__sentry-attachments so the -// crash daemon can locate and append them to the crash envelope. static void -native_backend_write_attachments( - const sentry_path_t *event_path, const sentry_hint_t *hint) +write_crash_attachments( + native_backend_state_t *state, const sentry_hint_t *hint) { - if (!event_path) { + if (!state || !state->run_path) { return; } - SENTRY_WITH_SCOPE (scope) { - sentry_value_t attachments = hint - ? sentry_value_incref(hint->attachments) - : sentry__scope_load_attachments(scope); - if (sentry_value_get_length(attachments) == 0 && !hint) { - sentry_value_decref(attachments); + sentry_value_t attachments = sentry__hint_resolve_attachments(hint); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + add_attachment(state, sentry_value_get_by_index(attachments, i)); + } + sentry_path_t *path + = sentry__path_join_str(state->run_path, "__sentry-attachments"); + if (path) { + sentry__write_attachment_manifest(path, attachments); + sentry__path_free(path); + } + sentry_value_decref(attachments); +} + +static void +write_crash_event(native_backend_state_t *state, sentry_value_t event) +{ + if (!state || !state->run_path) { + return; + } + sentry_path_t *path + = sentry__path_join_str(state->run_path, "__sentry-event"); + if (!path) { + return; + } + size_t len = 0; + char *json = sentry__value_to_json(event, &len); + if (json) { + sentry__path_write_buffer(path, json, len); + sentry_free(json); + } + sentry__path_free(path); +} + +static sentry_value_t +attachment_list(native_backend_state_t *state, const sentry_scope_t *scope) +{ + sentry_value_t attachments = sentry__scope_load_attachments(scope); + sentry_value_t attach_list = sentry_value_new_list(); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len; i++) { + sentry_value_t attachment = sentry_value_get_by_index(attachments, i); + sentry_path_t *path + = sentry__attachment_make_run_path(state->run_path, attachment); + if (!path) { continue; } - sentry_path_t *run_path = sentry__path_dir(event_path); - if (!run_path) { - sentry_value_decref(attachments); + // skip missing or partially written attachments + size_t bytes_len = 0; + if (sentry__attachment_get_bytes(attachment, &bytes_len) + && sentry__path_get_size(path) != bytes_len) { + sentry__path_free(path); continue; } - sentry_path_t *attach_list_path - = sentry__path_join_str(run_path, "__sentry-attachments"); - if (attach_list_path) { - sentry__write_attachment_manifest(attach_list_path, attachments); - sentry__path_free(attach_list_path); + sentry_value_t attach_info = sentry_value_new_object(); + sentry_value_set_by_key( + attach_info, "path", sentry_value_new_string(path->path)); + const char *filename = sentry__attachment_get_filename(attachment); + sentry_value_set_by_key( + attach_info, "filename", sentry_value_new_string(filename)); + const char *type = sentry__attachment_get_type(attachment); + if (!sentry__string_empty(type)) { + sentry_value_set_by_key( + attach_info, "attachment_type", sentry_value_new_string(type)); } - sentry__path_free(run_path); - sentry_value_decref(attachments); + const char *content_type + = sentry__attachment_get_content_type(attachment); + if (content_type) { + sentry_value_set_by_key(attach_info, "content_type", + sentry_value_new_string(content_type)); + } + sentry_value_append(attach_list, attach_info); + sentry__path_free(path); } + sentry_value_decref(attachments); + return attach_list; } -#if defined(SENTRY_PLATFORM_WINDOWS) -// Sentry's symbolicator needs `contexts.device.arch` to process PE modules. If -// the scope already carries a device context with arch (host SDKs like Unity -// provide one), leave it; otherwise synthesize a minimal one so native-only -// consumers still symbolicate. static void -ensure_device_arch(sentry_value_t event) +sync_attachments(void *data, sentry_value_t attachment) { - sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); - if (sentry_value_is_null(contexts)) { - contexts = sentry_value_new_object(); - sentry_value_set_by_key(event, "contexts", contexts); - } - sentry_value_t device = sentry_value_get_by_key(contexts, "device"); - if (sentry_value_is_null(device)) { - device = sentry_value_new_object(); - sentry_value_set_by_key( - device, "type", sentry_value_new_string("device")); - sentry_value_set_by_key(contexts, "device", device); - } - if (!sentry_value_is_null(sentry_value_get_by_key(device, "arch"))) { + (void)attachment; + native_backend_state_t *state = data; + if (sentry__atomic_fetch(&state->crashed)) { return; } -# if defined(_M_AMD64) - sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86_64")); -# elif defined(_M_IX86) - sentry_value_set_by_key(device, "arch", sentry_value_new_string("x86")); -# elif defined(_M_ARM64) - sentry_value_set_by_key(device, "arch", sentry_value_new_string("arm64")); -# endif + SENTRY_WITH_SCOPE (scope) { + sentry_value_t list = attachment_list(state, scope); + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, list); + sentry_value_decref(list); + } } -#endif static void -native_backend_flush_scope( - sentry_backend_t *backend, const sentry_options_t *options) +observe_attachment(void *data, sentry_value_t attachment) { - native_backend_state_t *state = (native_backend_state_t *)backend->data; - if (!state || !state->event_path || sentry__atomic_fetch(&state->crashed)) { + native_backend_state_t *state = data; + if (sentry__atomic_fetch(&state->crashed)) { return; } + add_attachment(data, attachment); + sync_attachments(data, attachment); +} - native_backend_write_attachments(state->event_path, NULL); - - // Create event with current scope - sentry_value_t event = sentry_value_new_object(); - sentry__ensure_event_id(event, NULL); +static bool +send_snapshot(native_backend_state_t *state, const sentry_scope_t *scope, + sentry_value_t event) +{ + sentry_value_t snapshot = sentry_value_new_object(); + sentry_value_set_by_key(snapshot, "event", sentry_value_incref(event)); sentry_value_set_by_key( - event, "level", sentry__value_new_level(SENTRY_LEVEL_FATAL)); + snapshot, "attachments", attachment_list(state, scope)); + bool sent = sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, snapshot); + sentry_value_decref(snapshot); + return sent; +} - // Apply scope with contexts - SENTRY_WITH_SCOPE (scope) { - sentry__scope_apply_to_event(scope, options, event, SENTRY_SCOPE_NONE); +static bool +snapshot_scope(native_backend_state_t *state, const sentry_scope_t *scope) +{ + if (sentry__atomic_fetch(&state->crashed)) { + return false; } -#if defined(SENTRY_PLATFORM_WINDOWS) - ensure_device_arch(event); -#endif - - size_t json_len = 0; - char *json_str = sentry__value_to_json(event, &json_len); + sentry_value_t event = sentry_value_new_object(); + sentry__scope_apply_to_event( + scope, state->options, event, SENTRY_SCOPE_BREADCRUMBS); + bool sent = send_snapshot(state, scope, event); sentry_value_decref(event); - - if (json_str) { - sentry__path_write_buffer(state->event_path, json_str, json_len); - sentry_free(json_str); - } + return sent; } static void -native_backend_add_breadcrumb(void *data, sentry_value_t breadcrumb) +clear_scope(void *data) { - native_backend_state_t *state = (native_backend_state_t *)data; - if (!state) { + native_backend_state_t *state = data; + if (sentry__atomic_fetch(&state->crashed)) { return; } + // clear observers run before the source scope is reset + SENTRY_WITH_SCOPE (scope) { + sentry_scope_t *empty = sentry_scope_clone(scope); + if (empty) { + sentry_scope_clear(empty); + snapshot_scope(state, empty); + sentry_scope_free(empty); + } + } +} - size_t max_breadcrumbs = state->max_breadcrumbs; - if (!max_breadcrumbs) { - return; +#define VALUE_OBSERVER(Name, Type) \ + static void Name(void *data, sentry_value_t value) \ + { \ + native_backend_state_t *state = data; \ + sentry__crash_ipc_send(state->ipc, Type, value); \ } - bool first_breadcrumb = state->num_breadcrumbs % max_breadcrumbs == 0; +VALUE_OBSERVER(set_release, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE) +VALUE_OBSERVER(set_environment, SENTRY_CRASH_IPC_MESSAGE_SET_ENVIRONMENT) +VALUE_OBSERVER(set_transaction, SENTRY_CRASH_IPC_MESSAGE_SET_TRANSACTION) +VALUE_OBSERVER(set_fingerprint, SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT) +VALUE_OBSERVER(add_breadcrumb, SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB) +#undef VALUE_OBSERVER - const sentry_path_t *breadcrumb_file - = state->num_breadcrumbs % (max_breadcrumbs * 2) < max_breadcrumbs - ? state->breadcrumb1_path - : state->breadcrumb2_path; +static void +set_user(void *data, sentry_value_t user) +{ + native_backend_state_t *state = data; + sentry_value_t value = sentry__value_clone(user); + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_OBJECT + && sentry_value_is_null(sentry_value_get_by_key(value, "id")) + && state->options->run->installation_id) { + sentry_value_set_by_key(value, "id", + sentry_value_new_string(state->options->run->installation_id)); + } + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SET_USER, value); + sentry_value_decref(value); +} - state->num_breadcrumbs++; +static void +set_level(void *data, sentry_level_t level) +{ + native_backend_state_t *state = data; + sentry_value_t value = sentry__value_new_level(level); + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SET_LEVEL, value); + sentry_value_decref(value); +} - if (!breadcrumb_file) { - return; +static void +send_key(void *data, uint16_t type, const char *key, sentry_value_t value) +{ + native_backend_state_t *state = data; + sentry_value_t pair = sentry_value_new_list(); + sentry_value_append(pair, sentry_value_new_string(key)); + sentry_value_append(pair, sentry_value_incref(value)); + sentry__crash_ipc_send(state->ipc, type, pair); + sentry_value_decref(pair); +} + +static void +set_tag(void *data, const char *key, sentry_value_t value) +{ + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_TAG, key, value); +} + +static void +set_extra(void *data, const char *key, sentry_value_t value) +{ + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA, key, value); +} + +static void +set_context(void *data, const char *key, sentry_value_t value) +{ + if (!sentry__string_eq(key, "trace")) { + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT, key, value); } +} - // Append as msgpack, matching the crashpad backend. msgpack values are - // self-delimiting, so the daemon can read the concatenated ring file back - // into a list via `sentry_value_from_msgpack`. - size_t mpack_size = 0; - char *mpack = sentry_value_to_msgpack(breadcrumb, &mpack_size); - if (!mpack) { - return; +#define REMOVE_OBSERVER(Name, Type) \ + static void Name(void *data, const char *key) \ + { \ + native_backend_state_t *state = data; \ + sentry_value_t value = sentry_value_new_string(key); \ + sentry__crash_ipc_send(state->ipc, Type, value); \ + sentry_value_decref(value); \ } - int rv = first_breadcrumb - ? sentry__path_write_buffer(breadcrumb_file, mpack, mpack_size) - : sentry__path_append_buffer(breadcrumb_file, mpack, mpack_size); +REMOVE_OBSERVER(remove_tag, SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG) +REMOVE_OBSERVER(remove_extra, SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA) +REMOVE_OBSERVER(remove_context_key, SENTRY_CRASH_IPC_MESSAGE_REMOVE_CONTEXT) +#undef REMOVE_OBSERVER - sentry_free(mpack); +static void +remove_context(void *data, const char *key) +{ + if (!sentry__string_eq(key, "trace")) { + remove_context_key(data, key); + } +} - if (rv != 0) { - SENTRY_WARN("failed to write breadcrumb"); +static void +set_trace(void *data, sentry_value_t trace) +{ + if (sentry_value_is_null(trace)) { + remove_context_key(data, "trace"); + } else { + send_key(data, SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT, "trace", trace); } } +static bool +init_scope_observer(void *data, const sentry_scope_t *scope) +{ + native_backend_state_t *state = data; + sentry_value_t attachments = sentry__scope_load_attachments(scope); + for (size_t i = 0; i < sentry_value_get_length(attachments); i++) { + add_attachment(state, sentry_value_get_by_index(attachments, i)); + } + sentry_value_decref(attachments); + return snapshot_scope(state, scope); +} + +static bool +observe_scope(native_backend_state_t *state) +{ + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + if (!observer) { + return false; + } + observer->data = state; + observer->clear = clear_scope; + observer->set_release = set_release; + observer->set_environment = set_environment; + observer->set_transaction = set_transaction; + observer->set_fingerprint = set_fingerprint; + observer->set_level = set_level; + observer->set_user = set_user; + observer->set_tag = set_tag; + observer->remove_tag = remove_tag; + observer->set_extra = set_extra; + observer->remove_extra = remove_extra; + observer->set_context = set_context; + observer->remove_context = remove_context; + observer->set_trace = set_trace; + observer->add_breadcrumb = add_breadcrumb; + observer->add_attachment = observe_attachment; + observer->remove_attachment = sync_attachments; + bool sent = false; + SENTRY_WITH_SCOPE_MUT (scope) { + if (sentry__scope_add_observer_with_init( + scope, observer, init_scope_observer)) { + state->scope_observer = observer; + sent = true; + } + } + return sent; +} + /** * Handle exception - called from signal handler via sentry_handle_exception * This processes the event with on_crash/before_send hooks and ends the session @@ -1379,6 +1487,9 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) native_backend_state_t *state = (native_backend_state_t *)backend->data; if (state) { sentry__atomic_store(&state->crashed, 1); + if (state->ipc) { + sentry__atomic_store(&state->ipc->stopped, 1); + } } SENTRY_WITH_OPTIONS (options) { @@ -1435,26 +1546,12 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } if (should_handle) { - bool modified = sentry__hint_is_modified(&hint); - if (modified) { - size_t len = sentry_value_get_length(hint.attachments); - for (size_t i = 0; i < len; i++) { - add_attachment(state, - sentry_value_get_by_index(hint.attachments, i)); - } - } - // Write modified hint attachments or reload the current scope - native_backend_write_attachments( - state ? state->event_path : NULL, modified ? &hint : NULL); - // Apply scope to the event. The daemon assembles breadcrumbs - // from the ring files + write_crash_attachments(state, &hint); SENTRY_WITH_SCOPE (scope) { sentry__scope_apply_to_event( - scope, options, event, SENTRY_SCOPE_NONE); + scope, options, event, SENTRY_SCOPE_BREADCRUMBS); } -#if defined(SENTRY_PLATFORM_WINDOWS) - ensure_device_arch(event); -#endif + write_crash_event(state, event); #ifndef SENTRY_SCREENSHOT_NONE // The screenshot is captured by the daemon out-of-process, so @@ -1476,24 +1573,6 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) } #endif - // Write event as JSON file - // Daemon will read this and create envelope with minidump - if (state && state->event_path) { - size_t event_json_len = 0; - char *event_json - = sentry__value_to_json(event, &event_json_len); - if (event_json) { - int rv = sentry__path_write_buffer( - state->event_path, event_json, event_json_len); - sentry_free(event_json); - if (rv == 0) { - SENTRY_DEBUG("Wrote crash event JSON for daemon"); - } else { - SENTRY_WARN("Failed to write event JSON"); - } - } - } - sentry_value_decref(event); // End session with crashed status and write session envelope to @@ -1537,7 +1616,7 @@ native_backend_except(sentry_backend_t *backend, const sentry_ucontext_t *uctx) sentry_value_decref(transaction); } if (!should_handle && ctx) { - ctx->event_path[0] = '\0'; + ctx->event_discarded = true; } sentry__hint_deinit(&hint); } @@ -1570,7 +1649,6 @@ sentry__backend_new(void) backend->shutdown_func = native_backend_shutdown; backend->free_func = native_backend_free; backend->except_func = native_backend_except; - backend->flush_scope_func = native_backend_flush_scope; backend->user_consent_changed_func = native_backend_user_consent_changed; backend->process_old_run_func = native_backend_process_old_run; backend->reinstall_func = native_backend_reinstall; diff --git a/src/integrations/sentry_integration_wer.c b/src/integrations/sentry_integration_wer.c index e7156fb9fe..11be4031f8 100644 --- a/src/integrations/sentry_integration_wer.c +++ b/src/integrations/sentry_integration_wer.c @@ -62,8 +62,9 @@ sentry_integration_wer_free(void *data) } static void -wer_set_tag(void *data, const char *key, const char *value) +wer_set_tag(void *data, const char *key, sentry_value_t tag) { + const char *value = sentry_value_as_string(tag); sentry_integration_wer_data_t *wer_data = (sentry_integration_wer_data_t *)data; if (!wer_data->WerRegisterCustomMetadata) { @@ -135,7 +136,7 @@ static int wer_sync_tag(const char *key, sentry_value_t value, void *data) { if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_STRING) { - wer_set_tag(data, key, sentry_value_as_string(value)); + wer_set_tag(data, key, value); } return 0; } diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 6d508fb620..c305b1372a 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -716,6 +716,14 @@ sentry__scope_observer_new(void) bool sentry__scope_add_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer) +{ + return sentry__scope_add_observer_with_init(scope, observer, NULL); +} + +bool +sentry__scope_add_observer_with_init(sentry_scope_t *scope, + sentry_scope_observer_t *observer, + bool (*init)(void *data, const sentry_scope_t *scope)) { if (!observer) { return false; @@ -738,6 +746,11 @@ sentry__scope_add_observer( new_array[scope->num_observers] = observer; scope->observers = new_array; scope->num_observers = new_count; + if (init && !init(observer->data, scope)) { + sentry__scope_remove_observer(scope, observer); + sentry__mutex_unlock(&scope->observers_lock); + return false; + } sentry__mutex_unlock(&scope->observers_lock); return true; } @@ -1117,6 +1130,46 @@ sentry__scope_load_propagation_context(const sentry_scope_t *scope) return propagation_context; } +static void +notify_trace(sentry_scope_t *scope) +{ + lock_scope_notify(scope); + bool observed = false; + for (size_t i = 0; i < scope->num_observers; i++) { + if (scope->observers[i] && scope->observers[i]->set_trace) { + observed = true; + break; + } + } + if (!observed) { + unlock_scope_notify(scope); + return; + } + sentry_value_t span = sentry__scope_load_span_or_transaction(scope); + sentry_value_t trace = sentry__value_get_trace_context(span); + if (!sentry_value_is_null(trace)) { + sentry_value_t data = sentry_value_get_by_key(span, "data"); + if (!sentry_value_is_null(data)) { + sentry_value_set_by_key(trace, "data", sentry_value_incref(data)); + } + } else { + sentry_value_t contexts = sentry__scope_load_contexts(scope); + trace = sentry__value_clone(sentry_value_get_by_key(contexts, "trace")); + sentry_value_decref(contexts); + sentry_value_t propagation = sentry__scope_load_trace_context(scope); + if (sentry_value_is_null(trace)) { + trace = propagation; + } else { + sentry__value_merge_objects(trace, propagation); + sentry_value_decref(propagation); + } + } + SENTRY_SCOPE_NOTIFY(scope, set_trace, trace); + sentry_value_decref(trace); + sentry_value_decref(span); + unlock_scope_notify(scope); +} + void sentry__scope_set_propagation_context( sentry_scope_t *scope, const char *key, sentry_value_t value) @@ -1124,6 +1177,7 @@ sentry__scope_set_propagation_context( SENTRY_SCOPE_WRITE_LOCK (scope->data) { sentry_value_set_by_key(scope->data->propagation_context, key, value); } + notify_trace(scope); sentry__scope_flush(scope); } @@ -1133,6 +1187,7 @@ sentry__scope_regenerate_propagation_context(sentry_scope_t *scope) SENTRY_SCOPE_WRITE_LOCK (scope->data) { generate_propagation_context(scope->data->propagation_context); } + notify_trace(scope); } sentry_value_t @@ -1158,6 +1213,7 @@ sentry__scope_set_trace_context( sentry_value_get_by_key(scope->data->propagation_context, "trace"), key, value); } + notify_trace(scope); } bool @@ -1672,17 +1728,19 @@ void sentry_scope_set_tag(sentry_scope_t *scope, const char *key, const char *value) { sentry_value_t tag_value = sentry_value_new_string(value); + sentry_value_t stored_value = sentry_value_incref(tag_value); bool did_set = false; lock_scope_notify(scope); SENTRY_SCOPE_WRITE_LOCK (scope->data) { - did_set - = sentry_value_set_by_key(scope->data->tags, key, tag_value) == 0; + did_set = sentry_value_set_by_key(scope->data->tags, key, stored_value) + == 0; } if (did_set) { - SENTRY_SCOPE_NOTIFY(scope, set_tag, key, value); + SENTRY_SCOPE_NOTIFY(scope, set_tag, key, tag_value); } unlock_scope_notify(scope); sentry__scope_flush(scope); + sentry_value_decref(tag_value); } void @@ -1705,8 +1763,7 @@ sentry_scope_set_tag_n(sentry_scope_t *scope, const char *key, size_t key_len, == 0; } if (did_set) { - SENTRY_SCOPE_NOTIFY( - scope, set_tag, notify_key, sentry_value_as_string(tag_value)); + SENTRY_SCOPE_NOTIFY(scope, set_tag, notify_key, tag_value); } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -1721,8 +1778,9 @@ set_tag_value(const char *key, sentry_value_t value, void *userdata) SENTRY_WARNF("set_tags: ignoring non-string value for tag `%s`", key); return 0; } - sentry_scope_set_tag( - (sentry_scope_t *)userdata, key, sentry_value_as_string(value)); + sentry_scope_set_tag_n((sentry_scope_t *)userdata, key, + sentry__guarded_strlen(key), sentry_value_as_string(value), + sentry_value_get_length(value)); return 0; } @@ -1975,6 +2033,9 @@ sentry_scope_set_context( } if (did_set) { SENTRY_SCOPE_NOTIFY(scope, set_context, key, value); + if (sentry__string_eq(key, "trace")) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2001,6 +2062,9 @@ sentry_scope_set_context_n(sentry_scope_t *scope, const char *key, } if (did_set) { SENTRY_SCOPE_NOTIFY(scope, set_context, notify_key, value); + if (key_len == 5 && memcmp(key, "trace", 5) == 0) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2018,6 +2082,9 @@ sentry_scope_remove_context(sentry_scope_t *scope, const char *key) } if (removed) { SENTRY_SCOPE_NOTIFY(scope, remove_context, key); + if (sentry__string_eq(key, "trace")) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2035,6 +2102,9 @@ sentry_scope_remove_context_n( } if (removed_key) { SENTRY_SCOPE_NOTIFY(scope, remove_context, removed_key); + if (key_len == 5 && memcmp(removed_key, "trace", 5) == 0) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2074,6 +2144,9 @@ sentry_scope_update_context_n(sentry_scope_t *scope, const char *key, } if (did_set) { SENTRY_SCOPE_NOTIFY(scope, set_context, notify_key, value); + if (key_len == 5 && memcmp(key, "trace", 5) == 0) { + notify_trace(scope); + } } unlock_scope_notify(scope); sentry__scope_flush(scope); @@ -2436,6 +2509,7 @@ sentry__scope_set_transaction_object( sentry__transaction_decref(scope->data->transaction_object); scope->data->transaction_object = transaction; } + notify_trace(scope); sentry__scope_flush(scope); } @@ -2452,6 +2526,7 @@ sentry__scope_remove_transaction_object( } if (removed) { sentry__transaction_decref(transaction); + notify_trace(scope); } if (removed) { sentry__scope_flush(scope); @@ -2475,6 +2550,9 @@ sentry__scope_remove_transaction_value( } } sentry__transaction_decref(transaction_object); + if (transaction_object) { + notify_trace(scope); + } return transaction_object != NULL; } @@ -2491,6 +2569,7 @@ sentry__scope_restore_transaction_object( } } if (restored) { + notify_trace(scope); sentry__scope_flush(scope); } return restored; @@ -2541,6 +2620,7 @@ sentry__scope_set_span(sentry_scope_t *scope, sentry_span_t *span) sentry__span_decref(scope->data->span); scope->data->span = span; } + notify_trace(scope); sentry__scope_flush(scope); } @@ -2556,6 +2636,7 @@ sentry__scope_remove_span(sentry_scope_t *scope, sentry_span_t *span) } if (removed) { sentry__span_decref(span); + notify_trace(scope); } if (removed) { sentry__scope_flush(scope); @@ -2578,6 +2659,7 @@ sentry__scope_remove_span_value(sentry_scope_t *scope, sentry_value_t span) } sentry__span_decref(scope_span); if (scope_span != NULL) { + notify_trace(scope); sentry__scope_flush(scope); } return scope_span != NULL; @@ -2594,6 +2676,7 @@ sentry__scope_restore_span(sentry_scope_t *scope, sentry_span_t *span) } } if (restored) { + notify_trace(scope); sentry__scope_flush(scope); } return restored; diff --git a/src/sentry_scope.h b/src/sentry_scope.h index f56e9f3e07..b3e2777324 100644 --- a/src/sentry_scope.h +++ b/src/sentry_scope.h @@ -37,7 +37,7 @@ typedef struct sentry_scope_observer_s { void (*add_breadcrumb)(void *data, sentry_value_t breadcrumb); - void (*set_tag)(void *data, const char *key, const char *value); + void (*set_tag)(void *data, const char *key, sentry_value_t value); void (*remove_tag)(void *data, const char *key); void (*set_extra)(void *data, const char *key, sentry_value_t value); @@ -48,6 +48,8 @@ typedef struct sentry_scope_observer_s { void (*add_attachment)(void *data, sentry_value_t attachment); void (*remove_attachment)(void *data, sentry_value_t attachment); + + void (*set_trace)(void *data, sentry_value_t trace); } sentry_scope_observer_t; typedef struct sentry_scope_data_s sentry_scope_data_t; @@ -249,6 +251,18 @@ sentry_scope_observer_t *sentry__scope_observer_new(void); bool sentry__scope_add_observer( sentry_scope_t *scope, sentry_scope_observer_t *observer); +/** + * Register and initialize a scope observer. + * + * Calls `init` with the observer's data and the scope after registration, + * while scope notifications are locked. The callback must only read the scope. + * Returning false removes the observer. Takes ownership of `observer`, + * freeing it if registration or initialization fails. + */ +bool sentry__scope_add_observer_with_init(sentry_scope_t *scope, + sentry_scope_observer_t *observer, + bool (*init)(void *data, const sentry_scope_t *scope)); + /** * Remove a scope observer. * diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index 1f529fb820..c8a22850cc 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -5,14 +5,12 @@ multi-thread capture, and FPU/SIMD register capture on all platforms. """ -import json import os import subprocess import sys import time import struct -import msgpack import pytest from . import ( @@ -384,21 +382,17 @@ def test_native_capture_minidump_generated(cmake, httpserver): assert not list(db_dir.glob("*.run*.lock")) -# Both daemon envelope writers merge the breadcrumb ring files: the native -# stacktrace writer builds the event from scratch, while the minidump-only -# writer re-parses the parent's event JSON, merges, and re-serializes. Exercise -# both so the minidump path's extra parse/serialize roundtrip is covered too. +# Both daemon envelope writers use the scope received over IPC. +# Exercise breadcrumbs in both native-stacktrace and minidump-only events. BREADCRUMB_CRASH_MODES = ["native", "minidump"] @pytest.mark.parametrize("crash_mode", BREADCRUMB_CRASH_MODES) def test_native_breadcrumbs(cmake, httpserver, crash_mode): - """Test that breadcrumbs survive the daemon's ring-file merge. + """Test that breadcrumbs survive the daemon's IPC scope updates. - The crashing process appends breadcrumbs as msgpack to the ring files; the - daemon reads, merges, and attaches them to the event. Asserting on the - default `debug crumb` verifies that whole roundtrip, not just that an event - arrived. + Asserting on the default `debug crumb` verifies the whole roundtrip, not + just that an event arrived. """ tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) @@ -423,7 +417,7 @@ def test_native_breadcrumbs(cmake, httpserver, crash_mode): @pytest.mark.parametrize("crash_mode", BREADCRUMB_CRASH_MODES) def test_native_overflow_breadcrumbs(cmake, httpserver, crash_mode): - """Test that the daemon caps merged breadcrumbs at max_breadcrumbs. + """Test that the daemon caps staged breadcrumbs at max_breadcrumbs. The example adds 3 default crumbs plus 101 numbered crumbs ("0".."100"). With the default max_breadcrumbs (100), the daemon keeps the newest 100, @@ -457,62 +451,61 @@ def test_native_overflow_breadcrumbs(cmake, httpserver, crash_mode): assert any(b.get("message") == "100" for b in breadcrumbs) -def test_native_attachment_manifest_is_current(cmake, httpserver): - """The attachment manifest must reflect the live attachment state (#1933). - - The daemon builds hard-crash envelopes from `/__sentry-attachments`, - which the backend only rewrites on a scope flush. `sentry_add_attachment` - adds the fully configured attachment before that flush, so the manifest - must contain all its metadata immediately. - - Asserting on the manifest rather than on a crash envelope keeps this - deterministic: the in-process crash handler happens to flush the scope (via - `sentry__trace_finish`), which repairs the manifest before the daemon reads - it, but out-of-process paths such as WER never run that handler. - """ - tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) - - cmd = run_command(str(tmp_path / "sentry_test_integration")) - child = subprocess.Popen( - [*cmd, "log", "attach-custom-filename", "sleep"], - cwd=tmp_path, - env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), +@pytest.mark.parametrize("mode", [0, 1, 2]) +@pytest.mark.parametrize("scoped_trace", [False, True]) +def test_native_scope_updates_without_snapshot(cmake, httpserver, mode, scoped_trace): + tmp_path = cmake(["sentry_test_unit", "sentry-crash"], {"SENTRY_BACKEND": "native"}) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict( + os.environ, + SENTRY_DSN=make_dsn(httpserver), + SENTRY_TEST_NATIVE_SCOPE_CRASH=str(mode), ) - db_dir = tmp_path / ".sentry-native" - - last_manifest = None - - def manifest_is_current(): - # Validate the complete expected state against a single snapshot - # instead of waiting on one field and re-reading the file. - nonlocal last_manifest - paths = list(db_dir.glob("*.run/__sentry-attachments")) - if not paths: - return False - try: - unpacker = msgpack.Unpacker(raw=False) - unpacker.feed(paths[0].read_bytes()) - manifest = list(unpacker) - except (OSError, msgpack.UnpackException): - # rewritten in place, so a read can catch a partial file - return False - last_manifest = manifest - return ( - len(manifest) == 1 - and manifest[0].get("filename") == "custom-name.log" - and manifest[0].get("content_type") == "application/zstd" - and manifest[0].get("path", "").endswith("CMakeCache.txt") + if scoped_trace: + env["SENTRY_TEST_NATIVE_SCOPE_SPAN"] = "1" + with httpserver.wait(timeout=15) as waiting: + run_crash( + tmp_path, + "sentry_test_unit", + ["--no-exec", "native_scope_updates"], + env=env, ) - - try: - up_to_date = wait_for(manifest_is_current) - finally: - child.terminate() - child.wait() - - assert ( - up_to_date - ), f"attachment manifest does not reflect the live attachment state: {last_manifest}" + assert waiting.result + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event = envelope.get_event() + assert event["release"] == "ipc-release" + assert event["environment"] == "ipc-environment" + assert event["transaction"] == "ipc-transaction" + assert event["level"] == "fatal" + assert event["fingerprint"] == ["ipc", "latest"] + assert event["user"]["username"] == "ipc-user" + assert event["user"]["id"] + assert event["tags"] == {"tag": "latest", "binary-tag": "v\0x"} + assert event["extra"]["large"] == "x" * (1024 * 1024) + assert set(event["extra"]) == {"large", "expected-trace"} + contexts = {"trace", "ipc", "os"} + if sys.platform == "win32" or is_wine: + contexts.add("device") + if is_wine: + contexts.add("wine") + assert set(event["contexts"]) == contexts + assert event["contexts"]["ipc"] == {"status": "latest"} + assert event["contexts"]["trace"] == event["extra"]["expected-trace"] + assert [crumb["index"] for crumb in event["breadcrumbs"]] == [2, 3, 4] + attachments = { + item.headers.get("filename"): item.payload.bytes + for item in envelope + if item.headers.get("type") == "attachment" + and item.headers.get("attachment_type") != "event.minidump" + } + assert attachments == {"latest.txt": b"new"} + for name in ( + "__sentry-event", + "__sentry-breadcrumb1", + "__sentry-breadcrumb2", + "__sentry-attachments", + ): + assert not list((tmp_path / ".sentry-native").glob(f"*.run/{name}")) def test_native_byte_attachment_is_confined(cmake, unreachable_dsn): @@ -1753,8 +1746,7 @@ def daemon_log(): assert "Parent process exited without crash" not in daemon_log() assert "Crash notification received, processing" not in daemon_log() assert not list(db_dir.glob("*.run/*.dmp")) - snapshot = json.loads(next(db_dir.glob("*.run/__sentry-event")).read_text()) - assert "exception" not in snapshot + assert not list(db_dir.glob("*.run/__sentry-event")) finally: if child.poll() is None: child.kill() @@ -1797,35 +1789,22 @@ def test_native_acquired_scope(cmake, httpserver, clear): def test_native_scope_snapshot(cmake, httpserver): tmp_path = cmake(["sentry_test_integration"], {"SENTRY_BACKEND": "native"}) - cmd = run_command(str(tmp_path / "sentry_test_integration")) - child = subprocess.Popen( - [*cmd, "initial-scope", "no-setup", "sleep"], - cwd=tmp_path, - env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), - ) - db_dir = tmp_path / ".sentry-native" - snapshot = None - - def scope_was_written(): - nonlocal snapshot - paths = list(db_dir.glob("*.run/__sentry-event")) - if not paths: - return False - try: - snapshot = json.loads(paths[0].read_text()) - except (OSError, json.JSONDecodeError): - return False - return snapshot.get("tags", {}).get("test.initial-tag") == "initial-value" + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") - try: - assert wait_for(scope_was_written) - assert snapshot["event_id"] - assert snapshot["level"] == "fatal" - assert "exception" not in snapshot - finally: - child.terminate() - child.wait() - assert wait_for(lambda: not list(db_dir.glob("*.run.daemon.lock"))) + with httpserver.wait(timeout=10) as waiting: + run_crash( + tmp_path, + "sentry_test_integration", + ["initial-scope", "no-setup", "crash"], + env=dict(os.environ, SENTRY_DSN=make_dsn(httpserver)), + ) + assert waiting.result + + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event = envelope.get_event() + assert event["tags"]["test.initial-tag"] == "initial-value" + assert event["event_id"] + assert event["level"] == "fatal" @pytest.mark.parametrize("crash_mode", ["native", "native-with-minidump"]) @@ -1892,3 +1871,38 @@ def test_native_symbolication(cmake, httpserver, crash_mode, symbolize): assert any( "function" in frame for frame in exc["stacktrace"]["frames"] ), "missing symbolicated function names" + + +@pytest.mark.parametrize("clear_hint", [False, True]) +def test_native_scope_updates_in_crash_callback(cmake, httpserver, clear_hint): + tmp_path = cmake(["sentry_test_unit", "sentry-crash"], {"SENTRY_BACKEND": "native"}) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + env = dict( + os.environ, + SENTRY_DSN=make_dsn(httpserver), + SENTRY_TEST_NATIVE_SCOPE_CRASH="1", + SENTRY_TEST_NATIVE_SCOPE_CALLBACK="1", + ) + if clear_hint: + env["SENTRY_TEST_NATIVE_SCOPE_HINT_CLEAR"] = "1" + with httpserver.wait(timeout=15) as waiting: + run_crash( + tmp_path, + "sentry_test_unit", + ["--no-exec", "native_scope_updates"], + env=env, + ) + assert waiting.result + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + event = envelope.get_event() + assert event["callback"] is True + assert event["tags"]["tag"] == "callback" + attachments = [ + item for item in envelope if item.headers.get("type") == "attachment" + ] + if clear_hint: + assert attachments == [] + else: + assert len(attachments) == 1 + assert attachments[0].headers["filename"] == "callback.txt" + assert attachments[0].payload.bytes == b"callback" diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index f91860946b..2d8b766dec 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -5,11 +5,17 @@ * and low-level crash handling functionality. */ +#include "sentry_backend.h" +#include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" #include "sentry_path.h" +#include "sentry_scope.h" #include "sentry_testsupport.h" +#include "sentry_tracing.h" +#include +#include #include #ifdef SENTRY_BACKEND_NATIVE @@ -924,7 +930,7 @@ SENTRY_TEST(crash_ipc_message_oversized) SKIP_TEST(); #else char oversized_len[4] = { 0 }; - uint32_t len = SENTRY_CRASH_IPC_MESSAGE_MAX_LEN; + uint32_t len = SENTRY_CRASH_IPC_MESSAGE_MAX_LEN + 1; oversized_len[0] = (char)(len & 0xffu); oversized_len[1] = (char)((len >> 8) & 0xffu); oversized_len[2] = (char)((len >> 16) & 0xffu); @@ -933,11 +939,7 @@ SENTRY_TEST(crash_ipc_message_oversized) sentry_crash_ipc_message_t message; sentry_crash_ipc_message_result_t result = sentry__crash_ipc_message_decode( oversized_len, sizeof(oversized_len), &message); -# if SIZE_MAX <= UINT32_MAX TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_OVERSIZED); -# else - TEST_CHECK_INT_EQUAL(result, SENTRY_CRASH_IPC_MESSAGE_PARTIAL); -# endif char payload = 0; char *buf = NULL; @@ -983,3 +985,481 @@ SENTRY_TEST(crash_ipc_message_large_roundtrip) sentry_free(payload); #endif } + +#ifdef SENTRY_BACKEND_NATIVE +static bool +apply_scope_message( + sentry_crash_scope_t *scope, uint16_t type, sentry_value_t value) +{ + size_t len = 0; + char *payload = sentry_value_to_msgpack(value, &len); + sentry_crash_ipc_message_t message + = { type, 0, scope->sequence + 1, payload, len }; + bool applied = sentry__crash_scope_apply(scope, &message); + sentry_free(payload); + sentry_value_decref(value); + return applied; +} + +static sentry_value_t +scope_snapshot(void) +{ + sentry_value_t snapshot = sentry_value_new_object(); + sentry_value_set_by_key(snapshot, "event", sentry_value_new_object()); + sentry_value_set_by_key(snapshot, "attachments", sentry_value_new_list()); + return snapshot; +} + +static sentry_value_t +scope_pair(const char *key, sentry_value_t value) +{ + sentry_value_t pair = sentry_value_new_list(); + sentry_value_append(pair, sentry_value_new_string(key)); + sentry_value_append(pair, value); + return pair; +} +#endif + +SENTRY_TEST(crash_scope_updates) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_scope_t scope; + TEST_ASSERT(sentry__crash_scope_init(&scope, 2)); + TEST_CHECK(!apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_object())); + TEST_ASSERT(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, scope_snapshot())); + struct { + uint16_t set; + uint16_t remove; + const char *field; + } keyed[] = { + { SENTRY_CRASH_IPC_MESSAGE_SET_TAG, SENTRY_CRASH_IPC_MESSAGE_REMOVE_TAG, + "tags" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_EXTRA, + SENTRY_CRASH_IPC_MESSAGE_REMOVE_EXTRA, "extra" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_CONTEXT, + SENTRY_CRASH_IPC_MESSAGE_REMOVE_CONTEXT, "contexts" }, + }; + for (size_t i = 0; i < sizeof(keyed) / sizeof(keyed[0]); i++) { + TEST_CHECK(apply_scope_message(&scope, keyed[i].set, + scope_pair("key", sentry_value_new_string("first")))); + TEST_CHECK(apply_scope_message( + &scope, keyed[i].remove, sentry_value_new_string("key"))); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key( + sentry_value_get_by_key(scope.event, keyed[i].field), "key"))); + TEST_CHECK(apply_scope_message(&scope, keyed[i].set, + scope_pair("key", sentry_value_new_string("last")))); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key( + sentry_value_get_by_key(scope.event, keyed[i].field), "key")), + "last"); + } + struct { + uint16_t type; + const char *field; + } fields[] = { + { SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, "release" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_ENVIRONMENT, "environment" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_TRANSACTION, "transaction" }, + { SENTRY_CRASH_IPC_MESSAGE_SET_LEVEL, "level" }, + }; + for (size_t i = 0; i < sizeof(fields) / sizeof(fields[0]); i++) { + TEST_CHECK(apply_scope_message( + &scope, fields[i].type, sentry_value_new_string_n("a\0b", 3))); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(sentry_value_get_by_key( + scope.event, fields[i].field)), + 3); + TEST_CHECK(apply_scope_message( + &scope, fields[i].type, sentry_value_new_null())); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope.event, fields[i].field))); + } + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_object())); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_null())); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT, sentry_value_new_list())); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_FINGERPRINT, sentry_value_new_null())); + for (int i = 0; i < 3; i++) { + sentry_value_t crumb = sentry_value_new_object(); + sentry_value_set_by_key(crumb, "index", sentry_value_new_int32(i)); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB, crumb)); + } + sentry_value_t event = sentry__crash_scope_event(&scope); + sentry_value_t crumbs = sentry_value_get_by_key(event, "breadcrumbs"); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(crumbs), 2); + TEST_CHECK_INT_EQUAL(sentry_value_as_int32(sentry_value_get_by_key( + sentry_value_get_by_index(crumbs, 0), "index")), + 1); + sentry_value_decref(event); + sentry_value_t attachments = sentry_value_new_list(); + sentry_value_append(attachments, sentry_value_new_object()); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, attachments)); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope.attachments), 1); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, sentry_value_new_list())); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope.attachments), 0); + TEST_CHECK(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, scope_snapshot())); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope.event), 0); + sentry_crash_ipc_message_t crash + = { SENTRY_CRASH_IPC_MESSAGE_CRASH, 0, scope.sequence + 1, NULL, 0 }; + TEST_CHECK(sentry__crash_scope_apply(&scope, &crash)); + TEST_CHECK(!apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_object())); + sentry__crash_scope_free(&scope); +#endif +} + +SENTRY_TEST(crash_scope_invalid_updates) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_scope_t scope; + TEST_ASSERT(sentry__crash_scope_init(&scope, 0)); + TEST_ASSERT(apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, scope_snapshot())); + TEST_CHECK(!apply_scope_message(&scope, SENTRY_CRASH_IPC_MESSAGE_SET_TAG, + sentry_value_new_string("invalid"))); + TEST_CHECK(!apply_scope_message( + &scope, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_int32(1))); + TEST_CHECK(!apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_SET_ATTACHMENT_LIST, + sentry_value_new_object())); + sentry_crash_ipc_message_t message + = { SENTRY_CRASH_IPC_MESSAGE_SET_USER, 0, 2, "\x81", 1 }; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + message.payload = "\xc0\xc0"; + message.payload_len = 2; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + message.payload = "\xc0"; + message.payload_len = 1; + message.sequence = 3; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + message.sequence = 2; + message.flags = 1; + TEST_CHECK(!sentry__crash_scope_apply(&scope, &message)); + TEST_CHECK_INT_EQUAL(scope.sequence, 1); + TEST_CHECK(apply_scope_message(&scope, + SENTRY_CRASH_IPC_MESSAGE_ADD_BREADCRUMB, sentry_value_new_object())); + sentry_value_t event = sentry__crash_scope_event(&scope); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(sentry_value_get_by_key(event, "breadcrumbs")), + 0); + sentry_value_decref(event); + sentry__crash_scope_free(&scope); +#endif +} + +#ifdef SENTRY_BACKEND_NATIVE +static sentry_value_t +native_scope_on_crash(const sentry_ucontext_t *UNUSED(uctx), + sentry_value_t event, sentry_hint_t *hint, void *UNUSED(data)) +{ + sentry_set_tag("tag", "callback"); + if (getenv("SENTRY_TEST_NATIVE_SCOPE_HINT_CLEAR")) { + sentry_hint_clear_attachments(hint); + } else { + sentry_clear_attachments(); + sentry_attach_bytes("callback", 8, "callback.txt"); + } + sentry_value_set_by_key(event, "callback", sentry_value_new_bool(true)); + return event; +} +#endif + +SENTRY_TEST(native_scope_updates) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + const char *crash_mode = getenv("SENTRY_TEST_NATIVE_SCOPE_CRASH"); + bool callback = getenv("SENTRY_TEST_NATIVE_SCOPE_CALLBACK") != NULL; + SENTRY_TEST_OPTIONS_NEW(options); + if (callback) { + sentry_options_set_on_crash(options, native_scope_on_crash, NULL); + } + sentry_options_set_auto_session_tracking(options, false); + sentry_options_set_max_breadcrumbs(options, 3); + sentry_options_set_traces_sample_rate(options, 1); + sentry_options_set_debug(options, true); + if (crash_mode) { + sentry_options_set_dsn(options, getenv("SENTRY_DSN")); + sentry_options_set_crash_reporting_mode(options, atoi(crash_mode)); + } + TEST_ASSERT(sentry_init(options) == 0); + sentry_set_tag("cleared", "old"); + sentry_attach_bytes("old", 3, "cleared.txt"); + SENTRY_WITH_SCOPE_MUT (scope) { + sentry_scope_clear(scope); + } + for (int i = 0; i < 1000; i++) { + sentry_set_tag("tag", "old"); + sentry_remove_tag("tag"); + } + sentry_set_tag("tag", "latest"); + sentry_set_tag_n("binary-tag-tail", 10, "v\0x", 3); + sentry_set_tag_n("gone-tag-tail", 8, "old", 3); + sentry_remove_tag_n("gone-tag-tail", 8); + sentry_set_extra_n("gone-extra-tail", 10, sentry_value_new_int32(1)); + sentry_remove_extra_n("gone-extra-tail", 10); + sentry_set_context_n("gone-context-tail", 12, sentry_value_new_object()); + sentry_remove_context_n("gone-context-tail", 12); + sentry_set_release("ipc-release"); + sentry_set_environment("ipc-environment"); + sentry_set_transaction("ipc-transaction"); + sentry_set_level(SENTRY_LEVEL_WARNING); + sentry_set_fingerprint("ipc", "latest", NULL); + sentry_value_t user = sentry_value_new_object(); + sentry_value_set_by_key( + user, "username", sentry_value_new_string("ipc-user")); + sentry_set_user(user); + sentry_set_extra("removed", sentry_value_new_int32(1)); + sentry_remove_extra("removed"); + size_t size = 1024 * 1024; + char *large = sentry_malloc(size); + TEST_ASSERT(large != NULL); + memset(large, 'x', size); + sentry_set_extra("large", sentry_value_new_string_n(large, size)); + sentry_free(large); + sentry_set_context("removed", sentry_value_new_object()); + sentry_remove_context("removed"); + sentry_value_t context = sentry_value_new_object(); + sentry_value_set_by_key( + context, "status", sentry_value_new_string("latest")); + sentry_set_context("ipc", context); + sentry_transaction_t *tx = sentry_transaction_start( + sentry_transaction_context_new("bound", "work"), + sentry_value_new_null()); + sentry_set_transaction_object(tx); + sentry_span_t *span = sentry_transaction_start_child(tx, "child", "work"); + sentry_set_span(span); + bool scoped_trace = getenv("SENTRY_TEST_NATIVE_SCOPE_SPAN") != NULL; + if (!scoped_trace) { + sentry_set_span(NULL); + } + sentry_set_context("trace", sentry_value_new_object()); + sentry_value_t trace = sentry_value_new_null(); + SENTRY_WITH_SCOPE_MUT (scope) { + sentry__scope_regenerate_propagation_context(scope); + if (scoped_trace) { + sentry_value_t active + = sentry__scope_load_span_or_transaction(scope); + trace = sentry__value_get_trace_context(active); + sentry_value_decref(active); + } else { + trace = sentry__scope_load_trace_context(scope); + } + } + sentry_set_extra("expected-trace", trace); + for (int i = 0; i < 5; i++) { + sentry_value_t crumb = sentry_value_new_breadcrumb(NULL, NULL); + sentry_value_set_by_key(crumb, "index", sentry_value_new_int32(i)); + sentry_add_breadcrumb(crumb); + } + sentry_attach_bytes("old", 3, "cleared-again.txt"); + sentry_clear_attachments(); + sentry_uuid_t removed = sentry_attach_bytes("old", 3, "removed.txt"); + sentry_attach_bytes("new", 3, "latest.txt"); + sentry_remove_attachment(removed); + SENTRY_WITH_OPTIONS (current) { + const char *names[] = { "__sentry-event", "__sentry-breadcrumb1", + "__sentry-breadcrumb2", "__sentry-attachments" }; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + sentry_path_t *path + = sentry__path_join_str(current->run->run_path, names[i]); + TEST_CHECK(!sentry__path_is_file(path)); + sentry__path_free(path); + } + if (crash_mode && !callback) { + // exercise staged IPC state without a crash-time snapshot + current->backend->except_func = NULL; + } + } + if (crash_mode) { +# if defined(SENTRY_PLATFORM_WINDOWS) + RaiseException(EXCEPTION_ACCESS_VIOLATION, 0, 0, NULL); +# else + raise(SIGSEGV); +# endif + } + sentry_span_finish(span); + sentry_transaction_finish(tx); + sentry_close(); +#endif +} + +#ifdef SENTRY_BACKEND_NATIVE +static sentry_crash_ipc_t * +new_test_ipc(void) +{ + sentry_crash_ipc_t *ipc = sentry__crash_ipc_init_app(NULL); + TEST_ASSERT(ipc != NULL); + TEST_ASSERT(sentry__crash_scope_init(&ipc->scope, 2)); + sentry_value_t snapshot = scope_snapshot(); + TEST_ASSERT(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SCOPE_SNAPSHOT, snapshot)); + sentry_value_decref(snapshot); + return ipc; +} + +static void +write_test_frame(sentry_crash_ipc_t *ipc, const char *buf, size_t len) +{ +# if defined(SENTRY_PLATFORM_WINDOWS) + DWORD written = 0; + TEST_ASSERT( + WriteFile(ipc->message_write_handle, buf, (DWORD)len, &written, NULL)); + TEST_CHECK_INT_EQUAL(written, len); +# else + TEST_CHECK_INT_EQUAL(write(ipc->message_fd[0], buf, len), len); +# endif +} +#endif + +SENTRY_TEST(crash_ipc_stream_ordering) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + sentry_value_t value = sentry_value_new_string("latest"); + TEST_CHECK(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, value)); + sentry_value_decref(value); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ipc->scope.event, "release")), + "latest"); + TEST_CHECK(!sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_null())); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_interrupted_frame) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + char *buf = NULL; + size_t len = 0; + const char payload[] = "\xa6latest"; + TEST_ASSERT( + sentry__crash_ipc_message_encode(SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, + 0, 2, payload, sizeof(payload) - 1, &buf, &len) + == SENTRY_CRASH_IPC_MESSAGE_OK); + write_test_frame(ipc, buf, 7); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + write_test_frame(ipc, buf + 7, len - 8); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + ipc->writing = 1; + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK_INT_EQUAL(ipc->scope.sequence, 1); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(ipc->scope.event, "release"))); + sentry_free(buf); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_partial_reads) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + char *buf = NULL; + size_t len = 0; + const char payload[] = "\xa6latest"; + TEST_ASSERT( + sentry__crash_ipc_message_encode(SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, + 0, 2, payload, sizeof(payload) - 1, &buf, &len) + == SENTRY_CRASH_IPC_MESSAGE_OK); + for (size_t i = 0; i < len; i++) { + write_test_frame(ipc, buf + i, 1); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + TEST_CHECK_INT_EQUAL(ipc->scope.sequence, i + 1 == len ? 2 : 1); + } + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ipc->scope.event, "release")), + "latest"); + sentry_free(buf); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_shutdown) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null())); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK(ipc->scope.stopped); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_broken_stream) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + char invalid[SENTRY_CRASH_IPC_MESSAGE_HEADER_SIZE] + = { 12, 0, 0, 0, (char)0xff, (char)0xff, 0, 0, 2 }; + write_test_frame(ipc, invalid, sizeof(invalid)); + TEST_CHECK(!sentry__crash_ipc_receive(ipc, 0)); + TEST_CHECK(ipc->message_closed); + TEST_CHECK_INT_EQUAL(ipc->scope.sequence, 1); + TEST_CHECK(!sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_USER, sentry_value_new_null())); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + sentry__crash_ipc_free(ipc); +#endif +} + +SENTRY_TEST(crash_ipc_crashing) +{ +#ifndef SENTRY_BACKEND_NATIVE + SKIP_TEST(); +#else + sentry_crash_ipc_t *ipc = new_test_ipc(); + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHING); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK(!ipc->scope.stopped); + TEST_CHECK(!ipc->notify_pending); + + sentry_value_t value = sentry_value_new_string("latest"); + TEST_CHECK(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SET_RELEASE, value)); + sentry_value_decref(value); + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHED); + sentry__crash_ipc_notify(ipc); + TEST_CHECK(sentry__crash_ipc_receive(ipc, 100)); + TEST_CHECK(ipc->scope.stopped); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ipc->scope.event, "release")), + "latest"); + sentry__crash_ipc_free(ipc); +#endif +} diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 8d312f583d..14417092e8 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -1940,20 +1940,20 @@ observe_add_breadcrumb(void *data, sentry_value_t breadcrumb) } static void -observe_set_tag(void *data, const char *key, const char *value) +observe_set_tag(void *data, const char *key, sentry_value_t value) { test_observer_data_t *d = (test_observer_data_t *)data; if (sentry_value_is_null(d->tags)) { d->tags = sentry_value_new_object(); } - sentry_value_set_by_key(d->tags, key, sentry_value_new_string(value)); + sentry_value_set_by_key(d->tags, key, sentry_value_incref(value)); d->was_called = true; d->set_tag_count++; } static void observe_set_tag_remove_self_and_add( - void *data, const char *key, const char *value) + void *data, const char *key, sentry_value_t value) { reentrant_observer_data_t *d = (reentrant_observer_data_t *)data; observe_set_tag(d->self_data, key, value); @@ -1967,7 +1967,7 @@ observe_set_tag_remove_self_and_add( } static void -observe_set_tag_remove_self(void *data, const char *key, const char *value) +observe_set_tag_remove_self(void *data, const char *key, sentry_value_t value) { reentrant_observer_data_t *d = (reentrant_observer_data_t *)data; observe_set_tag(d->self_data, key, value); @@ -1977,7 +1977,7 @@ observe_set_tag_remove_self(void *data, const char *key, const char *value) } static void -observe_set_tag_clear_scope(void *data, const char *key, const char *value) +observe_set_tag_clear_scope(void *data, const char *key, sentry_value_t value) { clear_observer_data_t *d = (clear_observer_data_t *)data; observe_set_tag(d->data, key, value); @@ -1986,7 +1986,7 @@ observe_set_tag_clear_scope(void *data, const char *key, const char *value) static void observe_set_tag_mutate_nested_scope( - void *data, const char *UNUSED(key), const char *UNUSED(value)) + void *data, const char *UNUSED(key), sentry_value_t UNUSED(value)) { deferred_flush_observer_data_t *d = (deferred_flush_observer_data_t *)data; d->was_called = true; @@ -4377,3 +4377,157 @@ SENTRY_TEST(scope_parent_lock) sentry_scope_release(parent); sentry_scope_free(scope); } + +typedef struct { + sentry_scope_t *scope; + sentry_waitable_flag_t started; + sentry_waitable_flag_t finished; + sentry_waitable_flag_t read_finished; + sentry_threadid_t writer; + sentry_threadid_t reader; + volatile long calls; +} trace_notify_state_t; + +static void +observe_trace(void *data, sentry_value_t UNUSED(trace)) +{ + trace_notify_state_t *state = data; + sentry__atomic_fetch_and_add(&state->calls, 1); +} + +SENTRY_THREAD_FN +notify_scope_trace(void *data) +{ + trace_notify_state_t *state = data; + sentry__waitable_flag_set(&state->started); + sentry_scope_set_span(state->scope, NULL); + sentry__waitable_flag_set(&state->finished); + return 0; +} + +SENTRY_THREAD_FN +read_scope_during_notify(void *data) +{ + trace_notify_state_t *state = data; + sentry_value_decref(sentry__scope_load_tags(state->scope)); + sentry__waitable_flag_set(&state->read_finished); + return 0; +} + +static void +observe_tag_with_trace( + void *data, const char *UNUSED(key), sentry_value_t UNUSED(value)) +{ + trace_notify_state_t *state = data; + TEST_ASSERT( + !sentry__thread_spawn(&state->writer, notify_scope_trace, state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state->started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state->finished, 100)); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state->calls), 0); + + TEST_ASSERT( + !sentry__thread_spawn(&state->reader, read_scope_during_notify, state)); + TEST_CHECK(sentry__waitable_flag_wait(&state->read_finished, 1000)); +} + +SENTRY_TEST(scope_observer_trace_serialized) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + trace_notify_state_t state = { .scope = scope }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + sentry__waitable_flag_init(&state.read_finished); + sentry__thread_init(&state.writer); + sentry__thread_init(&state.reader); + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + TEST_ASSERT(!!observer); + observer->data = &state; + observer->set_tag = observe_tag_with_trace; + observer->set_trace = observe_trace; + + TEST_ASSERT(sentry__scope_add_observer(scope, observer)); + sentry_scope_set_tag(scope, "tag", "value"); + + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(state.writer); + sentry__thread_join(state.reader); + sentry__thread_free(&state.writer); + sentry__thread_free(&state.reader); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.calls), 1); + sentry_scope_free(scope); +} + +static bool +init_trace_observer(void *data, const sentry_scope_t *scope) +{ + trace_notify_state_t *state = data; + TEST_CHECK(scope == state->scope); + TEST_ASSERT(scope->num_observers == 1); + TEST_CHECK(scope->observers[0]->data == data); + TEST_ASSERT( + !sentry__thread_spawn(&state->writer, notify_scope_trace, state)); + TEST_ASSERT(sentry__waitable_flag_wait(&state->started, 1000)); + TEST_CHECK(!sentry__waitable_flag_wait(&state->finished, 100)); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state->calls), 0); + + TEST_ASSERT( + !sentry__thread_spawn(&state->reader, read_scope_during_notify, state)); + TEST_CHECK(sentry__waitable_flag_wait(&state->read_finished, 1000)); + return true; +} + +SENTRY_TEST(scope_observer_init) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + trace_notify_state_t state = { .scope = scope }; + sentry__waitable_flag_init(&state.started); + sentry__waitable_flag_init(&state.finished); + sentry__waitable_flag_init(&state.read_finished); + sentry__thread_init(&state.writer); + sentry__thread_init(&state.reader); + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + TEST_ASSERT(!!observer); + observer->data = &state; + observer->set_trace = observe_trace; + + TEST_ASSERT(sentry__scope_add_observer_with_init( + scope, observer, init_trace_observer)); + + TEST_CHECK(sentry__waitable_flag_wait(&state.finished, 1000)); + sentry__thread_join(state.writer); + sentry__thread_join(state.reader); + sentry__thread_free(&state.writer); + sentry__thread_free(&state.reader); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.calls), 1); + sentry_scope_free(scope); +} + +static bool +reject_scope_observer(void *data, const sentry_scope_t *scope) +{ + TEST_CHECK_INT_EQUAL(scope->num_observers, 2); + *(bool *)data = true; + return false; +} + +SENTRY_TEST(scope_observer_init_failure) +{ + sentry_scope_t *scope = sentry_scope_new(); + TEST_ASSERT(!!scope); + sentry_scope_observer_t *existing = sentry__scope_observer_new(); + TEST_ASSERT(!!existing); + TEST_ASSERT(sentry__scope_add_observer(scope, existing)); + + bool initialized = false; + sentry_scope_observer_t *observer = sentry__scope_observer_new(); + TEST_ASSERT(!!observer); + observer->data = &initialized; + TEST_CHECK(!sentry__scope_add_observer_with_init( + scope, observer, reject_scope_observer)); + TEST_CHECK(initialized); + TEST_CHECK_INT_EQUAL(scope->num_observers, 1); + TEST_CHECK(scope->observers[0] == existing); + sentry_scope_free(scope); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 4af1367126..918734cb76 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -142,13 +142,21 @@ XX(crash_context_init) XX(crash_context_null_options) XX(crash_context_options_propagation) XX(crash_context_transport_fields) +XX(crash_ipc_broken_stream) +XX(crash_ipc_crashing) +XX(crash_ipc_interrupted_frame) XX(crash_ipc_message_invalid_lengths) XX(crash_ipc_message_large_roundtrip) XX(crash_ipc_message_oversized) XX(crash_ipc_message_partial) XX(crash_ipc_message_roundtrip) XX(crash_ipc_message_unknown_type) +XX(crash_ipc_partial_reads) +XX(crash_ipc_shutdown) +XX(crash_ipc_stream_ordering) XX(crash_marker) +XX(crash_scope_invalid_updates) +XX(crash_scope_updates) XX(crashed_last_run) XX(custom_logger) XX(daemon_adopts_existing_run) @@ -306,6 +314,7 @@ XX(mpack_newlines) XX(mpack_removed_tags) XX(multiple_inits) XX(multiple_transactions) +XX(native_scope_updates) XX(old_run_retry) XX(on_crashed_last_run) XX(on_crashed_last_run_cache) @@ -439,12 +448,15 @@ XX(scope_observer_deferred_flush) XX(scope_observer_environment) XX(scope_observer_extras) XX(scope_observer_fingerprint) +XX(scope_observer_init) +XX(scope_observer_init_failure) XX(scope_observer_level) XX(scope_observer_multiple) XX(scope_observer_mutate) XX(scope_observer_null) XX(scope_observer_release) XX(scope_observer_tags) +XX(scope_observer_trace_serialized) XX(scope_observer_transaction) XX(scope_observer_user) XX(scope_ownership) From 9b85087f89781ebf59fd5f1dda75a9bbfc209c34 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 16 Oct 2026 10:11:11 +0200 Subject: [PATCH 2/3] handle SENTRY_CRASH_STATE_CRASHING --- src/backends/native/sentry_crash_daemon.c | 2 +- src/backends/sentry_backend_native.c | 6 +- tests/test_integration_native.py | 17 ++++++ tests/unit/test_native_backend.c | 69 +++++++++++++++++++++++ tests/unit/tests.inc | 1 + 5 files changed, 91 insertions(+), 4 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index e9b9877e8d..3d983af555 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -4683,7 +4683,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle, SENTRY_DEBUG("Spurious notification or already processed"); } - if (ipc->scope.stopped) { + if (ipc->scope.stopped && state != SENTRY_CRASH_STATE_CRASHING) { break; } if (ipc->message_closed && parent_alive) { diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index c3da50cc3f..8291aae52d 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -1031,13 +1031,13 @@ native_backend_shutdown(sentry_backend_t *backend) wer_unregister_module(); #endif - sentry__crash_ipc_send( - state->ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null()); - // Shutdown crash handlers (signal handlers on Linux/macOS, Mach exception // handler on iOS) sentry__crash_handler_shutdown(); + sentry__crash_ipc_send( + state->ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null()); + bool daemon_stopped = false; #if defined(SENTRY_PLATFORM_UNIX) && !defined(SENTRY_PLATFORM_IOS) if (state->daemon_pid > 0) { diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index c8a22850cc..e77457c384 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -1906,3 +1906,20 @@ def test_native_scope_updates_in_crash_callback(cmake, httpserver, clear_hint): assert len(attachments) == 1 assert attachments[0].headers["filename"] == "callback.txt" assert attachments[0].payload.bytes == b"callback" + + +@pytest.mark.skipif( + sys.platform != "linux" or is_wine, + reason="Test uses Linux daemon startup", +) +def test_native_shutdown_during_crash(cmake): + tmp_path = cmake( + ["sentry_test_unit", "sentry-crash"], + {"SENTRY_BACKEND": "native", "SENTRY_TRANSPORT": "none"}, + ) + run( + tmp_path, + "sentry_test_unit", + ["--no-exec", "crash_daemon_shutdown_during_crash"], + env=dict(os.environ, SENTRY_TEST_NATIVE_DAEMON=str(tmp_path / "sentry-crash")), + ) diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 2d8b766dec..362bc8efe0 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -48,6 +48,7 @@ count_sent_envelopes(sentry_envelope_t *envelope, void *state) # include # include # include +# include # include #endif @@ -1463,3 +1464,71 @@ SENTRY_TEST(crash_ipc_crashing) sentry__crash_ipc_free(ipc); #endif } + +SENTRY_TEST(crash_daemon_shutdown_during_crash) +{ +#if !defined(SENTRY_BACKEND_NATIVE) || !defined(SENTRY_PLATFORM_LINUX) + SKIP_TEST(); +#else + const char *daemon = getenv("SENTRY_TEST_NATIVE_DAEMON"); + if (!daemon) { + SKIP_TEST(); + } + SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(sentry__path_create_dir_all(options->database_path) == 0); + sentry_run_t *run = sentry__run_new(options->database_path); + TEST_ASSERT(run != NULL); + sentry_crash_ipc_t *ipc = new_test_ipc(); + snprintf(ipc->shmem->database_path, sizeof(ipc->shmem->database_path), "%s", + options->database_path->path); + snprintf(ipc->shmem->run_path, sizeof(ipc->shmem->run_path), "%s", + run->run_path->path); + ipc->shmem->event_discarded = true; + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHING); + TEST_ASSERT(sentry__crash_ipc_send( + ipc, SENTRY_CRASH_IPC_MESSAGE_SHUTDOWN, sentry_value_new_null())); + + char pid[32], tid[32], notify[32], ready[32], message[32]; + snprintf(pid, sizeof(pid), "%d", (int)getpid()); + snprintf(tid, sizeof(tid), "%llx", (unsigned long long)pthread_self()); + snprintf(notify, sizeof(notify), "%d", ipc->notify_fd); + snprintf(ready, sizeof(ready), "%d", ipc->ready_fd); + snprintf(message, sizeof(message), "%d", ipc->message_fd[1]); + pid_t child = fork(); + TEST_ASSERT(child >= 0); + if (child == 0) { + int fds[] = { ipc->notify_fd, ipc->ready_fd, ipc->message_fd[1] }; + for (size_t i = 0; i < sizeof(fds) / sizeof(fds[0]); i++) { + fcntl(fds[i], F_SETFD, 0); + } + execl(daemon, daemon, pid, tid, notify, ready, message, NULL); + _exit(1); + } + TEST_CHECK(sentry__crash_ipc_wait_for_ready(ipc, 5000)); + sleep_ms(100); + TEST_CHECK_INT_EQUAL(waitpid(child, NULL, WNOHANG), 0); + + sentry__atomic_store(&ipc->shmem->state, SENTRY_CRASH_STATE_CRASHED); + sentry__crash_ipc_notify(ipc); + int status = 0; + pid_t result = 0; + for (int i = 0; i < 500; i++) { + result = waitpid(child, &status, WNOHANG); + if (result != 0) { + break; + } + sleep_ms(10); + } + if (result == 0) { + kill(child, SIGKILL); + waitpid(child, &status, 0); + } + TEST_CHECK_INT_EQUAL(result, child); + TEST_CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0); + TEST_CHECK_INT_EQUAL( + sentry__atomic_fetch(&ipc->shmem->state), SENTRY_CRASH_STATE_DONE); + sentry__crash_ipc_free(ipc); + sentry__run_free(run); + sentry_options_free(options); +#endif +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 918734cb76..e08f3e4fa6 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -142,6 +142,7 @@ XX(crash_context_init) XX(crash_context_null_options) XX(crash_context_options_propagation) XX(crash_context_transport_fields) +XX(crash_daemon_shutdown_during_crash) XX(crash_ipc_broken_stream) XX(crash_ipc_crashing) XX(crash_ipc_interrupted_frame) From f00c436006098ccd98204bf0ec96a1fca2c6399a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 16 Oct 2026 11:33:03 +0200 Subject: [PATCH 3/3] signal.h --- tests/unit/test_native_backend.c | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/test_native_backend.c b/tests/unit/test_native_backend.c index 362bc8efe0..d74933021e 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -14,7 +14,6 @@ #include "sentry_scope.h" #include "sentry_testsupport.h" #include "sentry_tracing.h" -#include #include #include