diff --git a/CHANGELOG.md b/CHANGELOG.md index 26067c939..8cb9fdc9c 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 627c415d4..4f5e30e17 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 ce9437bae..3d983af55 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 && state != SENTRY_CRASH_STATE_CRASHING) { + 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 94cdd0eef..fb123ffd2 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 dbefd8b3b..6704dbc60 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 64dc467dc..8291aae52 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; } @@ -1099,16 +1035,26 @@ native_backend_shutdown(sentry_backend_t *backend) // 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) - // 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 e7156fb9f..11be4031f 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 6d508fb62..c305b1372 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 f56e9f3e0..b3e277732 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 1f529fb82..e77457c38 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,55 @@ 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" + + +@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 f91860946..d74933021 100644 --- a/tests/unit/test_native_backend.c +++ b/tests/unit/test_native_backend.c @@ -5,11 +5,16 @@ * 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 #ifdef SENTRY_BACKEND_NATIVE @@ -42,6 +47,7 @@ count_sent_envelopes(sentry_envelope_t *envelope, void *state) # include # include # include +# include # include #endif @@ -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,549 @@ 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 +} + +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/test_scope.c b/tests/unit/test_scope.c index 8d312f583..14417092e 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 4af136712..e08f3e4fa 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -142,13 +142,22 @@ 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) 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 +315,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 +449,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)