From 5d23c2ad5d81d1b68c0e6179033539cf5b3489e0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 25 May 2026 12:08:35 +0200 Subject: [PATCH 1/2] fix: Cap JSON parser depth Reject JSON inputs whose object or array nesting exceeds 64 levels. The parser previously recursed once per nested container with no limit, so crafted cached JSON could exhaust the C call stack during SDK startup or envelope/session processing. Use 64 because the JSON writer already uses that maximum depth. Matching the writer preserves the SDK's existing JSON depth policy: JSON the SDK can emit remains accepted, while deeper untrusted input is rejected. Msgpack deserialization was already capped. Raise that existing cap from 32 to 64 because msgpack uses the same recursive value conversion shape, and 64 is the stack-safe bound chosen for JSON. Co-Authored-By: OpenAI Codex --- src/sentry_json.c | 16 ++++++++++++---- src/sentry_value.c | 2 +- tests/unit/test_value.c | 28 ++++++++++++++++++++++++++-- tests/unit/tests.inc | 1 + 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/sentry_json.c b/src/sentry_json.c index 7451b14cf..2f691be4c 100644 --- a/src/sentry_json.c +++ b/src/sentry_json.c @@ -21,6 +21,8 @@ #include "sentry_utils.h" #include "sentry_value.h" +#define SENTRY_JSON_MAX_DEPTH 64 + typedef struct { void (*free)(sentry_jsonwriter_t *writer); void (*write_str)(sentry_jsonwriter_t *writer, const char *str); @@ -207,7 +209,7 @@ sentry__jsonwriter_into_string(sentry_jsonwriter_t *jw, size_t *len_out) static bool at_max_depth(const sentry_jsonwriter_t *jw) { - return jw->depth >= 64; + return jw->depth >= SENTRY_JSON_MAX_DEPTH; } static void @@ -578,7 +580,7 @@ decode_string_inplace(char *buf) static size_t tokens_to_value(jsmntok_t *tokens, size_t token_count, const char *buf, - sentry_value_t *value_out) + size_t depth, sentry_value_t *value_out) { size_t offset = 0; @@ -586,7 +588,7 @@ tokens_to_value(jsmntok_t *tokens, size_t token_count, const char *buf, #define NESTED_PARSE(Target) \ do { \ size_t child_consumed = tokens_to_value( \ - tokens + offset, token_count - offset, buf, Target); \ + tokens + offset, token_count - offset, buf, depth + 1, Target); \ if (child_consumed == (size_t)-1) { \ goto error; \ } \ @@ -665,6 +667,9 @@ tokens_to_value(jsmntok_t *tokens, size_t token_count, const char *buf, break; } case JSMN_OBJECT: { + if (depth >= SENTRY_JSON_MAX_DEPTH) { + goto error; + } rv = sentry_value_new_object(); for (int i = 0; i < root->size; i++) { jsmntok_t *token = POP(); @@ -687,6 +692,9 @@ tokens_to_value(jsmntok_t *tokens, size_t token_count, const char *buf, break; } case JSMN_ARRAY: { + if (depth >= SENTRY_JSON_MAX_DEPTH) { + goto error; + } rv = sentry_value_new_list(); for (int i = 0; i < root->size; i++) { sentry_value_t child; @@ -738,7 +746,7 @@ sentry__value_from_json(const char *buf, size_t buflen) sentry_value_t value_out; size_t tokens_consumed - = tokens_to_value(tokens, (size_t)token_count, buf, &value_out); + = tokens_to_value(tokens, (size_t)token_count, buf, 0, &value_out); sentry_free(tokens); if (tokens_consumed == (size_t)token_count) { diff --git a/src/sentry_value.c b/src/sentry_value.c index e79d34a11..6af42af64 100644 --- a/src/sentry_value.c +++ b/src/sentry_value.c @@ -1643,7 +1643,7 @@ sentry_event_value_add_stacktrace(sentry_value_t event, void **ips, size_t len) sentry_event_add_thread(event, thread); } -#define SENTRY_MPACK_MAX_DEPTH 32 +#define SENTRY_MPACK_MAX_DEPTH 64 static sentry_value_t value_from_mpack(mpack_node_t node, size_t depth, bool *ok) diff --git a/tests/unit/test_value.c b/tests/unit/test_value.c index b9e0f81f2..f6a8e313c 100644 --- a/tests/unit/test_value.c +++ b/tests/unit/test_value.c @@ -41,6 +41,15 @@ write_nested_msgpack_arrays(char *buf, size_t depth) buf[depth] = (char)0xc0; // nil } +static void +write_nested_json_arrays(char *buf, size_t depth) +{ + memset(buf, '[', depth); + memcpy(buf + depth, "null", 4); + memset(buf + depth + 4, ']', depth); + buf[depth * 2 + 4] = '\0'; +} + SENTRY_TEST(value_null) { sentry_value_t val = sentry_value_new_null(); @@ -881,6 +890,21 @@ SENTRY_TEST(value_json_deeply_nested) sentry_value_decref(parsed); } +SENTRY_TEST(value_json_max_depth) +{ + char accepted[64 * 2 + sizeof("null")]; // SENTRY_JSON_MAX_DEPTH + write_nested_json_arrays(accepted, 64); + sentry_value_t value = sentry__value_from_json(accepted, strlen(accepted)); + TEST_CHECK(sentry_value_get_type(value) == SENTRY_VALUE_TYPE_LIST); + sentry_value_decref(value); + + char too_deep[65 * 2 + sizeof("null")]; // SENTRY_JSON_MAX_DEPTH + 1 + write_nested_json_arrays(too_deep, 65); + value = sentry__value_from_json(too_deep, strlen(too_deep)); + TEST_CHECK(sentry_value_is_null(value)); + sentry_value_decref(value); +} + SENTRY_TEST(value_json_escaping) { sentry_value_t rv = sentry__value_from_json( @@ -1558,14 +1582,14 @@ SENTRY_TEST(value_from_msgpack_list) SENTRY_TEST(value_from_msgpack_deeply_nested) { - char accepted[32 + 1]; // SENTRY_MPACK_MAX_DEPTH + 1 + char accepted[64 + 1]; // SENTRY_MPACK_MAX_DEPTH + 1 write_nested_msgpack_arrays(accepted, sizeof(accepted) - 1); sentry_value_t value = sentry__value_from_msgpack(accepted, sizeof(accepted)); TEST_CHECK(sentry_value_get_type(value) == SENTRY_VALUE_TYPE_LIST); sentry_value_decref(value); - char too_deep[32 + 2]; // SENTRY_MPACK_MAX_DEPTH + 2 + char too_deep[64 + 2]; // SENTRY_MPACK_MAX_DEPTH + 2 write_nested_msgpack_arrays(too_deep, sizeof(too_deep) - 1); value = sentry__value_from_msgpack(too_deep, sizeof(too_deep)); TEST_CHECK(sentry_value_is_null(value)); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index b1a224831..f84fb8117 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -369,6 +369,7 @@ XX(value_json_deeply_nested) XX(value_json_escaping) XX(value_json_invalid_doubles) XX(value_json_locales) +XX(value_json_max_depth) XX(value_json_parsing) XX(value_json_surrogates) XX(value_list) From dd12808be71c381fcf46f7fa208b717d5055dc3b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 25 May 2026 12:18:40 +0200 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c79f5b03..8fba3605b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Native/macOS: fix module `image_size` computation, which could have caused the symbolicator to misattribute every frame to the lowest-addressed image (typically `dyld` or `libsystem`). ([#1740](https://github.com/getsentry/sentry-native/pull/1740)) - Native: raise `SENTRY_CRASH_MAX_MODULES` from `512` to `2048` so processes that load many shared libraries no longer have their minidump module list truncated, which left frames in unrecorded modules without a `debug_id` and unsymbolicatable. ([#1738](https://github.com/getsentry/sentry-native/pull/1738)) -- Reject overly deep msgpack payloads during deserialization. ([#1727](https://github.com/getsentry/sentry-native/pull/1727)) +- Reject overly deep JSON and msgpack payloads during deserialization. ([#1727](https://github.com/getsentry/sentry-native/pull/1727), [#1748](https://github.com/getsentry/sentry-native/pull/1748)) - Read lengths for variadic fingerprints. ([#1730](https://github.com/getsentry/sentry-native/pull/1730)) - Guard against JSON token allocation overflow on 32-bit platforms. ([#1733](https://github.com/getsentry/sentry-native/pull/1733)) - Windows: fix HTTP rate limit response header parsing. ([#1732](https://github.com/getsentry/sentry-native/pull/1732))