From 2a742e6876cabe26441d3ecf9509de4b0e2384ca Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 7 Aug 2026 14:37:10 +0200 Subject: [PATCH 1/3] ref(value): track string length Since `sentry_value_new_string` calls `strlen` + `sentry_value_new_string_n` anyway, we might as well store the length. This does not only save us from inefficiently calling `strlen` later, but also implicitly adds support for embedded NUL bytes for presenting attachments (#1945). --- src/sentry_json.c | 19 +++++++++--- src/sentry_json.h | 6 ++++ src/sentry_value.c | 67 ++++++++++++++++++++++++++++++++--------- src/sentry_value.h | 5 +++ tests/unit/test_value.c | 9 ++++++ 5 files changed, 87 insertions(+), 19 deletions(-) diff --git a/src/sentry_json.c b/src/sentry_json.c index 08765ae21..263eb6a9e 100644 --- a/src/sentry_json.c +++ b/src/sentry_json.c @@ -22,6 +22,7 @@ #include "sentry_value.h" #define SENTRY_JSON_MAX_DEPTH 64 +#define SENTRY_JSON_STRLEN ((size_t)-1) struct sentry_jsonwriter_s { sentry_writer_t *writer; @@ -201,12 +202,15 @@ static unsigned char needs_escaping[256] = { }; static void -write_json_str(sentry_jsonwriter_t *jw, const char *str) +write_json_str(sentry_jsonwriter_t *jw, const char *str, size_t str_len) { // using unsigned here because utf-8 is > 127 :-) const unsigned char *ptr = (const unsigned char *)str; const unsigned char *start = ptr; - for (; *ptr && !sentry__jsonwriter_has_failed(jw); ptr++) { + const unsigned char *end + = str_len == SENTRY_JSON_STRLEN ? NULL : ptr + str_len; + for (; (end ? ptr < end : *ptr) && !sentry__jsonwriter_has_failed(jw); + ptr++) { if (!needs_escaping[*ptr]) { continue; } @@ -352,6 +356,13 @@ sentry__jsonwriter_write_double(sentry_jsonwriter_t *jw, double val) void sentry__jsonwriter_write_str(sentry_jsonwriter_t *jw, const char *val) +{ + sentry__jsonwriter_write_str_n(jw, val, SENTRY_JSON_STRLEN); +} + +void +sentry__jsonwriter_write_str_n( + sentry_jsonwriter_t *jw, const char *val, size_t val_len) { if (!val) { sentry__jsonwriter_write_null(jw); @@ -359,7 +370,7 @@ sentry__jsonwriter_write_str(sentry_jsonwriter_t *jw, const char *val) } if (can_write_item(jw)) { write_char(jw, '"'); - write_json_str(jw, val); + write_json_str(jw, val, val_len); write_char(jw, '"'); } } @@ -390,7 +401,7 @@ sentry__jsonwriter_write_key(sentry_jsonwriter_t *jw, const char *val) { if (can_write_item(jw)) { write_char(jw, '"'); - write_json_str(jw, val); + write_json_str(jw, val, SENTRY_JSON_STRLEN); write_char(jw, '"'); write_char(jw, ':'); jw->last_was_key = true; diff --git a/src/sentry_json.h b/src/sentry_json.h index 24c26cd00..7a3eebd31 100644 --- a/src/sentry_json.h +++ b/src/sentry_json.h @@ -88,6 +88,12 @@ void sentry__jsonwriter_write_double(sentry_jsonwriter_t *jw, double val); */ void sentry__jsonwriter_write_str(sentry_jsonwriter_t *jw, const char *val); +/** + * Write a string with an explicit byte length. + */ +void sentry__jsonwriter_write_str_n( + sentry_jsonwriter_t *jw, const char *val, size_t val_len); + /** * Write a UUID as a JSON string. * See `sentry_uuid_as_string`. diff --git a/src/sentry_value.c b/src/sentry_value.c index 11d74481f..cbed6e6d3 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -108,6 +108,11 @@ typedef struct { long refcount; } obj_t; +typedef struct { + char *s; + size_t len; +} blob_t; + static const char * level_as_string(sentry_level_t level) { @@ -316,7 +321,11 @@ thing_free(thing_t *thing) obj_free(thing->payload._ptr); break; case THING_TYPE_STRING: - sentry_free(thing->payload._ptr); + if (thing->payload._ptr) { + blob_t *blob = thing->payload._ptr; + sentry_free(blob->s); + sentry_free(blob); + } break; } sentry_free(thing); @@ -379,6 +388,16 @@ value_as_thing(sentry_value_t value) return (thing_t *)(size_t)value._bits; } +static const blob_t * +value_as_blob(sentry_value_t value) +{ + const thing_t *thing = value_as_thing(value); + if (thing && thing_get_type(thing) == THING_TYPE_STRING) { + return (const blob_t *)thing->payload._ptr; + } + return NULL; +} + static thing_t * value_as_unfrozen_thing(sentry_value_t value) { @@ -511,7 +530,7 @@ sentry_value_new_string_n(const char *value, size_t value_len) if (!s) { return sentry_value_new_null(); } - return sentry__value_new_string_owned(s); + return sentry__value_new_string_owned_n(s, value_len); } sentry_value_t @@ -1173,7 +1192,7 @@ sentry_value_get_length(sentry_value_t value) if (thing) { switch (thing_get_type(thing)) { case THING_TYPE_STRING: - return strlen(thing->payload._ptr); + return ((const blob_t *)thing->payload._ptr)->len; case THING_TYPE_LIST: return ((const list_t *)thing->payload._ptr)->len; case THING_TYPE_OBJECT: @@ -1267,9 +1286,9 @@ sentry_value_as_uint64(sentry_value_t value) const char * sentry_value_as_string(sentry_value_t value) { - const thing_t *thing = value_as_thing(value); - if (thing && thing_get_type(thing) == THING_TYPE_STRING) { - return (const char *)thing->payload._ptr; + const blob_t *blob = value_as_blob(value); + if (blob) { + return blob->s; } return ""; } @@ -1393,9 +1412,11 @@ sentry__jsonwriter_write_value(sentry_jsonwriter_t *jw, sentry_value_t value) case SENTRY_VALUE_TYPE_DOUBLE: sentry__jsonwriter_write_double(jw, sentry_value_as_double(value)); break; - case SENTRY_VALUE_TYPE_STRING: - sentry__jsonwriter_write_str(jw, sentry_value_as_string(value)); + case SENTRY_VALUE_TYPE_STRING: { + const blob_t *blob = value_as_blob(value); + sentry__jsonwriter_write_str_n(jw, blob->s, blob->len); break; + } case SENTRY_VALUE_TYPE_LIST: { const thing_t *thing = value_as_thing(value); if (!thing) { @@ -1473,7 +1494,8 @@ value_to_msgpack(mpack_writer_t *writer, sentry_value_t value) mpack_write_double(writer, sentry_value_as_double(value)); break; case SENTRY_VALUE_TYPE_STRING: { - mpack_write_cstr_or_nil(writer, sentry_value_as_string(value)); + const blob_t *blob = value_as_blob(value); + mpack_write_str(writer, blob->s, (uint32_t)blob->len); break; } case SENTRY_VALUE_TYPE_LIST: { @@ -1515,14 +1537,29 @@ sentry_value_to_msgpack(sentry_value_t value, size_t *size_out) sentry_value_t sentry__value_new_string_owned(char *s) +{ + return s ? sentry__value_new_string_owned_n(s, strlen(s)) + : sentry_value_new_null(); +} + +sentry_value_t +sentry__value_new_string_owned_n(char *s, size_t s_len) { if (!s) { return sentry_value_new_null(); } + blob_t *blob = SENTRY_MAKE(blob_t); + if (!blob) { + sentry_free(s); + return sentry_value_new_null(); + } + blob->s = s; + blob->len = s_len; sentry_value_t rv - = new_thing_value(s, THING_TYPE_STRING | THING_TYPE_FROZEN); + = new_thing_value(blob, THING_TYPE_STRING | THING_TYPE_FROZEN); if (sentry_value_is_null(rv)) { - sentry_free(s); + sentry_free(blob->s); + sentry_free(blob); } return rv; } @@ -1564,7 +1601,7 @@ sentry__value_new_hexstring(const uint8_t *bytes, size_t len) written += rv; } buf[written] = '\0'; - return sentry__value_new_string_owned(buf); + return sentry__value_new_string_owned_n(buf, written); } sentry_value_t @@ -1576,7 +1613,7 @@ sentry__value_new_span_uuid(const sentry_uuid_t *uuid) } sentry__span_uuid_as_string(uuid, buf); buf[16] = '\0'; - return sentry__value_new_string_owned(buf); + return sentry__value_new_string_owned_n(buf, 16); } sentry_value_t @@ -1588,7 +1625,7 @@ sentry__value_new_internal_uuid(const sentry_uuid_t *uuid) } sentry__internal_uuid_as_string(uuid, buf); buf[32] = '\0'; - return sentry__value_new_string_owned(buf); + return sentry__value_new_string_owned_n(buf, 32); } sentry_value_t @@ -1600,7 +1637,7 @@ sentry__value_new_uuid(const sentry_uuid_t *uuid) } sentry_uuid_as_string(uuid, buf); buf[36] = '\0'; - return sentry__value_new_string_owned(buf); + return sentry__value_new_string_owned_n(buf, 36); } sentry_value_t diff --git a/src/sentry_value.h b/src/sentry_value.h index ec90c1d95..e6428158d 100644 --- a/src/sentry_value.h +++ b/src/sentry_value.h @@ -8,6 +8,11 @@ */ sentry_value_t sentry__value_new_string_owned(char *s); +/** + * Create a new Value from an owned string with explicit byte length. + */ +sentry_value_t sentry__value_new_string_owned_n(char *s, size_t s_len); + #ifdef SENTRY_PLATFORM_WINDOWS /** * Create a new Value from a Wide String. diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index 04e08310b..bdb1f9cb2 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -269,6 +269,15 @@ SENTRY_TEST(value_string_n) TEST_CHECK(sentry_value_get_type(val) == SENTRY_VALUE_TYPE_STRING); TEST_CHECK(sentry_value_is_true(val) == true); sentry_value_decref(val); + + char string_with_nul[] = { 'h', 'e', '\0', 'l', 'o' }; + val = sentry_value_new_string_n(string_with_nul, sizeof(string_with_nul)); + TEST_CHECK(sentry_value_get_length(val) == sizeof(string_with_nul)); + TEST_CHECK(memcmp(sentry_value_as_string(val), string_with_nul, + sizeof(string_with_nul)) + == 0); + TEST_CHECK_JSON_VALUE(val, "\"he\\u0000lo\""); + sentry_value_decref(val); } SENTRY_TEST(value_unicode) From 7e761ec98510f3005881ed192a6e5448d00a0df5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 7 Aug 2026 15:28:05 +0200 Subject: [PATCH 2/3] revise --- src/sentry_value.c | 66 +++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/sentry_value.c b/src/sentry_value.c index cbed6e6d3..7523662ea 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -194,6 +194,13 @@ obj_free(obj_t *obj) sentry_free(obj); } +static void +blob_free(blob_t *blob) +{ + sentry_free(blob->s); + sentry_free(blob); +} + static list_t * list_clone(const list_t *list) { @@ -321,11 +328,7 @@ thing_free(thing_t *thing) obj_free(thing->payload._ptr); break; case THING_TYPE_STRING: - if (thing->payload._ptr) { - blob_t *blob = thing->payload._ptr; - sentry_free(blob->s); - sentry_free(blob); - } + blob_free(thing->payload._ptr); break; } sentry_free(thing); @@ -388,16 +391,6 @@ value_as_thing(sentry_value_t value) return (thing_t *)(size_t)value._bits; } -static const blob_t * -value_as_blob(sentry_value_t value) -{ - const thing_t *thing = value_as_thing(value); - if (thing && thing_get_type(thing) == THING_TYPE_STRING) { - return (const blob_t *)thing->payload._ptr; - } - return NULL; -} - static thing_t * value_as_unfrozen_thing(sentry_value_t value) { @@ -1286,9 +1279,9 @@ sentry_value_as_uint64(sentry_value_t value) const char * sentry_value_as_string(sentry_value_t value) { - const blob_t *blob = value_as_blob(value); - if (blob) { - return blob->s; + const thing_t *thing = value_as_thing(value); + if (thing && thing_get_type(thing) == THING_TYPE_STRING) { + return ((const blob_t *)thing->payload._ptr)->s; } return ""; } @@ -1413,8 +1406,14 @@ sentry__jsonwriter_write_value(sentry_jsonwriter_t *jw, sentry_value_t value) sentry__jsonwriter_write_double(jw, sentry_value_as_double(value)); break; case SENTRY_VALUE_TYPE_STRING: { - const blob_t *blob = value_as_blob(value); - sentry__jsonwriter_write_str_n(jw, blob->s, blob->len); + const thing_t *thing = value_as_thing(value); + if (!thing) { + UNREACHABLE("thing of a string is NULL during serialization"); + return; + } + + const blob_t *b = thing->payload._ptr; + sentry__jsonwriter_write_str_n(jw, b->s, b->len); break; } case SENTRY_VALUE_TYPE_LIST: { @@ -1494,8 +1493,9 @@ value_to_msgpack(mpack_writer_t *writer, sentry_value_t value) mpack_write_double(writer, sentry_value_as_double(value)); break; case SENTRY_VALUE_TYPE_STRING: { - const blob_t *blob = value_as_blob(value); - mpack_write_str(writer, blob->s, (uint32_t)blob->len); + const blob_t *b = value_as_thing(value)->payload._ptr; + + mpack_write_str(writer, b->s, (uint32_t)b->len); break; } case SENTRY_VALUE_TYPE_LIST: { @@ -1548,20 +1548,20 @@ sentry__value_new_string_owned_n(char *s, size_t s_len) if (!s) { return sentry_value_new_null(); } - blob_t *blob = SENTRY_MAKE(blob_t); - if (!blob) { + blob_t *b = SENTRY_MAKE(blob_t); + if (b) { + b->s = s; + b->len = s_len; + sentry_value_t rv + = new_thing_value(b, THING_TYPE_STRING | THING_TYPE_FROZEN); + if (sentry_value_is_null(rv)) { + blob_free(b); + } + return rv; + } else { sentry_free(s); return sentry_value_new_null(); } - blob->s = s; - blob->len = s_len; - sentry_value_t rv - = new_thing_value(blob, THING_TYPE_STRING | THING_TYPE_FROZEN); - if (sentry_value_is_null(rv)) { - sentry_free(blob->s); - sentry_free(blob); - } - return rv; } #ifdef SENTRY_PLATFORM_WINDOWS From 50e9e3a8e2a10d4f45c23ecfdfe69a6b851917ff Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 7 Aug 2026 15:45:37 +0200 Subject: [PATCH 3/3] support json round-trip with embedded NUL bytes --- src/sentry_json.c | 16 +++++++++------- tests/unit/test_value.c | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/sentry_json.c b/src/sentry_json.c index 263eb6a9e..922880f0d 100644 --- a/src/sentry_json.c +++ b/src/sentry_json.c @@ -470,7 +470,7 @@ read_escaped_unicode_char(const char *buf) } static bool -decode_string_inplace(char *buf) +decode_string_inplace(char *buf, size_t *len_out) { const char *input = buf; char *output = buf; @@ -519,9 +519,7 @@ decode_string_inplace(char *buf) return false; } - if (uchar) { - output += sentry__unichar_to_utf8((uint32_t)uchar, output); - } + output += sentry__unichar_to_utf8((uint32_t)uchar, output); break; } default: @@ -531,6 +529,9 @@ decode_string_inplace(char *buf) #undef SIMPLE_ESCAPE + if (len_out) { + *len_out = (size_t)(output - buf); + } *output = 0; return true; } @@ -615,8 +616,9 @@ tokens_to_value(jsmntok_t *tokens, size_t token_count, const char *buf, case JSMN_STRING: { char *string = sentry__string_clone_n_unchecked( buf + root->start, (size_t)(root->end - root->start)); - if (decode_string_inplace(string)) { - rv = sentry__value_new_string_owned(string); + size_t string_len = 0; + if (decode_string_inplace(string, &string_len)) { + rv = sentry__value_new_string_owned_n(string, string_len); } else { sentry_free(string); rv = sentry_value_new_null(); @@ -639,7 +641,7 @@ tokens_to_value(jsmntok_t *tokens, size_t token_count, const char *buf, char *key = sentry__string_clone_n_unchecked( buf + token->start, (size_t)(token->end - token->start)); - if (decode_string_inplace(key)) { + if (decode_string_inplace(key, NULL)) { sentry_value_set_by_key(rv, key, child); } else { sentry_value_decref(child); diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index bdb1f9cb2..e52ff9572 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -277,6 +277,16 @@ SENTRY_TEST(value_string_n) sizeof(string_with_nul)) == 0); TEST_CHECK_JSON_VALUE(val, "\"he\\u0000lo\""); + + char *json = sentry_value_to_json(val); + sentry_value_t deserialized = sentry__value_from_json(json, strlen(json)); + TEST_CHECK( + sentry_value_get_length(deserialized) == sizeof(string_with_nul)); + TEST_CHECK(memcmp(sentry_value_as_string(deserialized), string_with_nul, + sizeof(string_with_nul)) + == 0); + sentry_free(json); + sentry_value_decref(deserialized); sentry_value_decref(val); } @@ -1132,6 +1142,14 @@ SENTRY_TEST(value_json_parsing) "foo\xe2\x98\x83"); sentry_value_decref(rv); + char string_with_nul[] = { 'h', 'e', '\0', 'l', 'o' }; + rv = sentry__value_from_json(STRING("\"he\\u0000lo\"")); + TEST_CHECK(sentry_value_get_length(rv) == sizeof(string_with_nul)); + TEST_CHECK(memcmp(sentry_value_as_string(rv), string_with_nul, + sizeof(string_with_nul)) + == 0); + sentry_value_decref(rv); + rv = sentry__value_from_json( STRING("[false, 42, \"foo\\u2603\", \"bar\", {\"foo\": 42}]")); TEST_CHECK_JSON_VALUE(rv, "[false,42,\"foo☃\",\"bar\",{\"foo\":42}]");