From b1bdf5ac7f256bd2e6edb90391c0540c236657cd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 4 Jun 2026 12:40:10 +0200 Subject: [PATCH 1/8] fix: clone user and fingerprint in scope_apply_to_event --- src/sentry_scope.c | 4 +- tests/unit/test_scope.c | 82 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/sentry_scope.c b/src/sentry_scope.c index 0df499c31..d46d977e3 100644 --- a/src/sentry_scope.c +++ b/src/sentry_scope.c @@ -414,10 +414,10 @@ sentry__scope_apply_to_event(const sentry_scope_t *scope, sentry_value_new_string(options->run->installation_id)); } } else if (sentry_value_get_length(scope->user) > 0) { - PLACE_VALUE("user", scope->user); + PLACE_CLONED_VALUE("user", scope->user); } } - PLACE_VALUE("fingerprint", scope->fingerprint); + PLACE_CLONED_VALUE("fingerprint", scope->fingerprint); PLACE_STRING("transaction", scope->transaction); PLACE_VALUE("sdk", scope->client_sdk); diff --git a/tests/unit/test_scope.c b/tests/unit/test_scope.c index 79855c8b1..8687c6399 100644 --- a/tests/unit/test_scope.c +++ b/tests/unit/test_scope.c @@ -981,6 +981,88 @@ SENTRY_TEST(before_breadcrumb_passthrough) sentry_close(); } +static sentry_value_t +before_send_modify_scope_values( + sentry_value_t event, void *UNUSED(hint), void *UNUSED(data)) +{ + sentry_value_t contexts = sentry_value_get_by_key(event, "contexts"); + sentry_value_t gpu = sentry_value_get_by_key(contexts, "gpu"); + sentry_value_set_by_key(gpu, "name", sentry_value_new_string("modified")); + sentry_value_set_by_key( + gpu, "injected", sentry_value_new_string("injected")); + + sentry_value_t extra = sentry_value_get_by_key(event, "extra"); + sentry_value_t data_obj = sentry_value_get_by_key(extra, "data"); + sentry_value_set_by_key( + data_obj, "key", sentry_value_new_string("modified")); + sentry_value_set_by_key( + data_obj, "injected", sentry_value_new_string("injected")); + + sentry_value_t user = sentry_value_get_by_key(event, "user"); + sentry_value_set_by_key( + user, "username", sentry_value_new_string("modified")); + sentry_value_set_by_key( + user, "injected", sentry_value_new_string("injected")); + + sentry_value_t fingerprint = sentry_value_get_by_key(event, "fingerprint"); + sentry_value_append(fingerprint, sentry_value_new_string("injected")); + + return event; +} + +SENTRY_TEST(scope_clone) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_before_send( + options, before_send_modify_scope_values, NULL); + sentry_init(options); + + sentry_value_t gpu = sentry_value_new_object(); + sentry_value_set_by_key(gpu, "name", sentry_value_new_string("original")); + sentry_set_context("gpu", gpu); + + sentry_value_t data = sentry_value_new_object(); + sentry_value_set_by_key(data, "key", sentry_value_new_string("original")); + sentry_set_extra("data", data); + + sentry_set_user(sentry_value_new_user("1", "original", NULL, NULL)); + sentry_set_fingerprint("fp1", "fp2", NULL); + + // before_send modifies contexts, extra, user, and fingerprint on the event + sentry_capture_event( + sentry_value_new_message_event(SENTRY_LEVEL_INFO, NULL, "test")); + + // scope values must not be corrupted by before_send modifications + SENTRY_WITH_SCOPE (scope) { + sentry_value_t scope_gpu + = sentry_value_get_by_key(scope->contexts, "gpu"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(scope_gpu, "name")), + "original"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope_gpu, "injected"))); + + sentry_value_t scope_data + = sentry_value_get_by_key(scope->extra, "data"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(scope_data, "key")), + "original"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope_data, "injected"))); + + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + scope->user, "username")), + "original"); + TEST_CHECK(sentry_value_is_null( + sentry_value_get_by_key(scope->user, "injected"))); + + TEST_CHECK_INT_EQUAL(sentry_value_get_length(scope->fingerprint), 2); + } + + sentry_close(); +} + SENTRY_TEST(scope_global_attributes) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index dd693985b..847d91efe 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -284,6 +284,7 @@ XX(sampling_before_send) XX(sampling_decision) XX(sampling_transaction) XX(scope_breadcrumbs) +XX(scope_clone) XX(scope_contexts) XX(scope_update_context) XX(scope_extra) From ec8e399f125b093e237de8889eab4aee7d74ecf4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Mar 2026 11:27:37 +0100 Subject: [PATCH 2/8] ref: copy-on-write for sentry_value_t clone Add copy-on-write (COW) semantics to sentry__value_clone() for list and object values. Instead of deep-copying items/pairs on clone, the new thing_t shares the underlying list_t/obj_t data via a data-level refcount. The actual copy is deferred to mutation time (thing_detach), making clone O(1) for flat containers. Two-level refcounting: - thing_t.refcount: how many sentry_value_t handles reference the thing - list_t.refcount / obj_t.refcount: how many thing_ts share the data Container children (nested lists/objects) are still recursively cloned to maintain value semantics at each nesting level. Co-Authored-By: Claude Opus 4.6 --- src/sentry_value.c | 228 +++++++++++++++++++++++++++++++++------ tests/unit/test_value.c | 232 ++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 9 ++ 3 files changed, 439 insertions(+), 30 deletions(-) diff --git a/src/sentry_value.c b/src/sentry_value.c index 6a1ea6d06..fb076b13d 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -93,6 +93,7 @@ typedef struct { sentry_value_t *items; size_t len; size_t allocated; + long refcount; } list_t; typedef struct { @@ -104,6 +105,7 @@ typedef struct { obj_pair_t *pairs; size_t len; size_t allocated; + long refcount; } obj_t; static const char * @@ -161,33 +163,143 @@ thing_get_type(const thing_t *thing) } static void -thing_free(thing_t *thing) +list_free(list_t *list) +{ + if (sentry__atomic_fetch_and_add(&list->refcount, -1) != 1) { + return; + } + for (size_t i = 0; i < list->len; i++) { + sentry_value_decref(list->items[i]); + } + sentry_free(list->items); + sentry_free(list); +} + +static void +obj_free(obj_t *obj) +{ + if (sentry__atomic_fetch_and_add(&obj->refcount, -1) != 1) { + return; + } + for (size_t i = 0; i < obj->len; i++) { + sentry_free(obj->pairs[i].k); + sentry_value_decref(obj->pairs[i].v); + } + sentry_free(obj->pairs); + sentry_free(obj); +} + +static bool +thing_detach(thing_t *thing) { switch (thing_get_type(thing)) { case THING_TYPE_LIST: { - list_t *list = thing->payload._ptr; - for (size_t i = 0; i < list->len; i++) { - sentry_value_decref(list->items[i]); + list_t *old = thing->payload._ptr; + if (sentry__atomic_fetch(&old->refcount) <= 1) { + return true; } - sentry_free(list->items); - sentry_free(list); - break; + list_t *new_list = SENTRY_MAKE(list_t); + if (!new_list) { + return false; + } + new_list->len = old->len; + new_list->allocated = old->len; + new_list->refcount = 1; + if (old->len) { + new_list->items = sentry_malloc(sizeof(sentry_value_t) * old->len); + if (!new_list->items) { + sentry_free(new_list); + return false; + } + memcpy( + new_list->items, old->items, sizeof(sentry_value_t) * old->len); + for (size_t i = 0; i < old->len; i++) { + sentry_value_incref(new_list->items[i]); + } + } else { + new_list->items = NULL; + } + list_free(old); + thing->payload._ptr = new_list; + return true; } case THING_TYPE_OBJECT: { - obj_t *obj = thing->payload._ptr; - for (size_t i = 0; i < obj->len; i++) { - sentry_free(obj->pairs[i].k); - sentry_value_decref(obj->pairs[i].v); + obj_t *old = thing->payload._ptr; + if (sentry__atomic_fetch(&old->refcount) <= 1) { + return true; + } + obj_t *new_obj = SENTRY_MAKE(obj_t); + if (!new_obj) { + return false; + } + new_obj->len = old->len; + new_obj->allocated = old->len; + new_obj->refcount = 1; + if (old->len) { + new_obj->pairs = sentry_malloc(sizeof(obj_pair_t) * old->len); + if (!new_obj->pairs) { + sentry_free(new_obj); + return false; + } + for (size_t i = 0; i < old->len; i++) { + new_obj->pairs[i].k = sentry__string_clone(old->pairs[i].k); + new_obj->pairs[i].v = old->pairs[i].v; + sentry_value_incref(new_obj->pairs[i].v); + } + } else { + new_obj->pairs = NULL; } - sentry_free(obj->pairs); - sentry_free(obj); + obj_free(old); + thing->payload._ptr = new_obj; + return true; + } + default: + return true; + } +} + +static sentry_value_t +thing_get_child(const thing_t *thing, size_t i) +{ + switch (thing_get_type(thing)) { + case THING_TYPE_LIST: + return ((const list_t *)thing->payload._ptr)->items[i]; + case THING_TYPE_OBJECT: + return ((const obj_t *)thing->payload._ptr)->pairs[i].v; + default: + return sentry_value_new_null(); + } +} + +static void +thing_set_child(thing_t *thing, size_t i, sentry_value_t value) +{ + switch (thing_get_type(thing)) { + case THING_TYPE_LIST: + ((list_t *)thing->payload._ptr)->items[i] = value; + break; + case THING_TYPE_OBJECT: + ((obj_t *)thing->payload._ptr)->pairs[i].v = value; + break; + default: break; } - case THING_TYPE_STRING: { +} + +static void +thing_free(thing_t *thing) +{ + switch (thing_get_type(thing)) { + case THING_TYPE_LIST: + list_free(thing->payload._ptr); + break; + case THING_TYPE_OBJECT: + obj_free(thing->payload._ptr); + break; + case THING_TYPE_STRING: sentry_free(thing->payload._ptr); break; } - } sentry_free(thing); } @@ -395,6 +507,8 @@ sentry_value_new_list(void) { list_t *l = SENTRY_MAKE(list_t); if (l) { + memset(l, 0, sizeof(list_t)); + l->refcount = 1; sentry_value_t rv = new_thing_value(l, THING_TYPE_LIST); if (sentry_value_is_null(rv)) { sentry_free(l); @@ -410,6 +524,8 @@ sentry__value_new_list_with_size(size_t size) { list_t *l = SENTRY_MAKE(list_t); if (l) { + memset(l, 0, sizeof(list_t)); + l->refcount = 1; l->allocated = size; if (size) { l->items = sentry_malloc(sizeof(sentry_value_t) * size); @@ -434,6 +550,8 @@ sentry_value_new_object(void) { obj_t *o = SENTRY_MAKE(obj_t); if (o) { + memset(o, 0, sizeof(obj_t)); + o->refcount = 1; sentry_value_t rv = new_thing_value(o, THING_TYPE_OBJECT); if (sentry_value_is_null(rv)) { sentry_free(o); @@ -449,6 +567,8 @@ sentry__value_new_object_with_size(size_t size) { obj_t *o = SENTRY_MAKE(obj_t); if (o) { + memset(o, 0, sizeof(obj_t)); + o->refcount = 1; o->allocated = size; if (size) { o->pairs = sentry_malloc(sizeof(obj_pair_t) * size); @@ -639,7 +759,8 @@ sentry_value_set_by_key_n( } sentry_slice_t k_slice = { k, k_len }; thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT) { + if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT + || !thing_detach(thing)) { goto fail; } obj_t *o = thing->payload._ptr; @@ -690,7 +811,8 @@ sentry_value_remove_by_key_n(sentry_value_t value, const char *k, size_t k_len) } sentry_slice_t k_slice = { k, k_len }; thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT) { + if (!thing || thing_get_type(thing) != THING_TYPE_OBJECT + || !thing_detach(thing)) { return 1; } obj_t *o = thing->payload._ptr; @@ -722,7 +844,8 @@ int sentry_value_append(sentry_value_t value, sentry_value_t v) { thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_LIST) { + if (!thing || thing_get_type(thing) != THING_TYPE_LIST + || !thing_detach(thing)) { goto fail; } @@ -791,6 +914,39 @@ sentry__value_stringify(sentry_value_t value) #undef STRINGIFY_NUMERIC } +static bool +value_is_container(sentry_value_t value) +{ + const thing_t *thing = value_as_thing(value); + if (!thing) { + return false; + } + int type = thing_get_type(thing); + return type == THING_TYPE_LIST || type == THING_TYPE_OBJECT; +} + +static bool +thing_clone_children(thing_t *thing, size_t len) +{ + bool detached = false; + for (size_t i = 0; i < len; i++) { + sentry_value_t child = thing_get_child(thing, i); + if (!value_is_container(child)) { + continue; + } + if (!detached) { + if (!thing_detach(thing)) { + return false; + } + detached = true; + } + sentry_value_t cloned = sentry__value_clone(child); + sentry_value_decref(child); + thing_set_child(thing, i, cloned); + } + return true; +} + sentry_value_t sentry__value_clone(sentry_value_t value) { @@ -800,20 +956,30 @@ sentry__value_clone(sentry_value_t value) } switch (thing_get_type(thing)) { case THING_TYPE_LIST: { - const list_t *list = thing->payload._ptr; - sentry_value_t rv = sentry__value_new_list_with_size(list->len); - for (size_t i = 0; i < list->len; i++) { - sentry_value_incref(list->items[i]); - sentry_value_append(rv, list->items[i]); + list_t *list = thing->payload._ptr; + sentry__atomic_fetch_and_add(&list->refcount, 1); + sentry_value_t rv = new_thing_value(list, THING_TYPE_LIST); + if (sentry_value_is_null(rv)) { + list_free(list); + return rv; + } + if (!thing_clone_children(value_as_thing(rv), list->len)) { + sentry_value_decref(rv); + return sentry_value_new_null(); } return rv; } case THING_TYPE_OBJECT: { - const obj_t *obj = thing->payload._ptr; - sentry_value_t rv = sentry__value_new_object_with_size(obj->len); - for (size_t i = 0; i < obj->len; i++) { - sentry_value_incref(obj->pairs[i].v); - sentry_value_set_by_key(rv, obj->pairs[i].k, obj->pairs[i].v); + obj_t *obj = thing->payload._ptr; + sentry__atomic_fetch_and_add(&obj->refcount, 1); + sentry_value_t rv = new_thing_value(obj, THING_TYPE_OBJECT); + if (sentry_value_is_null(rv)) { + obj_free(obj); + return rv; + } + if (!thing_clone_children(value_as_thing(rv), obj->len)) { + sentry_value_decref(rv); + return sentry_value_new_null(); } return rv; } @@ -832,7 +998,8 @@ int sentry_value_set_by_index(sentry_value_t value, size_t index, sentry_value_t v) { thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_LIST) { + if (!thing || thing_get_type(thing) != THING_TYPE_LIST + || !thing_detach(thing)) { goto fail; } @@ -862,7 +1029,8 @@ int sentry_value_remove_by_index(sentry_value_t value, size_t index) { thing_t *thing = value_as_unfrozen_thing(value); - if (!thing || thing_get_type(thing) != THING_TYPE_LIST) { + if (!thing || thing_get_type(thing) != THING_TYPE_LIST + || !thing_detach(thing)) { return 1; } diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index ba0ef57c2..c966b6d19 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -436,6 +436,238 @@ SENTRY_TEST(value_object_merge_nested) sentry_value_decref(dst); } +SENTRY_TEST(value_clone_list) +{ + sentry_value_t original = sentry_value_new_list(); + sentry_value_append(original, sentry_value_new_int32(1)); + sentry_value_append(original, sentry_value_new_int32(2)); + sentry_value_append(original, sentry_value_new_int32(3)); + + sentry_value_t clone = sentry__value_clone(original); + TEST_CHECK(sentry_value_refcount(clone) == 1); + TEST_CHECK_JSON_VALUE(clone, "[1,2,3]"); + + // mutating the clone triggers COW — original is unaffected + sentry_value_append(clone, sentry_value_new_int32(4)); + TEST_CHECK_JSON_VALUE(clone, "[1,2,3,4]"); + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + + sentry_value_set_by_index(clone, 0, sentry_value_new_int32(10)); + TEST_CHECK_JSON_VALUE(clone, "[10,2,3,4]"); + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + + sentry_value_remove_by_index(clone, 1); + TEST_CHECK_JSON_VALUE(clone, "[10,3,4]"); + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + + sentry_value_decref(clone); + // original still valid after clone is freed + TEST_CHECK_JSON_VALUE(original, "[1,2,3]"); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_object) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "a", sentry_value_new_int32(1)); + sentry_value_set_by_key(original, "b", sentry_value_new_int32(2)); + + sentry_value_t clone = sentry__value_clone(original); + TEST_CHECK(sentry_value_refcount(clone) == 1); + TEST_CHECK_JSON_VALUE(clone, "{\"a\":1,\"b\":2}"); + + // mutating the clone triggers COW — original is unaffected + sentry_value_set_by_key(clone, "a", sentry_value_new_int32(10)); + TEST_CHECK_INT_EQUAL( + sentry_value_as_int32(sentry_value_get_by_key(clone, "a")), 10); + TEST_CHECK_INT_EQUAL( + sentry_value_as_int32(sentry_value_get_by_key(original, "a")), 1); + + sentry_value_set_by_key(clone, "c", sentry_value_new_int32(3)); + TEST_CHECK(sentry_value_get_length(clone) == 3); + TEST_CHECK(sentry_value_get_length(original) == 2); + + sentry_value_remove_by_key(clone, "b"); + TEST_CHECK(sentry_value_get_length(clone) == 2); + TEST_CHECK(sentry_value_get_length(original) == 2); + + sentry_value_decref(clone); + TEST_CHECK_JSON_VALUE(original, "{\"a\":1,\"b\":2}"); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_frozen) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "a", sentry_value_new_int32(1)); + sentry_value_freeze(original); + TEST_CHECK(sentry_value_is_frozen(original)); + + sentry_value_t clone = sentry__value_clone(original); + TEST_CHECK(!sentry_value_is_frozen(clone)); + + // clone is mutable even though original is frozen + sentry_value_set_by_key(clone, "b", sentry_value_new_int32(2)); + TEST_CHECK_JSON_VALUE(clone, "{\"a\":1,\"b\":2}"); + TEST_CHECK_JSON_VALUE(original, "{\"a\":1}"); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_multiple) +{ + sentry_value_t original = sentry_value_new_list(); + sentry_value_append(original, sentry_value_new_int32(1)); + + sentry_value_t clone1 = sentry__value_clone(original); + sentry_value_t clone2 = sentry__value_clone(original); + + // each clone can be mutated independently + sentry_value_append(clone1, sentry_value_new_int32(2)); + sentry_value_append(clone2, sentry_value_new_int32(3)); + + TEST_CHECK_JSON_VALUE(original, "[1]"); + TEST_CHECK_JSON_VALUE(clone1, "[1,2]"); + TEST_CHECK_JSON_VALUE(clone2, "[1,3]"); + + sentry_value_decref(original); + sentry_value_decref(clone1); + sentry_value_decref(clone2); +} + +SENTRY_TEST(value_clone_of_clone) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "x", sentry_value_new_int32(1)); + + sentry_value_t clone1 = sentry__value_clone(original); + sentry_value_t clone2 = sentry__value_clone(clone1); + + sentry_value_set_by_key(clone2, "y", sentry_value_new_int32(2)); + TEST_CHECK_JSON_VALUE(original, "{\"x\":1}"); + TEST_CHECK_JSON_VALUE(clone1, "{\"x\":1}"); + TEST_CHECK_JSON_VALUE(clone2, "{\"x\":1,\"y\":2}"); + + sentry_value_decref(original); + sentry_value_decref(clone1); + sentry_value_decref(clone2); +} + +SENTRY_TEST(value_clone_merge) +{ + sentry_value_t original = sentry_value_new_object(); + sentry_value_set_by_key(original, "a", sentry_value_new_int32(1)); + + sentry_value_t clone = sentry__value_clone(original); + + sentry_value_t src = sentry_value_new_object(); + sentry_value_set_by_key(src, "b", sentry_value_new_int32(2)); + + int rv = sentry__value_merge_objects(clone, src); + TEST_CHECK_INT_EQUAL(rv, 0); + sentry_value_decref(src); + + TEST_CHECK_JSON_VALUE(clone, "{\"a\":1,\"b\":2}"); + TEST_CHECK_JSON_VALUE(original, "{\"a\":1}"); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_free_original_first) +{ + sentry_value_t original = sentry_value_new_list(); + sentry_value_append(original, sentry_value_new_string("hello")); + sentry_value_append(original, sentry_value_new_int32(42)); + + sentry_value_t clone = sentry__value_clone(original); + // free original while clone still shares data + sentry_value_decref(original); + + TEST_CHECK_JSON_VALUE(clone, "[\"hello\",42]"); + sentry_value_append(clone, sentry_value_new_int32(3)); + TEST_CHECK_JSON_VALUE(clone, "[\"hello\",42,3]"); + sentry_value_decref(clone); +} + +SENTRY_TEST(value_clone_nested_modify_leaf) +{ + // {ctx: {name: "os", ver: "14"}, tag: "v1"} + sentry_value_t original = sentry_value_new_object(); + sentry_value_t child = sentry_value_new_object(); + sentry_value_set_by_key(child, "name", sentry_value_new_string("os")); + sentry_value_set_by_key(child, "ver", sentry_value_new_string("14")); + sentry_value_set_by_key(original, "ctx", child); + sentry_value_set_by_key(original, "tag", sentry_value_new_string("v1")); + + sentry_value_t clone = sentry__value_clone(original); + + // mutate a leaf key on the clone — original unaffected + sentry_value_set_by_key(clone, "tag", sentry_value_new_string("v2")); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(original, "tag")), "v1"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone, "tag")), "v2"); + + // nested child objects are independent + sentry_value_t clone_ctx = sentry_value_get_by_key(clone, "ctx"); + sentry_value_set_by_key(clone_ctx, "name", sentry_value_new_string("win")); + sentry_value_set_by_key(clone_ctx, "arch", sentry_value_new_string("x64")); + + sentry_value_t orig_ctx = sentry_value_get_by_key(original, "ctx"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(orig_ctx, "name")), + "os"); + TEST_CHECK(sentry_value_is_null(sentry_value_get_by_key(orig_ctx, "arch"))); + TEST_CHECK(sentry_value_get_length(orig_ctx) == 2); + + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone_ctx, "name")), + "win"); + TEST_CHECK_STRING_EQUAL( + sentry_value_as_string(sentry_value_get_by_key(clone_ctx, "arch")), + "x64"); + TEST_CHECK(sentry_value_get_length(clone_ctx) == 3); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + +SENTRY_TEST(value_clone_nested_modify_middle) +{ + // [[1, 2], [3, 4]] + sentry_value_t original = sentry_value_new_list(); + sentry_value_t inner0 = sentry_value_new_list(); + sentry_value_append(inner0, sentry_value_new_int32(1)); + sentry_value_append(inner0, sentry_value_new_int32(2)); + sentry_value_t inner1 = sentry_value_new_list(); + sentry_value_append(inner1, sentry_value_new_int32(3)); + sentry_value_append(inner1, sentry_value_new_int32(4)); + sentry_value_append(original, inner0); + sentry_value_append(original, inner1); + + sentry_value_t clone = sentry__value_clone(original); + + // replace a whole nested list in the clone (middle-level mutation) + sentry_value_t replacement = sentry_value_new_list(); + sentry_value_append(replacement, sentry_value_new_int32(99)); + sentry_value_set_by_index(clone, 0, replacement); + + TEST_CHECK_JSON_VALUE(original, "[[1,2],[3,4]]"); + TEST_CHECK_JSON_VALUE(clone, "[[99],[3,4]]"); + + // mutate a leaf inside the other nested list + sentry_value_t clone_inner1 = sentry_value_get_by_index(clone, 1); + sentry_value_append(clone_inner1, sentry_value_new_int32(5)); + + TEST_CHECK_JSON_VALUE(original, "[[1,2],[3,4]]"); + TEST_CHECK_JSON_VALUE(clone, "[[99],[3,4,5]]"); + + sentry_value_decref(clone); + sentry_value_decref(original); +} + SENTRY_TEST(value_user) { const char *id = "42"; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 847d91efe..78dd776db 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -370,6 +370,15 @@ XX(uuid_api) XX(uuid_v4) XX(value_attribute) XX(value_bool) +XX(value_clone_free_original_first) +XX(value_clone_frozen) +XX(value_clone_list) +XX(value_clone_merge) +XX(value_clone_multiple) +XX(value_clone_nested_modify_leaf) +XX(value_clone_nested_modify_middle) +XX(value_clone_object) +XX(value_clone_of_clone) XX(value_double) XX(value_foreach_key_value) XX(value_freezing) From d4f7f460c01f1d0bf1f59b89b1d60d0c6a5ddb90 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 4 Jun 2026 19:04:16 +0200 Subject: [PATCH 3/8] fix(value): Fix nested clone failure in thing_clone_children In `thing_clone_children`, if `sentry__value_clone` fails for a nested container, the error was ignored. This resulted in clones that appeared usable but were missing nested data. Add a check to return false on clone failure, ensuring the entire operation fails rather than producing an incomplete result. Co-Authored-By: OpenAI Codex --- src/sentry_value.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sentry_value.c b/src/sentry_value.c index fb076b13d..87030eb0e 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -941,6 +941,9 @@ thing_clone_children(thing_t *thing, size_t len) detached = true; } sentry_value_t cloned = sentry__value_clone(child); + if (sentry_value_is_null(cloned)) { + return false; + } sentry_value_decref(child); thing_set_child(thing, i, cloned); } From 9e9c260a991021a3ba0bba1d68f4519918cb2836 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 6 Jul 2026 11:47:36 +0200 Subject: [PATCH 4/8] fix: Handle object detach key clone failures Keep object copy-on-write detach atomic when cloning keys fails. Track initialized pairs so partial clones can be freed without installing null keys. Co-Authored-By: OpenAI Codex --- src/sentry_value.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sentry_value.c b/src/sentry_value.c index 87030eb0e..598f0d5ca 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -232,7 +232,7 @@ thing_detach(thing_t *thing) if (!new_obj) { return false; } - new_obj->len = old->len; + new_obj->len = 0; new_obj->allocated = old->len; new_obj->refcount = 1; if (old->len) { @@ -243,8 +243,13 @@ thing_detach(thing_t *thing) } for (size_t i = 0; i < old->len; i++) { new_obj->pairs[i].k = sentry__string_clone(old->pairs[i].k); + if (!new_obj->pairs[i].k) { + obj_free(new_obj); + return false; + } new_obj->pairs[i].v = old->pairs[i].v; sentry_value_incref(new_obj->pairs[i].v); + new_obj->len++; } } else { new_obj->pairs = NULL; From 837e73d00aa73d07627def5ad4355e9d8d7ca086 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 6 Jul 2026 11:52:54 +0200 Subject: [PATCH 5/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d95bcbf0..fa900e3ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **Fixes**: - Apply the propagation context to events that already have contexts set, so that events captured with a local scope or with event-level contexts keep their trace. ([#1843](https://github.com/getsentry/sentry-native/pull/1843)) +- Fix scope data loss from shared `sentry_value_t` containers while significantly improving scope merge performance with copy-on-write cloning. ([#1794](https://github.com/getsentry/sentry-native/pull/1794)) ## 0.15.3 From 70d6ce69a6c5f292fbdfa947f51e5b3ca473ca49 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 8 Jul 2026 17:38:58 +0200 Subject: [PATCH 6/8] clean up memset(0)'s --- src/sentry_value.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/sentry_value.c b/src/sentry_value.c index 598f0d5ca..d00875b65 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -512,7 +512,6 @@ sentry_value_new_list(void) { list_t *l = SENTRY_MAKE(list_t); if (l) { - memset(l, 0, sizeof(list_t)); l->refcount = 1; sentry_value_t rv = new_thing_value(l, THING_TYPE_LIST); if (sentry_value_is_null(rv)) { @@ -529,7 +528,6 @@ sentry__value_new_list_with_size(size_t size) { list_t *l = SENTRY_MAKE(list_t); if (l) { - memset(l, 0, sizeof(list_t)); l->refcount = 1; l->allocated = size; if (size) { @@ -555,7 +553,6 @@ sentry_value_new_object(void) { obj_t *o = SENTRY_MAKE(obj_t); if (o) { - memset(o, 0, sizeof(obj_t)); o->refcount = 1; sentry_value_t rv = new_thing_value(o, THING_TYPE_OBJECT); if (sentry_value_is_null(rv)) { @@ -572,7 +569,6 @@ sentry__value_new_object_with_size(size_t size) { obj_t *o = SENTRY_MAKE(obj_t); if (o) { - memset(o, 0, sizeof(obj_t)); o->refcount = 1; o->allocated = size; if (size) { From 9403a341310adba92e54d0b093925566656ce5c8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 9 Jul 2026 09:42:50 +0200 Subject: [PATCH 7/8] extract list/obj_clone helpers for better readability --- src/sentry_value.c | 104 +++++++++++++++++++++++++-------------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/src/sentry_value.c b/src/sentry_value.c index d00875b65..60fcab40b 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -189,6 +189,59 @@ obj_free(obj_t *obj) sentry_free(obj); } +static list_t * +list_clone(const list_t *list) +{ + list_t *clone = SENTRY_MAKE(list_t); + if (!clone) { + return NULL; + } + clone->len = list->len; + clone->allocated = list->len; + clone->refcount = 1; + if (list->len) { + clone->items = sentry_malloc(sizeof(sentry_value_t) * list->len); + if (!clone->items) { + sentry_free(clone); + return NULL; + } + memcpy(clone->items, list->items, sizeof(sentry_value_t) * list->len); + for (size_t i = 0; i < list->len; i++) { + sentry_value_incref(clone->items[i]); + } + } + return clone; +} + +static obj_t * +obj_clone(const obj_t *obj) +{ + obj_t *clone = SENTRY_MAKE(obj_t); + if (!clone) { + return NULL; + } + clone->allocated = obj->len; + clone->refcount = 1; + if (obj->len) { + clone->pairs = sentry_malloc(sizeof(obj_pair_t) * obj->len); + if (!clone->pairs) { + sentry_free(clone); + return NULL; + } + for (size_t i = 0; i < obj->len; i++) { + clone->pairs[i].k = sentry__string_clone(obj->pairs[i].k); + if (!clone->pairs[i].k) { + obj_free(clone); + return NULL; + } + clone->pairs[i].v = obj->pairs[i].v; + sentry_value_incref(clone->pairs[i].v); + clone->len++; + } + } + return clone; +} + static bool thing_detach(thing_t *thing) { @@ -198,29 +251,12 @@ thing_detach(thing_t *thing) if (sentry__atomic_fetch(&old->refcount) <= 1) { return true; } - list_t *new_list = SENTRY_MAKE(list_t); - if (!new_list) { + list_t *clone = list_clone(old); + if (!clone) { return false; } - new_list->len = old->len; - new_list->allocated = old->len; - new_list->refcount = 1; - if (old->len) { - new_list->items = sentry_malloc(sizeof(sentry_value_t) * old->len); - if (!new_list->items) { - sentry_free(new_list); - return false; - } - memcpy( - new_list->items, old->items, sizeof(sentry_value_t) * old->len); - for (size_t i = 0; i < old->len; i++) { - sentry_value_incref(new_list->items[i]); - } - } else { - new_list->items = NULL; - } list_free(old); - thing->payload._ptr = new_list; + thing->payload._ptr = clone; return true; } case THING_TYPE_OBJECT: { @@ -228,34 +264,12 @@ thing_detach(thing_t *thing) if (sentry__atomic_fetch(&old->refcount) <= 1) { return true; } - obj_t *new_obj = SENTRY_MAKE(obj_t); - if (!new_obj) { + obj_t *clone = obj_clone(old); + if (!clone) { return false; } - new_obj->len = 0; - new_obj->allocated = old->len; - new_obj->refcount = 1; - if (old->len) { - new_obj->pairs = sentry_malloc(sizeof(obj_pair_t) * old->len); - if (!new_obj->pairs) { - sentry_free(new_obj); - return false; - } - for (size_t i = 0; i < old->len; i++) { - new_obj->pairs[i].k = sentry__string_clone(old->pairs[i].k); - if (!new_obj->pairs[i].k) { - obj_free(new_obj); - return false; - } - new_obj->pairs[i].v = old->pairs[i].v; - sentry_value_incref(new_obj->pairs[i].v); - new_obj->len++; - } - } else { - new_obj->pairs = NULL; - } obj_free(old); - thing->payload._ptr = new_obj; + thing->payload._ptr = clone; return true; } default: From 5a7bc812885535d6a5d0c9d59cc9b0f0b6daa941 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 21 Jul 2026 09:18:54 +0200 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a86d6c04e..e8161ae5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Fixes**: + +- Fix scope data loss from shared `sentry_value_t` containers while significantly improving scope merge performance with copy-on-write cloning. ([#1794](https://github.com/getsentry/sentry-native/pull/1794)) + ## 0.15.4 **Features**: @@ -18,7 +24,6 @@ **Fixes**: - Apply the propagation context to events that already have contexts set, so that events captured with a local scope or with event-level contexts keep their trace. ([#1843](https://github.com/getsentry/sentry-native/pull/1843)) -- Fix scope data loss from shared `sentry_value_t` containers while significantly improving scope merge performance with copy-on-write cloning. ([#1794](https://github.com/getsentry/sentry-native/pull/1794)) - Crashpad: reject runtime control IPC from processes other than the one that started the handler. ([#1853](https://github.com/getsentry/sentry-native/pull/1853)) - Native/macOS: resolve symbol names for crash stacktraces from Mach-O symbol tables and dSYM companions. ([#1856](https://github.com/getsentry/sentry-native/pull/1856)) - Route libcurl debug output through the Sentry logger (`SENTRY_TRACE`) instead of writing to `stderr`. ([#1854](https://github.com/getsentry/sentry-native/pull/1854))