From 6b691ac4d34195504dfa5ba8354764f1f273c892 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 17 Sep 2026 17:59:17 +0200 Subject: [PATCH 1/7] ref: attachment manifest Use a shared MessagePack stream for attachment manifests. The native backend uses the manifest for attachments generally, while both out-of-process crash handlers can also use it for crash-time hint attachments that cannot cross backend IPC. This lets Crashpad consume those attachments without adding JSON support. Stream attachment objects directly to avoid format-specific conversion and buffer the complete manifest for a single file write. Median release benchmarks measured write/read improvements over legacy JSON of 17%/6% for one attachment, 74%/17% for 10, and 86%/24% for 100: 1 attachment 10 attachments 100 attachments MessagePack write 2.3 us 2.4 us 11.8 us JSON write 2.7 us 8.9 us 82.6 us MessagePack read 1.3 us 5.6 us 54.1 us JSON read 1.4 us 6.8 us 71.1 us Keep legacy JSON compatibility private to the native daemon so it can still consume manifests written by older SDK versions. --- external/crashpad | 2 +- src/backends/native/sentry_crash_daemon.c | 238 +++++++++------------- src/backends/sentry_backend_crashpad.cpp | 40 +--- src/backends/sentry_backend_native.c | 84 +------- src/sentry_attachment.c | 137 +++++++++++++ src/sentry_attachment.h | 20 ++ tests/assertions.py | 1 + tests/test_integration_native.py | 7 +- tests/unit/test_attachments.c | 108 ++++++++++ tests/unit/tests.inc | 1 + 10 files changed, 383 insertions(+), 255 deletions(-) diff --git a/external/crashpad b/external/crashpad index 95733c1ee..60be52d4a 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 95733c1ee6fe77fc82ae6bb1bac2c34b9a6d0c17 +Subproject commit 60be52d4aa63db37d68a1bc81aa95182fc878e78 diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index a81e552f3..aab3822d2 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -364,6 +364,103 @@ attachment_is_placeholder(const sentry_options_t *options, const char *path) return is_placeholder; } +static sentry_value_t +read_legacy_manifest(const char *buf, size_t buf_len) +{ + sentry_value_t legacy = sentry__value_from_json(buf, buf_len); + if (sentry_value_get_type(legacy) != SENTRY_VALUE_TYPE_LIST) { + sentry_value_decref(legacy); + return sentry_value_new_null(); + } + + sentry_value_t attachments = sentry_value_new_list(); + size_t len = sentry_value_get_length(legacy); + for (size_t i = 0; i < len; i++) { + sentry_value_t info = sentry_value_get_by_index(legacy, i); + const char *path + = sentry_value_as_string(sentry_value_get_by_key(info, "path")); + const char *filename + = sentry_value_as_string(sentry_value_get_by_key(info, "filename")); + if (sentry__string_empty(path) || sentry__string_empty(filename)) { + continue; + } + sentry_value_t attachment = sentry__attachment_from_file(path); + if (sentry_value_is_null(attachment)) { + continue; + } + sentry_attachment_set_filename(attachment, filename); + sentry_attachment_set_type(attachment, + sentry_value_as_string( + sentry_value_get_by_key(info, "attachment_type"))); + sentry_attachment_set_content_type(attachment, + sentry_value_as_string( + sentry_value_get_by_key(info, "content_type"))); + sentry_value_append(attachments, attachment); + } + sentry_value_decref(legacy); + return attachments; +} + +static sentry_value_t +read_attachment_manifest(const sentry_path_t *run_folder) +{ + sentry_path_t *path + = sentry__path_join_str(run_folder, "__sentry-attachments"); + if (!path) { + return sentry_value_new_null(); + } + sentry_value_t attachments = sentry__read_attachment_manifest(path); + if (!sentry_value_is_null(attachments)) { + sentry__path_free(path); + return attachments; + } + sentry_value_decref(attachments); + + size_t buf_len = 0; + char *buf = sentry__path_read_to_buffer(path, &buf_len); + sentry__path_free(path); + if (!buf) { + return sentry_value_new_null(); + } + const char *start = buf; + const char *end = buf + buf_len; + while (start < end + && (*start == ' ' || *start == '\t' || *start == '\r' + || *start == '\n')) { + start++; + } + const char *trimmed_end = end; + while (trimmed_end > start + && (trimmed_end[-1] == ' ' || trimmed_end[-1] == '\t' + || trimmed_end[-1] == '\r' || trimmed_end[-1] == '\n')) { + trimmed_end--; + } + attachments = start < trimmed_end && *start == '[' && trimmed_end[-1] == ']' + ? read_legacy_manifest(start, (size_t)(trimmed_end - start)) + : sentry_value_new_null(); + sentry_free(buf); + return attachments; +} + +static void +write_attachments_from_manifest( + int fd, const sentry_options_t *options, 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_value_decref(attachments); +} + // For each large attachment listed in `/__sentry-attachments`, // cache it as an attachment-ref item. Small attachments were already inlined // during envelope writing. @@ -375,23 +472,7 @@ add_attachment_refs(sentry_envelope_t *envelope, || !options->enable_large_attachments || !run_folder) { return; } - sentry_path_t *attach_list_path - = sentry__path_join_str(run_folder, "__sentry-attachments"); - if (!attach_list_path) { - SENTRY_WARN("Failed to resolve attachment manifest path"); - return; - } - size_t attach_json_len = 0; - char *attach_json - = sentry__path_read_to_buffer(attach_list_path, &attach_json_len); - sentry__path_free(attach_list_path); - if (!attach_json) { - return; - } - sentry_value_t list = attach_json_len > 0 - ? sentry__value_from_json(attach_json, attach_json_len) - : sentry_value_new_null(); - sentry_free(attach_json); + sentry_value_t list = read_attachment_manifest(run_folder); if (sentry_value_is_null(list)) { SENTRY_WARN("Failed to parse attachment manifest"); return; @@ -400,34 +481,12 @@ add_attachment_refs(sentry_envelope_t *envelope, bool materialized = false; size_t len = sentry_value_get_length(list); for (size_t i = 0; i < len; i++) { - sentry_value_t info = sentry_value_get_by_index(list, i); - const char *path - = sentry_value_as_string(sentry_value_get_by_key(info, "path")); - const char *filename - = sentry_value_as_string(sentry_value_get_by_key(info, "filename")); - const char *attachment_type = sentry_value_as_string( - sentry_value_get_by_key(info, "attachment_type")); - const char *content_type = sentry_value_as_string( - sentry_value_get_by_key(info, "content_type")); - if (sentry__string_empty(path) || sentry__string_empty(filename)) { - SENTRY_WARN("Skipping malformed attachment manifest entry"); - continue; - } - sentry_value_t attachment = sentry__attachment_from_file(path); - if (sentry_value_is_null(attachment)) { - SENTRY_WARNF("Failed to allocate attachment paths for: %s", path); - continue; - } - sentry_attachment_set_filename(attachment, filename); - sentry_attachment_set_type(attachment, attachment_type); - sentry_attachment_set_content_type(attachment, content_type); + sentry_value_t attachment = sentry_value_get_by_index(list, i); if (!sentry__attachment_is_placeholder(attachment, options)) { - sentry_value_decref(attachment); continue; } if (!materialized && !sentry__envelope_materialize(envelope)) { SENTRY_WARN("Failed to materialize envelope for attachment-refs"); - sentry_value_decref(attachment); break; } materialized = true; @@ -435,7 +494,6 @@ add_attachment_refs(sentry_envelope_t *envelope, envelope, attachment, options->run->cache_path, NULL)) { SENTRY_WARN("failed to cache attachment-ref"); } - sentry_value_decref(attachment); } sentry_value_decref(list); } @@ -3927,54 +3985,7 @@ write_envelope_with_native_stacktrace(const sentry_options_t *options, // Add scope attachments using metadata file if (run_folder) { - sentry_path_t *attach_list_path - = sentry__path_join_str(run_folder, "__sentry-attachments"); - if (attach_list_path) { - size_t attach_json_len = 0; - char *attach_json = sentry__path_read_to_buffer( - attach_list_path, &attach_json_len); - sentry__path_free(attach_list_path); - - if (attach_json && attach_json_len > 0) { - // Parse attachment list JSON - sentry_value_t attach_list - = sentry__value_from_json(attach_json, attach_json_len); - sentry_free(attach_json); - - if (!sentry_value_is_null(attach_list)) { - size_t len = sentry_value_get_length(attach_list); - for (size_t i = 0; i < len; i++) { - sentry_value_t attach_info - = sentry_value_get_by_index(attach_list, 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); - } - } - sentry_value_decref(attach_list); - } - } - } + write_attachments_from_manifest(fd, options, run_folder); } // Add screenshot attachment if captured by the daemon @@ -4204,54 +4215,7 @@ write_envelope_with_minidump(const sentry_options_t *options, // Add scope attachments using metadata file if (run_folder) { - sentry_path_t *attach_list_path - = sentry__path_join_str(run_folder, "__sentry-attachments"); - if (attach_list_path) { - size_t attach_json_len = 0; - char *attach_json = sentry__path_read_to_buffer( - attach_list_path, &attach_json_len); - sentry__path_free(attach_list_path); - - if (attach_json && attach_json_len > 0) { - // Parse attachment list JSON - sentry_value_t attach_list - = sentry__value_from_json(attach_json, attach_json_len); - sentry_free(attach_json); - - if (!sentry_value_is_null(attach_list)) { - size_t len = sentry_value_get_length(attach_list); - for (size_t i = 0; i < len; i++) { - sentry_value_t attach_info - = sentry_value_get_by_index(attach_list, 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); - } - } - sentry_value_decref(attach_list); - } - } - } + write_attachments_from_manifest(fd, options, run_folder); } // Add screenshot attachment if captured by the daemon diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 7133fa2f1..33a3d8a29 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -153,8 +153,6 @@ typedef struct { static void crashpad_backend_add_breadcrumb(sentry_backend_t *backend, sentry_value_t breadcrumb, const sentry_options_t *options); -static sentry_path_t *make_attachment_path( - const sentry_path_t *run_path, sentry_value_t attachment); /** * Correctly destruct C++ members of the crashpad state. @@ -405,7 +403,8 @@ prepare_initial_attachment( { size_t bytes_len = 0; const char *bytes = sentry__attachment_get_bytes(attachment, &bytes_len); - sentry_path_t *path = make_attachment_path(run_path, attachment); + sentry_path_t *path + = sentry__attachment_make_run_path(run_path, attachment); if (!path) { return nullptr; } @@ -883,35 +882,6 @@ process_completed_reports( #if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX) \ || defined(SENTRY_PLATFORM_MACOS) -static sentry_path_t * -make_attachment_path(const sentry_path_t *run_path, sentry_value_t attachment) -{ - if (!sentry__attachment_get_bytes(attachment, nullptr)) { - return sentry__attachment_make_path(attachment); - } - - sentry_uuid_t id = sentry__attachment_get_id(attachment); - const char *filename = sentry__attachment_get_filename(attachment); - if (!run_path || sentry_uuid_is_nil(&id) - || sentry__string_empty(filename)) { - return nullptr; - } - - char uuid[37]; - sentry_uuid_as_string(&id, uuid); - sentry_path_t *dir = sentry__path_join_str(run_path, uuid); - sentry_path_t *path = dir ? sentry__path_join_str(dir, filename) : nullptr; - sentry_path_t *parent = path ? sentry__path_dir(path) : nullptr; - bool valid = parent && sentry__path_eq(parent, dir); - sentry__path_free(parent); - sentry__path_free(dir); - if (!valid) { - sentry__path_free(path); - return nullptr; - } - return path; -} - static void add_attachment(void *state, sentry_value_t attachment) { @@ -922,7 +892,8 @@ add_attachment(void *state, sentry_value_t attachment) size_t bytes_len = 0; const char *bytes = sentry__attachment_get_bytes(attachment, &bytes_len); - sentry_path_t *path = make_attachment_path(data->run_path, attachment); + sentry_path_t *path + = sentry__attachment_make_run_path(data->run_path, attachment); if (!path) { const char *filename = sentry__attachment_get_filename(attachment); SENTRY_WARNF("failed to create path for crashpad attachment \"%s\"", @@ -953,7 +924,8 @@ remove_attachment(void *state, sentry_value_t attachment) if (!data || !data->client) { return; } - sentry_path_t *path = make_attachment_path(data->run_path, attachment); + sentry_path_t *path + = sentry__attachment_make_run_path(data->run_path, attachment); if (!path) { return; } diff --git a/src/backends/sentry_backend_native.c b/src/backends/sentry_backend_native.c index 974c66b0e..e70f5502d 100644 --- a/src/backends/sentry_backend_native.c +++ b/src/backends/sentry_backend_native.c @@ -245,39 +245,6 @@ native_backend_preload_scope( sentry_value_decref(breadcrumbs); } -/** - * Creates an attachment path, deriving a unique path in the run directory for - * buffer attachments. - */ -static sentry_path_t * -make_attachment_path(const sentry_path_t *run_path, sentry_value_t attachment) -{ - if (!sentry__attachment_get_bytes(attachment, NULL)) { - return sentry__attachment_make_path(attachment); - } - - sentry_uuid_t id = sentry__attachment_get_id(attachment); - const char *filename = sentry__attachment_get_filename(attachment); - if (!run_path || sentry_uuid_is_nil(&id) - || sentry__string_empty(filename)) { - return NULL; - } - - char uuid[37]; - sentry_uuid_as_string(&id, uuid); - sentry_path_t *dir = sentry__path_join_str(run_path, uuid); - sentry_path_t *path = dir ? sentry__path_join_str(dir, filename) : NULL; - sentry_path_t *parent = path ? sentry__path_dir(path) : NULL; - bool valid = parent && sentry__path_eq(parent, dir); - sentry__path_free(parent); - sentry__path_free(dir); - if (!valid) { - sentry__path_free(path); - return NULL; - } - return path; -} - static void add_attachment(void *data, sentry_value_t attachment) { @@ -291,7 +258,8 @@ add_attachment(void *data, sentry_value_t attachment) size_t bytes_len = 0; const char *bytes = sentry__attachment_get_bytes(attachment, &bytes_len); if (bytes) { - sentry_path_t *path = make_attachment_path(state->run_path, attachment); + sentry_path_t *path + = sentry__attachment_make_run_path(state->run_path, attachment); if (!path) { const char *filename = sentry__attachment_get_filename(attachment); SENTRY_WARNF("failed to create path for native backend attachment " @@ -1222,53 +1190,7 @@ native_backend_write_attachments(const sentry_path_t *event_path) sentry_path_t *attach_list_path = sentry__path_join_str(run_path, "__sentry-attachments"); if (attach_list_path) { - 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 - = make_attachment_path(run_path, attachment); - if (!path) { - continue; - } - // 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_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)); - } - 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); - } - size_t attach_json_len = 0; - char *attach_json - = sentry__value_to_json(attach_list, &attach_json_len); - sentry_value_decref(attach_list); - if (attach_json) { - sentry__path_write_buffer( - attach_list_path, attach_json, attach_json_len); - sentry_free(attach_json); - } + sentry__write_attachment_manifest(attach_list_path, attachments); sentry__path_free(attach_list_path); } sentry__path_free(run_path); diff --git a/src/sentry_attachment.c b/src/sentry_attachment.c index 11a7791d4..5011844f7 100644 --- a/src/sentry_attachment.c +++ b/src/sentry_attachment.c @@ -1,6 +1,7 @@ #include "sentry_attachment.h" #include "sentry_alloc.h" #include "sentry_logger.h" +#include "sentry_mpack.h" #include "sentry_options.h" #include "sentry_path.h" #include "sentry_string.h" @@ -401,6 +402,36 @@ sentry__attachment_make_path(sentry_value_t attachment) return sentry__path_from_str(sentry__attachment_get_path(attachment)); } +sentry_path_t * +sentry__attachment_make_run_path( + const sentry_path_t *run_path, sentry_value_t attachment) +{ + if (!sentry__attachment_get_bytes(attachment, NULL)) { + return sentry__attachment_make_path(attachment); + } + + sentry_uuid_t id = sentry__attachment_get_id(attachment); + const char *filename = sentry__attachment_get_filename(attachment); + if (!run_path || sentry_uuid_is_nil(&id) + || sentry__string_empty(filename)) { + return NULL; + } + + char uuid[37]; + sentry_uuid_as_string(&id, uuid); + sentry_path_t *dir = sentry__path_join_str(run_path, uuid); + sentry_path_t *path = dir ? sentry__path_join_str(dir, filename) : NULL; + sentry_path_t *parent = path ? sentry__path_dir(path) : NULL; + bool valid = parent && sentry__path_eq(parent, dir); + sentry__path_free(parent); + sentry__path_free(dir); + if (!valid) { + sentry__path_free(path); + return NULL; + } + return path; +} + bool sentry__attachment_is_placeholder( sentry_value_t attachment, const sentry_options_t *options) @@ -662,3 +693,109 @@ sentry__attachments_clone(sentry_value_t attachments) } return clone; } + +static sentry_value_t +read_manifest(const char *buf, size_t buf_len) +{ + if (buf_len == 0) { + return sentry_value_new_list(); + } + + sentry_value_t attachments + = sentry__value_from_msgpack_stream(buf, buf_len); + 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); + if (sentry_value_get_type(attachment) != SENTRY_VALUE_TYPE_OBJECT + || sentry__string_empty(sentry__attachment_get_path(attachment)) + || sentry__string_empty( + sentry__attachment_get_filename(attachment))) { + sentry_value_decref(attachments); + return sentry_value_new_null(); + } + } + return attachments; +} + +sentry_value_t +sentry__read_attachment_manifest(const sentry_path_t *path) +{ + if (!path) { + return sentry_value_new_null(); + } + size_t buf_len = 0; + char *buf = sentry__path_read_to_buffer(path, &buf_len); + if (!buf) { + return sentry_value_new_null(); + } + sentry_value_t attachments = read_manifest(buf, buf_len); + sentry_free(buf); + return attachments; +} + +bool +sentry__write_attachment_manifest( + const sentry_path_t *path, sentry_value_t attachments) +{ + if (!path) { + return false; + } + + sentry_path_t *run_path = sentry__path_dir(path); + if (!run_path) { + return false; + } + + const char *keys[] = { ATTACHMENT_ID, ATTACHMENT_FILENAME, ATTACHMENT_TYPE, + ATTACHMENT_CONTENT_TYPE }; + mpack_writer_t writer; + char *buf = NULL; + size_t buf_len = 0; + mpack_writer_init_growable(&writer, &buf, &buf_len); + size_t len = sentry_value_get_length(attachments); + for (size_t i = 0; i < len && mpack_writer_error(&writer) == mpack_ok; + i++) { + sentry_value_t attachment = sentry_value_get_by_index(attachments, i); + sentry_path_t *attachment_path + = sentry__attachment_make_run_path(run_path, attachment); + if (!attachment_path) { + continue; + } + + // skip missing or partially written attachments + size_t bytes_len = 0; + if (sentry__attachment_get_bytes(attachment, &bytes_len) + && sentry__path_get_size(attachment_path) != bytes_len) { + sentry__path_free(attachment_path); + continue; + } + + uint32_t count = 1; + for (size_t j = 0; j < sizeof(keys) / sizeof(keys[0]); j++) { + if (sentry_value_get_type( + sentry_value_get_by_key(attachment, keys[j])) + == SENTRY_VALUE_TYPE_STRING) { + count++; + } + } + mpack_start_map(&writer, count); + mpack_write_cstr(&writer, ATTACHMENT_PATH); + mpack_write_cstr(&writer, attachment_path->path); + sentry__path_free(attachment_path); + for (size_t j = 0; j < sizeof(keys) / sizeof(keys[0]); j++) { + sentry_value_t value = sentry_value_get_by_key(attachment, keys[j]); + if (sentry_value_get_type(value) == SENTRY_VALUE_TYPE_STRING) { + mpack_write_cstr(&writer, keys[j]); + mpack_write_str(&writer, sentry_value_as_string(value), + (uint32_t)sentry_value_get_length(value)); + } + } + mpack_finish_map(&writer); + } + + sentry__path_free(run_path); + bool success = mpack_writer_destroy(&writer) == mpack_ok + && sentry__path_write_buffer(path, buf ? buf : "", buf_len) == 0; + sentry_free(buf); + return success; +} diff --git a/src/sentry_attachment.h b/src/sentry_attachment.h index 1b0668ad0..808c3fe53 100644 --- a/src/sentry_attachment.h +++ b/src/sentry_attachment.h @@ -69,6 +69,13 @@ const char *sentry__attachment_get_path(sentry_value_t attachment); */ sentry_path_t *sentry__attachment_make_path(sentry_value_t attachment); +/** + * Creates an attachment path, deriving a unique path in the run directory for + * buffer attachments. + */ +sentry_path_t *sentry__attachment_make_run_path( + const sentry_path_t *run_path, sentry_value_t attachment); + /** * Returns true if the attachment should be represented as an attachment-ref. */ @@ -142,4 +149,17 @@ sentry_value_t sentry__attachments_find( */ sentry_value_t sentry__attachments_clone(sentry_value_t attachments); +/** + * Reads a list of attachments from a manifest file. + */ +sentry_value_t sentry__read_attachment_manifest(const sentry_path_t *path); + +/** + * Writes attachment metadata to a manifest file in the run directory. + * Byte attachments reference their separately persisted files; missing or + * partially written files are skipped. Does not modify the attachments. + */ +bool sentry__write_attachment_manifest( + const sentry_path_t *path, sentry_value_t attachments); + #endif diff --git a/tests/assertions.py b/tests/assertions.py index d67943c51..904429d99 100644 --- a/tests/assertions.py +++ b/tests/assertions.py @@ -537,6 +537,7 @@ def _load_crashpad_attachments(msg): bytes_bin = None minidump = None for part in msg.walk(): + assert part.get_filename() != "__sentry-attachments" if part.get_filename() is not None: assert part.get("Content-Type") is None diff --git a/tests/test_integration_native.py b/tests/test_integration_native.py index c06352b05..30940ea6e 100644 --- a/tests/test_integration_native.py +++ b/tests/test_integration_native.py @@ -12,6 +12,7 @@ import time import struct +import msgpack import pytest from . import ( @@ -433,8 +434,10 @@ def manifest_is_current(): if not paths: return False try: - manifest = json.loads(paths[0].read_text()) - except (OSError, json.JSONDecodeError): + 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 diff --git a/tests/unit/test_attachments.c b/tests/unit/test_attachments.c index 5240f2133..525318304 100644 --- a/tests/unit/test_attachments.c +++ b/tests/unit/test_attachments.c @@ -756,3 +756,111 @@ SENTRY_TEST(attachments_more_than_ten) TEST_CHECK_INT_EQUAL(testdata.called, 1); } + +SENTRY_TEST(attachment_manifest) +{ + sentry_value_t attachment = sentry__attachment_from_file("attachment.bin"); + sentry_attachment_set_filename(attachment, "renamed.bin"); + sentry_value_t attachments = sentry_value_new_list(); + sentry_value_append(attachments, attachment); + + const char bytes[] = { 'a', '\0', 'b' }; + const char *id = "00000000-0000-4000-8000-000000000001"; + sentry_value_t bytes_attachment + = sentry_attachment_from_bytes(bytes, sizeof(bytes), "bytes.bin"); + sentry_value_set_by_key( + bytes_attachment, "id", sentry_value_new_string(id)); + sentry_value_append(attachments, bytes_attachment); + for (size_t i = 0; i < sentry_value_get_length(attachments); i++) { + attachment = sentry_value_get_by_index(attachments, i); + sentry_attachment_set_type(attachment, "event.attachment"); + sentry_attachment_set_content_type( + attachment, "application/octet-stream"); + } + sentry_value_freeze(attachments); + TEST_CHECK(!sentry__write_attachment_manifest(NULL, attachments)); + + sentry_path_t *run_path = sentry__path_from_str( + SENTRY_TEST_PATH_PREFIX ".attachment-manifest-run"); + sentry_path_t *dir = sentry__path_join_str(run_path, id); + sentry_path_t *path = sentry__path_join_str(dir, "bytes.bin"); + sentry_path_t *manifest_path + = sentry__path_join_str(run_path, "__sentry-attachments"); + TEST_ASSERT(sentry__path_create_dir_all(dir) == 0); + sentry__path_remove(path); + + // missing and partially written byte attachments are excluded + for (size_t size = 0; size <= sizeof(bytes); size++) { + if (size) { + TEST_ASSERT(sentry__path_write_buffer(path, bytes, size) == 0); + } + TEST_ASSERT( + sentry__write_attachment_manifest(manifest_path, attachments)); + sentry_value_t parsed = sentry__read_attachment_manifest(manifest_path); + TEST_CHECK(sentry_value_get_type(parsed) == SENTRY_VALUE_TYPE_LIST); + TEST_CHECK_INT_EQUAL( + sentry_value_get_length(parsed), size == sizeof(bytes) ? 2 : 1); + for (size_t i = 0; i < sentry_value_get_length(parsed); i++) { + sentry_value_t info = sentry_value_get_by_index(parsed, i); + attachment = sentry_value_get_by_index(attachments, i); + TEST_CHECK_STRING_EQUAL(sentry__attachment_get_path(info), + i == 0 ? "attachment.bin" : path->path); + TEST_CHECK_STRING_EQUAL(sentry__attachment_get_filename(info), + i == 0 ? "renamed.bin" : "bytes.bin"); + TEST_CHECK_STRING_EQUAL( + sentry__attachment_get_type(info), "event.attachment"); + TEST_CHECK_STRING_EQUAL(sentry__attachment_get_content_type(info), + "application/octet-stream"); + TEST_CHECK(sentry__attachment_eq(info, attachment)); + TEST_CHECK(sentry__attachment_get_bytes(info, NULL) == NULL); + } + sentry_value_decref(parsed); + } + + size_t size = 0; + const char *original + = sentry__attachment_get_bytes(bytes_attachment, &size); + TEST_CHECK(size == sizeof(bytes)); + TEST_CHECK(memcmp(original, bytes, sizeof(bytes)) == 0); + TEST_CHECK(sentry__attachment_get_path(bytes_attachment) == NULL); + TEST_CHECK(sentry_value_is_frozen(bytes_attachment)); + char *persisted = sentry__path_read_to_buffer(path, &size); + TEST_ASSERT(persisted != NULL); + TEST_CHECK(size == sizeof(bytes)); + TEST_CHECK(memcmp(persisted, bytes, sizeof(bytes)) == 0); + sentry_free(persisted); + sentry_value_decref(attachments); + + sentry_value_t parsed = sentry__read_attachment_manifest(NULL); + TEST_CHECK(sentry_value_is_null(parsed)); + sentry_value_decref(parsed); + + sentry_path_t *missing_path = sentry__path_from_str( + SENTRY_TEST_PATH_PREFIX ".missing-attachment-manifest"); + sentry__path_remove(missing_path); + parsed = sentry__read_attachment_manifest(missing_path); + TEST_CHECK(sentry_value_is_null(parsed)); + sentry_value_decref(parsed); + sentry__path_free(missing_path); + + TEST_ASSERT( + sentry__path_write_buffer(manifest_path, "incomplete", 10) == 0); + parsed = sentry__read_attachment_manifest(manifest_path); + TEST_CHECK(sentry_value_is_null(parsed)); + sentry_value_decref(parsed); + + sentry_value_t empty = sentry_value_new_list(); + TEST_ASSERT(sentry__write_attachment_manifest(manifest_path, empty)); + parsed = sentry__read_attachment_manifest(manifest_path); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(parsed), 0); + sentry_value_decref(parsed); + sentry_value_decref(empty); + sentry__path_remove(path); + sentry__path_remove(dir); + sentry__path_remove(manifest_path); + sentry__path_remove(run_path); + sentry__path_free(path); + sentry__path_free(dir); + sentry__path_free(manifest_path); + sentry__path_free(run_path); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 623e5bc97..98d78647f 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -10,6 +10,7 @@ XX(assert_sdk_name) XX(assert_sdk_user_agent) XX(assert_sdk_version) XX(attachment_bytes_no_copy) +XX(attachment_manifest) XX(attachment_placeholder) XX(attachment_properties) XX(attachment_rate_limit) From 2b570e2bd60858782c7cad4c02fb55b7b8ac107e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 21 Sep 2026 09:40:18 +0200 Subject: [PATCH 2/7] exclude ids --- src/sentry_attachment.c | 4 ++-- tests/unit/test_attachments.c | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sentry_attachment.c b/src/sentry_attachment.c index 5011844f7..f788e5432 100644 --- a/src/sentry_attachment.c +++ b/src/sentry_attachment.c @@ -746,8 +746,8 @@ sentry__write_attachment_manifest( return false; } - const char *keys[] = { ATTACHMENT_ID, ATTACHMENT_FILENAME, ATTACHMENT_TYPE, - ATTACHMENT_CONTENT_TYPE }; + const char *keys[] + = { ATTACHMENT_FILENAME, ATTACHMENT_TYPE, ATTACHMENT_CONTENT_TYPE }; mpack_writer_t writer; char *buf = NULL; size_t buf_len = 0; diff --git a/tests/unit/test_attachments.c b/tests/unit/test_attachments.c index 525318304..16e821f54 100644 --- a/tests/unit/test_attachments.c +++ b/tests/unit/test_attachments.c @@ -811,7 +811,8 @@ SENTRY_TEST(attachment_manifest) sentry__attachment_get_type(info), "event.attachment"); TEST_CHECK_STRING_EQUAL(sentry__attachment_get_content_type(info), "application/octet-stream"); - TEST_CHECK(sentry__attachment_eq(info, attachment)); + sentry_uuid_t info_id = sentry__attachment_get_id(info); + TEST_CHECK(sentry_uuid_is_nil(&info_id)); TEST_CHECK(sentry__attachment_get_bytes(info, NULL) == NULL); } sentry_value_decref(parsed); From 904ecefefacbdcbfa25b4d20a77178ddcf25057f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 21 Sep 2026 09:45:53 +0200 Subject: [PATCH 3/7] fix absent --- src/backends/native/sentry_crash_daemon.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index aab3822d2..563abc700 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -420,7 +420,7 @@ read_attachment_manifest(const sentry_path_t *run_folder) char *buf = sentry__path_read_to_buffer(path, &buf_len); sentry__path_free(path); if (!buf) { - return sentry_value_new_null(); + return sentry_value_new_list(); } const char *start = buf; const char *end = buf + buf_len; From 8b4998bacb80d120620555607c04b646dfad3c21 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 21 Sep 2026 09:54:38 +0200 Subject: [PATCH 4/7] exclude malformed attachment entries --- src/sentry_attachment.c | 16 ++++++++++++---- tests/unit/test_attachments.c | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/sentry_attachment.c b/src/sentry_attachment.c index f788e5432..a02f2d9f6 100644 --- a/src/sentry_attachment.c +++ b/src/sentry_attachment.c @@ -703,16 +703,24 @@ read_manifest(const char *buf, size_t buf_len) sentry_value_t attachments = sentry__value_from_msgpack_stream(buf, buf_len); - size_t len = sentry_value_get_length(attachments); - for (size_t i = 0; i < len; i++) { + size_t i = 0; + while (i < sentry_value_get_length(attachments)) { sentry_value_t attachment = sentry_value_get_by_index(attachments, i); if (sentry_value_get_type(attachment) != SENTRY_VALUE_TYPE_OBJECT || sentry__string_empty(sentry__attachment_get_path(attachment)) || sentry__string_empty( sentry__attachment_get_filename(attachment))) { - sentry_value_decref(attachments); - return sentry_value_new_null(); + if (sentry_value_remove_by_index(attachments, i)) { + sentry_value_decref(attachments); + return sentry_value_new_null(); + } + continue; } + i++; + } + if (sentry_value_get_length(attachments) == 0) { + sentry_value_decref(attachments); + return sentry_value_new_null(); } return attachments; } diff --git a/tests/unit/test_attachments.c b/tests/unit/test_attachments.c index 16e821f54..3ca5f8b84 100644 --- a/tests/unit/test_attachments.c +++ b/tests/unit/test_attachments.c @@ -818,6 +818,33 @@ SENTRY_TEST(attachment_manifest) sentry_value_decref(parsed); } + // malformed manifest entries are excluded + sentry_value_t malformed = sentry_value_new_object(); + size_t valid_len = 0; + size_t malformed_len = 0; + char *valid_buf = sentry_value_to_msgpack( + sentry_value_get_by_index(attachments, 0), &valid_len); + char *malformed_buf = sentry_value_to_msgpack(malformed, &malformed_len); + char *manifest_buf = sentry_malloc(valid_len + malformed_len); + TEST_ASSERT(valid_buf != NULL); + TEST_ASSERT(malformed_buf != NULL); + TEST_ASSERT(manifest_buf != NULL); + memcpy(manifest_buf, malformed_buf, malformed_len); + memcpy(manifest_buf + malformed_len, valid_buf, valid_len); + TEST_ASSERT(sentry__path_write_buffer( + manifest_path, manifest_buf, valid_len + malformed_len) + == 0); + sentry_value_t salvaged = sentry__read_attachment_manifest(manifest_path); + TEST_CHECK_INT_EQUAL(sentry_value_get_length(salvaged), 1); + TEST_CHECK_STRING_EQUAL( + sentry__attachment_get_filename(sentry_value_get_by_index(salvaged, 0)), + "renamed.bin"); + sentry_value_decref(salvaged); + sentry_free(manifest_buf); + sentry_free(malformed_buf); + sentry_free(valid_buf); + sentry_value_decref(malformed); + size_t size = 0; const char *original = sentry__attachment_get_bytes(bytes_attachment, &size); From 59d3c72d350efc257d478b380fe9f6a5ee78fee9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 21 Sep 2026 10:46:13 +0200 Subject: [PATCH 5/7] revise --- src/backends/native/sentry_crash_daemon.c | 59 ++++++++++++----------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/backends/native/sentry_crash_daemon.c b/src/backends/native/sentry_crash_daemon.c index 563abc700..4794136a8 100644 --- a/src/backends/native/sentry_crash_daemon.c +++ b/src/backends/native/sentry_crash_daemon.c @@ -364,10 +364,36 @@ attachment_is_placeholder(const sentry_options_t *options, const char *path) return is_placeholder; } +/** + * Reads a legacy JSON attachment manifest (TODO: remove in 1.0) + */ static sentry_value_t -read_legacy_manifest(const char *buf, size_t buf_len) +read_legacy_manifest(const sentry_path_t *manifest_path) { - sentry_value_t legacy = sentry__value_from_json(buf, buf_len); + size_t buf_len = 0; + char *buf = sentry__path_read_to_buffer(manifest_path, &buf_len); + if (!buf) { + return sentry_value_new_list(); + } + + const char *start = buf; + const char *end = buf + buf_len; + while (start < end + && (*start == ' ' || *start == '\t' || *start == '\r' + || *start == '\n')) { + start++; + } + const char *trimmed_end = end; + while (trimmed_end > start + && (trimmed_end[-1] == ' ' || trimmed_end[-1] == '\t' + || trimmed_end[-1] == '\r' || trimmed_end[-1] == '\n')) { + trimmed_end--; + } + sentry_value_t legacy + = start < trimmed_end && *start == '[' && trimmed_end[-1] == ']' + ? sentry__value_from_json(start, (size_t)(trimmed_end - start)) + : sentry_value_new_null(); + sentry_free(buf); if (sentry_value_get_type(legacy) != SENTRY_VALUE_TYPE_LIST) { sentry_value_decref(legacy); return sentry_value_new_null(); @@ -410,35 +436,10 @@ read_attachment_manifest(const sentry_path_t *run_folder) return sentry_value_new_null(); } sentry_value_t attachments = sentry__read_attachment_manifest(path); - if (!sentry_value_is_null(attachments)) { - sentry__path_free(path); - return attachments; + if (sentry_value_is_null(attachments)) { + attachments = read_legacy_manifest(path); } - sentry_value_decref(attachments); - - size_t buf_len = 0; - char *buf = sentry__path_read_to_buffer(path, &buf_len); sentry__path_free(path); - if (!buf) { - return sentry_value_new_list(); - } - const char *start = buf; - const char *end = buf + buf_len; - while (start < end - && (*start == ' ' || *start == '\t' || *start == '\r' - || *start == '\n')) { - start++; - } - const char *trimmed_end = end; - while (trimmed_end > start - && (trimmed_end[-1] == ' ' || trimmed_end[-1] == '\t' - || trimmed_end[-1] == '\r' || trimmed_end[-1] == '\n')) { - trimmed_end--; - } - attachments = start < trimmed_end && *start == '[' && trimmed_end[-1] == ']' - ? read_legacy_manifest(start, (size_t)(trimmed_end - start)) - : sentry_value_new_null(); - sentry_free(buf); return attachments; } From 82ed4ffec8ec13183c68d18a24a4338e17abd07d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 21 Sep 2026 13:26:43 +0200 Subject: [PATCH 6/7] fix msvc --- src/sentry_mpack.h | 22 +++++++++++++++++++ src/sentry_value.c | 25 +--------------------- src/session_replay/sentry_session_replay.c | 25 +--------------------- 3 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/sentry_mpack.h b/src/sentry_mpack.h index 57c36949f..064f232da 100644 --- a/src/sentry_mpack.h +++ b/src/sentry_mpack.h @@ -387,6 +387,28 @@ #define mpack_writer_track_pop sentry__mpack_writer_track_pop #define mpack_writer_track_push sentry__mpack_writer_track_push +#if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4127) // conditional expression is constant +# if defined(__clang__) // clang-cl +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wdocumentation" +# pragma clang diagnostic ignored "-Wpre-c11-compat" +# endif +#elif defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wstatic-in-inline" +#endif + #include "../vendor/mpack.h" +#if defined(_MSC_VER) +# pragma warning(pop) +# ifdef __clang__ // clang-cl +# pragma clang diagnostic pop +# endif +#elif defined(__clang__) +# pragma clang diagnostic pop +#endif + #endif diff --git a/src/sentry_value.c b/src/sentry_value.c index c5aba36fc..6e09804c2 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -6,33 +6,10 @@ #include #include -#if defined(_MSC_VER) -# pragma warning(push) -# pragma warning(disable : 4127) // conditional expression is constant -# if defined(__clang__) // clang-cl -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wdocumentation" -# pragma clang diagnostic ignored "-Wpre-c11-compat" -# endif -#elif defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wstatic-in-inline" -#endif - -#include "sentry_mpack.h" - -#if defined(_MSC_VER) -# pragma warning(pop) -# ifdef __clang__ // clang-cl -# pragma clang diagnostic pop -# endif -#elif defined(__clang__) -# pragma clang diagnostic pop -#endif - #include "sentry_alloc.h" #include "sentry_core.h" #include "sentry_json.h" +#include "sentry_mpack.h" #include "sentry_slice.h" #include "sentry_string.h" #include "sentry_sync.h" diff --git a/src/session_replay/sentry_session_replay.c b/src/session_replay/sentry_session_replay.c index e0705636d..bc7492c00 100644 --- a/src/session_replay/sentry_session_replay.c +++ b/src/session_replay/sentry_session_replay.c @@ -4,35 +4,12 @@ #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_json.h" +#include "sentry_mpack.h" #include "sentry_string.h" #include "sentry_utils.h" #include "sentry_uuid.h" #include "sentry_value.h" -#if defined(_MSC_VER) -# pragma warning(push) -# pragma warning(disable : 4127) // conditional expression is constant -# if defined(__clang__) // clang-cl -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wdocumentation" -# pragma clang diagnostic ignored "-Wpre-c11-compat" -# endif -#elif defined(__clang__) -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wstatic-in-inline" -#endif - -#include "sentry_mpack.h" - -#if defined(_MSC_VER) -# pragma warning(pop) -# ifdef __clang__ // clang-cl -# pragma clang diagnostic pop -# endif -#elif defined(__clang__) -# pragma clang diagnostic pop -#endif - #include sentry_path_t * From 5f3b00e201d7eb7996a8fe2981bb3f28a333b8ab Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 21 Sep 2026 18:01:24 +0200 Subject: [PATCH 7/7] bump crashpad --- external/crashpad | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/crashpad b/external/crashpad index 60be52d4a..000e3ad7a 160000 --- a/external/crashpad +++ b/external/crashpad @@ -1 +1 @@ -Subproject commit 60be52d4aa63db37d68a1bc81aa95182fc878e78 +Subproject commit 000e3ad7a51c89e49bc00eca9846aa0485cccc96