From 527bbde4c66993ed0153c30ad5045a4e4e24df9a Mon Sep 17 00:00:00 2001 From: w4ffl35 <25737761+w4ffl35@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:28:23 -0600 Subject: [PATCH] Guard print_string_ptr counters against 32-bit overflow (CVE-2026-16554) On 32-bit platforms size_t is 32 bits: escape_characters += 5 wraps past ~858M control characters, and the length sums can wrap too, so ensure() hands out a small buffer while the write loop emits the true (giant) escaped length -- heap overflow, reachable via cJSON_PrintBuffered with a pre-allocated buffer. Fail instead of wrapping at each accumulation step and before sizing the allocation. Add an escape-heavy exactness regression test. --- cJSON.c | 24 +++++++++++++++++++++++- tests/print_string.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/cJSON.c b/cJSON.c index 88c2d95b3..bd2141990 100644 --- a/cJSON.c +++ b/cJSON.c @@ -991,18 +991,40 @@ static cJSON_bool print_string_ptr(const unsigned char * const input, printbuffe case '\r': case '\t': /* one character escape sequence */ + if (escape_characters == (size_t)-1) + { + /* counter would wrap and under-allocate below */ + return false; + } escape_characters++; break; default: if (*input_pointer < 32) { /* UTF-16 escape sequence uXXXX */ + if (escape_characters > ((size_t)-1 - 5)) + { + /* counter would wrap and under-allocate below */ + return false; + } escape_characters += 5; } break; } } - output_length = (size_t)(input_pointer - input) + escape_characters; + { + size_t input_length = (size_t)(input_pointer - input); + /* The allocation below adds the quotes; if any of these sums + * wrapped (32-bit size_t past ~4G, CVE-2026-16554), ensure() + * would hand out a small buffer while the write loop emits + * the true length. Fail instead. */ + if ((escape_characters > ((size_t)-1 - sizeof("\"\""))) || + (input_length > ((size_t)-1 - sizeof("\"\"") - escape_characters))) + { + return false; + } + output_length = input_length + escape_characters; + } output = ensure(output_buffer, output_length + sizeof("\"\"")); if (output == NULL) diff --git a/tests/print_string.c b/tests/print_string.c index e6f5b92dc..6951d3705 100644 --- a/tests/print_string.c +++ b/tests/print_string.c @@ -65,6 +65,36 @@ static void print_string_should_print_utf8(void) assert_print_string("\"ü猫慕\"", "ü猫慕"); } +static void print_string_should_print_escape_heavy_strings_exactly(void) +{ + /* CVE-2026-16554 regression guard: escape counting must stay exact + * at scale so the allocation always matches the emitted length. */ + char input[101]; + char expected[701]; + unsigned char printed[1024]; + printbuffer buffer = { 0, 0, 0, 0, 0, 0, { 0, 0, 0 } }; + size_t i = 0; + + memset(input, 1, 100); + input[100] = '\0'; + expected[0] = '"'; + for (i = 0; i < 100; i++) + { + memcpy(expected + 1 + 6 * i, "\\u0001", 6); + } + expected[601] = '"'; + expected[602] = '\0'; + + buffer.buffer = printed; + buffer.length = sizeof(printed); + buffer.offset = 0; + buffer.noalloc = true; + buffer.hooks = global_hooks; + + TEST_ASSERT_TRUE_MESSAGE(print_string_ptr((const unsigned char*)input, &buffer), "Failed to print string."); + TEST_ASSERT_EQUAL_STRING_MESSAGE(expected, printed, "Escape-heavy output isn't exact."); +} + int CJSON_CDECL main(void) { /* initialize cJSON item */ @@ -73,6 +103,7 @@ int CJSON_CDECL main(void) RUN_TEST(print_string_should_print_empty_strings); RUN_TEST(print_string_should_print_ascii); RUN_TEST(print_string_should_print_utf8); + RUN_TEST(print_string_should_print_escape_heavy_strings_exactly); return UNITY_END(); }