From 3d301785761133a6dc472aa288d762f6fc281ff0 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:24:12 -0700 Subject: [PATCH 01/15] F-11599 - https://fenrir.wolfssl.com/finding/11599 - parse documented attached and negative array index path forms --- src/json/centijson_value.c | 57 ++++++++++-- tests/unittests.c | 181 +++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 10 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index 51cf405..c3044e6 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -320,30 +320,67 @@ json_value_path(JSON_VALUE* root, const char* path) { const unsigned char* token_beg = (const unsigned char *)path; const unsigned char* token_end; + const unsigned char* p; JSON_VALUE* v = root; + size_t path_index; + size_t arr_size; + int negative; + int have_digit; while(1) { token_end = token_beg; while(*token_end != '\0' && *token_end != '/') token_end++; - if(token_end - token_beg > 2 && token_beg[0] == '[' && token_end[-1] == ']') { - size_t path_index = 0; + p = token_beg; - token_beg++; - while('0' <= *token_beg && *token_beg <= '9') { - path_index = (path_index * 10U) + (*token_beg - (unsigned)'0'); - token_beg++; + /* Optional leading dictionary key, up to the first '[' or token end. */ + if(p < token_end && *p != '[') { + const unsigned char* key_end = p; + while(key_end < token_end && *key_end != '[') + key_end++; + v = json_value_dict_get_(v, p, (size_t)(key_end - p)); + if(v == NULL) + return NULL; + p = key_end; + } + + /* Zero or more attached array indexes "[n]", where n may be negative + * (relative to the array length). */ + while(p < token_end && *p == '[') { + p++; + negative = 0; + if(*p == '-') { + negative = 1; + p++; } - if(*token_beg != ']') + path_index = 0; + have_digit = 0; + while('0' <= *p && *p <= '9') { + size_t digit = (size_t)(*p - (unsigned char)'0'); + if(path_index > (SIZE_MAX - digit) / 10U) + return NULL; + path_index = (path_index * 10U) + digit; + have_digit = 1; + p++; + } + if((! have_digit) || (*p != ']')) return NULL; + p++; + if(negative) { + arr_size = json_value_array_size(v); + if((path_index == 0) || (path_index > arr_size)) + return NULL; + path_index = arr_size - path_index; + } v = json_value_array_get(v, path_index); - } else if(token_end - token_beg > 0) { - v = json_value_dict_get_(v, token_beg, (size_t)(token_end - token_beg)); + if(v == NULL) + return NULL; } - if(v == NULL) + /* The token must be fully consumed; reject trailing bytes. */ + if(p != token_end) return NULL; if(*token_end == '\0') diff --git a/tests/unittests.c b/tests/unittests.c index 7a75686..3a40c88 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5466,6 +5466,180 @@ static int test_json(const char *fname, const char *extra_fname) { WOLFSENTRY_RETURN_OK; } +#ifdef WOLFSENTRY_HAVE_JSON_DOM + +/* Regression coverage for the centijson JSON_VALUE (DOM) public API. Uses a + * controllable test allocator so the error paths can be exercised through the + * public interface. */ + +static size_t test_cjv_live = 0; /* count of live allocations */ +static size_t test_cjv_fail_big = 0; /* if nonzero, fail allocations >= this size */ +static int test_cjv_zero_null = 0; /* if set, zero-size requests return NULL */ +static long test_cjv_budget = -1; /* if >= 0, allow this many mallocs then fail */ + +static void test_cjv_reset(void) { + test_cjv_live = 0; + test_cjv_fail_big = 0; + test_cjv_zero_null = 0; + test_cjv_budget = -1; +} + +#ifdef WOLFSENTRY_THREADSAFE +static void *test_cjv_malloc(void *context, struct wolfsentry_thread_context *thread, size_t size) +#else +static void *test_cjv_malloc(void *context, size_t size) +#endif +{ + void *p; + (void)context; +#ifdef WOLFSENTRY_THREADSAFE + (void)thread; +#endif + if (test_cjv_zero_null && (size == 0)) + return NULL; + if ((test_cjv_fail_big != 0) && (size >= test_cjv_fail_big)) + return NULL; + if (test_cjv_budget >= 0) { + if (test_cjv_budget == 0) + return NULL; + --test_cjv_budget; + } + p = malloc(size); + if (p != NULL) + ++test_cjv_live; + return p; +} + +#ifdef WOLFSENTRY_THREADSAFE +static void test_cjv_free(void *context, struct wolfsentry_thread_context *thread, void *ptr) +#else +static void test_cjv_free(void *context, void *ptr) +#endif +{ + (void)context; +#ifdef WOLFSENTRY_THREADSAFE + (void)thread; +#endif + if (ptr != NULL) + --test_cjv_live; + free(ptr); +} + +#ifdef WOLFSENTRY_THREADSAFE +static void *test_cjv_realloc(void *context, struct wolfsentry_thread_context *thread, void *ptr, size_t size) +#else +static void *test_cjv_realloc(void *context, void *ptr, size_t size) +#endif +{ + void *p; + int was_null; + (void)context; +#ifdef WOLFSENTRY_THREADSAFE + (void)thread; +#endif + if (test_cjv_zero_null && (size == 0)) { + if (ptr != NULL) + --test_cjv_live; + free(ptr); + return NULL; + } + if ((test_cjv_fail_big != 0) && (size >= test_cjv_fail_big)) + return NULL; + was_null = (ptr == NULL); + p = realloc(ptr, size); + if (p != NULL) { + if (was_null) + ++test_cjv_live; + } else if ((size == 0) && (! was_null)) { + --test_cjv_live; + } + return p; +} + +static int test_cjvalue_path(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE root; + JSON_VALUE *list; + JSON_VALUE *elem; + JSON_VALUE *found; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &root) < 0); + + list = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &root, (const unsigned char *)"list", 4); + WOLFSENTRY_EXIT_ON_TRUE(list == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), list) < 0); + + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), list); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, 10) < 0); + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), list); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, 20) < 0); + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), list); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, 30) < 0); + + /* Attached array index (key followed immediately by a bracket). */ + found = json_value_path(&root, "list[0]"); + WOLFSENTRY_EXIT_ON_TRUE(found == NULL); + WOLFSENTRY_EXIT_ON_FALSE(json_value_int32(found) == 10); + + found = json_value_path(&root, "list[2]"); + WOLFSENTRY_EXIT_ON_TRUE(found == NULL); + WOLFSENTRY_EXIT_ON_FALSE(json_value_int32(found) == 30); + + /* Slash-separated index is a documented equivalent of the attached form. */ + found = json_value_path(&root, "list/[1]"); + WOLFSENTRY_EXIT_ON_TRUE(found == NULL); + WOLFSENTRY_EXIT_ON_FALSE(json_value_int32(found) == 20); + + /* Negative indexes are relative to the array length. */ + found = json_value_path(&root, "list[-1]"); + WOLFSENTRY_EXIT_ON_TRUE(found == NULL); + WOLFSENTRY_EXIT_ON_FALSE(json_value_int32(found) == 30); + + found = json_value_path(&root, "list[-3]"); + WOLFSENTRY_EXIT_ON_TRUE(found == NULL); + WOLFSENTRY_EXIT_ON_FALSE(json_value_int32(found) == 10); + + /* Out-of-range indexes resolve to NULL. */ + WOLFSENTRY_EXIT_ON_FALSE(json_value_path(&root, "list[3]") == NULL); + WOLFSENTRY_EXIT_ON_FALSE(json_value_path(&root, "list[-4]") == NULL); + + /* Trailing bytes after a bracket index must be rejected. */ + WOLFSENTRY_EXIT_ON_FALSE(json_value_path(&root, "list/[0]garbage]") == NULL); + + /* An index that overflows size_t must be rejected, not wrapped. */ + WOLFSENTRY_EXIT_ON_FALSE(json_value_path(&root, "list/[18446744073709551616]") == NULL); + + /* An empty path returns the root. */ + WOLFSENTRY_EXIT_ON_FALSE(json_value_path(&root, "") == &root); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &root) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + +static wolfsentry_errcode_t test_cjvalue_fixes(void) { + struct wolfsentry_allocator test_allocator; + WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); + + memset(&test_allocator, 0, sizeof test_allocator); + test_allocator.malloc = test_cjv_malloc; + test_allocator.free = test_cjv_free; + test_allocator.realloc = test_cjv_realloc; + + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_path(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); + + WOLFSENTRY_RETURN_OK; +} + +#endif /* WOLFSENTRY_HAVE_JSON_DOM */ + #endif /* TEST_JSON */ #ifdef TEST_JSON_CORPUS @@ -5784,6 +5958,13 @@ int main(int argc, char* argv[]) { (void)fprintf(stderr, "test_json failed for " TEST_NUMERIC_JSON_CONFIG_PATH ", " WOLFSENTRY_ERROR_FMT "\n", WOLFSENTRY_ERROR_FMT_ARGS(ret)); err = 1; } +#ifdef WOLFSENTRY_HAVE_JSON_DOM + ret = test_cjvalue_fixes(); + if (! WOLFSENTRY_ERROR_CODE_IS(ret, OK)) { + (void)fprintf(stderr, "test_cjvalue_fixes failed, " WOLFSENTRY_ERROR_FMT "\n", WOLFSENTRY_ERROR_FMT_ARGS(ret)); + err = 1; + } +#endif #endif #ifdef TEST_JSON_CORPUS From 76a15155e25d0a62ca10731a246a6227ff84c2c6 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:24:52 -0700 Subject: [PATCH 02/15] F-8777 - https://fenrir.wolfssl.com/finding/8777 - reject wrapping index and count in array range removal --- src/json/centijson_value.c | 14 +++++++++----- tests/unittests.c | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index c3044e6..9b7e1e7 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1027,12 +1027,16 @@ json_value_array_remove_range( { ARRAY* a = json_value_array_payload(v); size_t i; + size_t end; int ret; - if(a == NULL || index + count > a->size) + /* Validate without adding, so index + count cannot wrap. */ + if(a == NULL || index > a->size || count > a->size - index) return -1; - for(i = index; i < index + count; i++) { + end = index + count; + + for(i = index; i < end; i++) { ret = json_value_fini( #ifdef WOLFSENTRY WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), @@ -1042,9 +1046,9 @@ json_value_array_remove_range( return ret; } - if(index + count < a->size) { - memmove(a->json_value_buf + index, a->json_value_buf + index + count, - (a->size - (index + count)) * sizeof(JSON_VALUE)); + if(end < a->size) { + memmove(a->json_value_buf + index, a->json_value_buf + end, + (a->size - end) * sizeof(JSON_VALUE)); } a->size -= count; diff --git a/tests/unittests.c b/tests/unittests.c index 3a40c88..63336ce 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5621,6 +5621,36 @@ static int test_cjvalue_path(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_all return 0; } +static int test_cjvalue_array_remove_range(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE arr; + JSON_VALUE *elem; + int i; + int ret; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr) < 0); + for (i = 0; i < 3; ++i) { + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, i) < 0); + } + WOLFSENTRY_EXIT_ON_FALSE(json_value_array_size(&arr) == 3); + + /* index + count must not wrap past the array size. */ + ret = json_value_array_remove_range(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr, 3, SIZE_MAX); + WOLFSENTRY_EXIT_ON_FALSE(ret == -1); + WOLFSENTRY_EXIT_ON_FALSE(json_value_array_size(&arr) == 3); + + /* a legitimate removal still works. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_array_remove_range(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr, 1, 1) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_array_size(&arr) == 2); + WOLFSENTRY_EXIT_ON_FALSE(json_value_int32(json_value_array_get(&arr, 1)) == 2); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5633,6 +5663,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_path(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_array_remove_range(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 5ee8a0b00d1cac2e3aff8462f77c6728dea8f62b Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:27:19 -0700 Subject: [PATCH 03/15] F-8763 - https://fenrir.wolfssl.com/finding/8763 - finalize cloned array on failure and track only initialized elements --- src/json/centijson_value.c | 49 ++++++++++++++++------------ tests/unittests.c | 66 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index 9b7e1e7..fa5e509 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1917,38 +1917,45 @@ json_value_clone(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allo const ARRAY* src_a = json_value_array_payload((JSON_VALUE *)node); size_t n = json_value_array_size(node); const JSON_VALUE* src_values = json_value_array_get_all(node); - ARRAY* dest_a; - JSON_VALUE* dest_values; + ARRAY* dest_a = NULL; + JSON_VALUE* dest_values = NULL; size_t i; ret = json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), clone); - if (ret < 0) - break; - dest_a = json_value_array_payload(clone); - if (! dest_a) { - ret = JSON_ERR_INTERNAL; - break; + if (ret == 0) { + dest_a = json_value_array_payload(clone); + if (dest_a == NULL) + ret = JSON_ERR_INTERNAL; } - ret = json_value_array_realloc(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), - dest_a, src_a->alloc); - if (ret < 0) - break; + if (ret == 0) + ret = json_value_array_realloc(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), + dest_a, src_a->alloc); - dest_values = json_value_array_get_all(clone); - if (! dest_values) { - ret = JSON_ERR_INTERNAL; - break; + if (ret == 0) { + dest_values = json_value_array_get_all(clone); + if (dest_values == NULL) + ret = JSON_ERR_INTERNAL; } - for(i = 0; i < n; i++) { - ret = json_value_clone(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src_values[i], &dest_values[i]); - if (ret < 0) - break; + if (ret == 0) { + for(i = 0; i < n; i++) { + json_value_init_null(&dest_values[i]); + ret = json_value_clone(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src_values[i], &dest_values[i]); + if (ret < 0) + break; + dest_a->size = i + 1; + } } - dest_a->size = src_a->size; + /* On any failure finalize the clone so the initialized prefix and + * the array allocation are released, not leaked. */ + if (ret < 0) { + int ret2 = json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), clone); + if (ret2 < 0) + WOLFSENTRY_WARN("json_value_fini: %s\n", json_error_str(ret2)); + } break; } diff --git a/tests/unittests.c b/tests/unittests.c index 63336ce..c44d506 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5651,6 +5651,69 @@ static int test_cjvalue_array_remove_range(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct return 0; } +static int test_cjvalue_clone_array_partial(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE src; + JSON_VALUE clone; + JSON_VALUE *elem; + JSON_VALUE *inner; + unsigned char small_str[50]; + unsigned char big_str[256]; + int i; + int ret; + + memset(small_str, 'a', sizeof small_str); + memset(big_str, 'b', sizeof big_str); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src) < 0); + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_string_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, small_str, sizeof small_str) < 0); + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_string_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, big_str, sizeof big_str) < 0); + + json_value_init_null(&clone); + + /* Force the clone of the second (large) element to fail partway. */ + test_cjv_fail_big = 200; + ret = json_value_clone(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src, &clone); + test_cjv_fail_big = 0; + WOLFSENTRY_EXIT_ON_FALSE(ret < 0); + + /* The failed clone must finalize the already-cloned prefix, leaking nothing. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + /* Also exercise failure on a partially-built nested aggregate element: a + * nested array whose element buffer allocation fails after its payload was + * allocated must not leak that payload. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src) < 0); + inner = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src); + WOLFSENTRY_EXIT_ON_TRUE(inner == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), inner) < 0); + for (i = 0; i < 16; ++i) { + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), inner); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, i) < 0); + inner = json_value_array_get(&src, 0); + WOLFSENTRY_EXIT_ON_TRUE(inner == NULL); + } + + json_value_init_null(&clone); + /* The clone of the 16-slot inner array needs a 256-byte element buffer, + * which fails after the inner array payload has been allocated. */ + test_cjv_fail_big = 200; + ret = json_value_clone(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src, &clone); + test_cjv_fail_big = 0; + WOLFSENTRY_EXIT_ON_FALSE(ret < 0); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5666,6 +5729,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_array_remove_range(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_clone_array_partial(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 902f1066c957cef55615cc4ee08fc3dfba258955 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:28:03 -0700 Subject: [PATCH 04/15] F-13882 - https://fenrir.wolfssl.com/finding/13882 - clone empty array without a zero-size allocation --- src/json/centijson_value.c | 6 ++++-- tests/unittests.c | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index fa5e509..5b4259e 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1929,13 +1929,15 @@ json_value_clone(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allo ret = JSON_ERR_INTERNAL; } - if (ret == 0) + /* Skip the element buffer entirely for an empty array; a zero-size + * allocation may legitimately return NULL. */ + if ((ret == 0) && (src_a->alloc > 0)) ret = json_value_array_realloc(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), dest_a, src_a->alloc); if (ret == 0) { dest_values = json_value_array_get_all(clone); - if (dest_values == NULL) + if ((n > 0) && (dest_values == NULL)) ret = JSON_ERR_INTERNAL; } diff --git a/tests/unittests.c b/tests/unittests.c index c44d506..bf523da 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5714,6 +5714,31 @@ static int test_cjvalue_clone_array_partial(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct return 0; } +static int test_cjvalue_clone_empty_array(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE src; + JSON_VALUE clone; + int ret; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src) < 0); + json_value_init_null(&clone); + + /* With an allocator that returns NULL for zero-size requests, cloning an + * empty array must still succeed (it needs no element storage). */ + test_cjv_zero_null = 1; + ret = json_value_clone(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src, &clone); + test_cjv_zero_null = 0; + WOLFSENTRY_EXIT_ON_FALSE(ret >= 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_type(&clone) == JSON_VALUE_ARRAY); + WOLFSENTRY_EXIT_ON_FALSE(json_value_array_size(&clone) == 0); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &clone) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &src) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5732,6 +5757,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_clone_array_partial(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_clone_empty_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 737dc5f131a2278dc09435ea0b218ba0f9d5ddaf Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:29:34 -0700 Subject: [PATCH 05/15] F-10491 - https://fenrir.wolfssl.com/finding/10491 - reject lossy numeric conversions in compatibility predicate --- src/json/centijson_value.c | 36 +++++++++++++++++++++++---- tests/unittests.c | 50 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index 5b4259e..4d3110f 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -56,6 +56,7 @@ #endif #include +#include #ifndef WOLFSENTRY_NO_ALLOCA #include #endif @@ -255,6 +256,27 @@ json_value_type(const JSON_VALUE* v) return (JSON_VALUE_TYPE)(v->data.data_bytes[0] & TYPE_MASK); } +static uint64_t json_int_magnitude(int64_t x) +{ + return (x < 0) ? (~(uint64_t)x + 1U) : (uint64_t)x; +} + +/* An integer is exactly representable in a binary floating type with + * mantissa_bits of precision when its significant bits (ignoring trailing + * zeros) fit in that precision. */ +static int json_int_fits_mantissa(uint64_t u, int mantissa_bits) +{ + int bits = 0; + + while((u != 0) && ((u & 1U) == 0)) + u >>= 1; + while(u != 0) { + ++bits; + u >>= 1; + } + return bits <= mantissa_bits; +} + WOLFSENTRY_API int json_value_is_compatible(const JSON_VALUE* v, JSON_VALUE_TYPE type) { @@ -265,22 +287,26 @@ json_value_is_compatible(const JSON_VALUE* v, JSON_VALUE_TYPE type) * long as the conversion does not loose much information. */ switch(json_value_type(v)) { case JSON_VALUE_INT32: - return (type == JSON_VALUE_INT64 || type == JSON_VALUE_FLOAT || type == JSON_VALUE_DOUBLE) || + return (type == JSON_VALUE_INT64 || type == JSON_VALUE_DOUBLE) || + (type == JSON_VALUE_FLOAT && json_int_fits_mantissa(json_int_magnitude(json_value_int32(v)), FLT_MANT_DIG)) || (type == JSON_VALUE_UINT32 && json_value_int32(v) >= 0) || (type == JSON_VALUE_UINT64 && json_value_int32(v) >= 0); case JSON_VALUE_UINT32: - return (type == JSON_VALUE_INT64 || type == JSON_VALUE_UINT64 || type == JSON_VALUE_FLOAT || type == JSON_VALUE_DOUBLE) || + return (type == JSON_VALUE_INT64 || type == JSON_VALUE_UINT64 || type == JSON_VALUE_DOUBLE) || + (type == JSON_VALUE_FLOAT && json_int_fits_mantissa((uint64_t)json_value_uint32(v), FLT_MANT_DIG)) || (type == JSON_VALUE_INT32 && json_value_uint32(v) <= INT32_MAX); case JSON_VALUE_INT64: - return (type == JSON_VALUE_FLOAT || type == JSON_VALUE_DOUBLE) || + return (type == JSON_VALUE_DOUBLE && json_int_fits_mantissa(json_int_magnitude(json_value_int64(v)), DBL_MANT_DIG)) || + (type == JSON_VALUE_FLOAT && json_int_fits_mantissa(json_int_magnitude(json_value_int64(v)), FLT_MANT_DIG)) || (type == JSON_VALUE_INT32 && json_value_int64(v) >= INT32_MIN && json_value_int64(v) <= INT32_MAX) || (type == JSON_VALUE_UINT32 && json_value_int64(v) >= 0 && json_value_int64(v) <= UINT32_MAX) || (type == JSON_VALUE_UINT64 && json_value_int64(v) >= 0); case JSON_VALUE_UINT64: - return (type == JSON_VALUE_FLOAT || type == JSON_VALUE_DOUBLE) || + return (type == JSON_VALUE_DOUBLE && json_int_fits_mantissa(json_value_uint64(v), DBL_MANT_DIG)) || + (type == JSON_VALUE_FLOAT && json_int_fits_mantissa(json_value_uint64(v), FLT_MANT_DIG)) || (type == JSON_VALUE_INT32 && json_value_uint64(v) <= INT32_MAX) || (type == JSON_VALUE_UINT32 && json_value_uint64(v) <= UINT32_MAX) || (type == JSON_VALUE_INT64 && json_value_uint64(v) <= INT64_MAX); @@ -293,7 +319,7 @@ json_value_is_compatible(const JSON_VALUE* v, JSON_VALUE_TYPE type) (type == JSON_VALUE_UINT64 && json_value_float(v) == (float)json_value_uint64(v)); case JSON_VALUE_DOUBLE: - return (type == JSON_VALUE_FLOAT) || + return (type == JSON_VALUE_FLOAT && json_value_double(v) >= -(double)FLT_MAX && json_value_double(v) <= (double)FLT_MAX && (double)(float)json_value_double(v) == json_value_double(v)) || (type == JSON_VALUE_INT32 && json_value_double(v) == (double)json_value_int32(v)) || (type == JSON_VALUE_UINT32 && json_value_double(v) == (double)json_value_uint32(v)) || (type == JSON_VALUE_INT64 && json_value_double(v) == (double)json_value_int64(v)) || diff --git a/tests/unittests.c b/tests/unittests.c index bf523da..15eb983 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5739,6 +5739,53 @@ static int test_cjvalue_clone_empty_array(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct w return 0; } +static int test_cjvalue_compatible_lossy(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE v; + + /* A finite double outside the float range is not float-compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_double(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, 1e308) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_FLOAT)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* A small double is float-compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_double(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, 2.0) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_is_compatible(&v, JSON_VALUE_FLOAT)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* 2^24 + 1 is not exactly representable in a float. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (int64_t)16777217) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_FLOAT)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* 2^24 + 2 is exactly representable in a float and must stay compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (int64_t)16777218) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_is_compatible(&v, JSON_VALUE_FLOAT)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* A small integer is float-compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (int32_t)1000) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_is_compatible(&v, JSON_VALUE_FLOAT)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* 2^53 + 1 is not exactly representable in a double. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_uint64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (uint64_t)9007199254740993ULL) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_DOUBLE)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* 2^53 + 2 is exactly representable in a double and must stay compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_uint64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (uint64_t)9007199254740994ULL) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_is_compatible(&v, JSON_VALUE_DOUBLE)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* A small integer is double-compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (int32_t)5) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_is_compatible(&v, JSON_VALUE_DOUBLE)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5760,6 +5807,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_clone_empty_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_compatible_lossy(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 962014d2dc585a1d17f3f27dc4f60386161bbb6a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:30:49 -0700 Subject: [PATCH 06/15] F-13888 - https://fenrir.wolfssl.com/finding/13888 - range check floating values before casting in compatibility predicate --- src/json/centijson_value.c | 16 ++++++++-------- tests/unittests.c | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index 4d3110f..458fc48 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -313,17 +313,17 @@ json_value_is_compatible(const JSON_VALUE* v, JSON_VALUE_TYPE type) case JSON_VALUE_FLOAT: return (type == JSON_VALUE_DOUBLE) || - (type == JSON_VALUE_INT32 && json_value_float(v) == (float)json_value_int32(v)) || - (type == JSON_VALUE_UINT32 && json_value_float(v) == (float)json_value_uint32(v)) || - (type == JSON_VALUE_INT64 && json_value_float(v) == (float)json_value_int64(v)) || - (type == JSON_VALUE_UINT64 && json_value_float(v) == (float)json_value_uint64(v)); + (type == JSON_VALUE_INT32 && (double)json_value_float(v) >= -2147483648.0 && (double)json_value_float(v) <= 2147483647.0 && json_value_float(v) == (float)json_value_int32(v)) || + (type == JSON_VALUE_UINT32 && (double)json_value_float(v) >= 0.0 && (double)json_value_float(v) <= 4294967295.0 && json_value_float(v) == (float)json_value_uint32(v)) || + (type == JSON_VALUE_INT64 && (double)json_value_float(v) >= -9223372036854775808.0 && (double)json_value_float(v) < 9223372036854775808.0 && json_value_float(v) == (float)json_value_int64(v)) || + (type == JSON_VALUE_UINT64 && (double)json_value_float(v) >= 0.0 && (double)json_value_float(v) < 18446744073709551616.0 && json_value_float(v) == (float)json_value_uint64(v)); case JSON_VALUE_DOUBLE: return (type == JSON_VALUE_FLOAT && json_value_double(v) >= -(double)FLT_MAX && json_value_double(v) <= (double)FLT_MAX && (double)(float)json_value_double(v) == json_value_double(v)) || - (type == JSON_VALUE_INT32 && json_value_double(v) == (double)json_value_int32(v)) || - (type == JSON_VALUE_UINT32 && json_value_double(v) == (double)json_value_uint32(v)) || - (type == JSON_VALUE_INT64 && json_value_double(v) == (double)json_value_int64(v)) || - (type == JSON_VALUE_UINT64 && json_value_double(v) == (double)json_value_uint64(v)); + (type == JSON_VALUE_INT32 && json_value_double(v) >= -2147483648.0 && json_value_double(v) <= 2147483647.0 && json_value_double(v) == (double)json_value_int32(v)) || + (type == JSON_VALUE_UINT32 && json_value_double(v) >= 0.0 && json_value_double(v) <= 4294967295.0 && json_value_double(v) == (double)json_value_uint32(v)) || + (type == JSON_VALUE_INT64 && json_value_double(v) >= -9223372036854775808.0 && json_value_double(v) < 9223372036854775808.0 && json_value_double(v) == (double)json_value_int64(v)) || + (type == JSON_VALUE_UINT64 && json_value_double(v) >= 0.0 && json_value_double(v) < 18446744073709551616.0 && json_value_double(v) == (double)json_value_uint64(v)); default: break; diff --git a/tests/unittests.c b/tests/unittests.c index 15eb983..f3f9cee 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5786,6 +5786,37 @@ static int test_cjvalue_compatible_lossy(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wo return 0; } +static int test_cjvalue_compatible_float_to_int(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE v; + + /* Out-of-range float/double values must not be treated as integer-compatible, + * and the predicate must not perform an out-of-range floating-to-integer cast. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_float(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, 1e30F) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_INT32)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_INT64)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_UINT64)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_double(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, 1e300) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_INT32)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_INT64)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_UINT64)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* An in-range integral float is integer-compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_float(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, 42.0F) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_is_compatible(&v, JSON_VALUE_INT32)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + /* A non-integral double is not integer-compatible. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_double(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, 1.5) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_INT32)); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5810,6 +5841,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_compatible_lossy(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_compatible_float_to_int(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 156d92f45b608af3bb2ba09e0a70d98bf4bd4e35 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:32:18 -0700 Subject: [PATCH 07/15] F-11604 - https://fenrir.wolfssl.com/finding/11604 - free removed dictionary node and keep size consistent on finalize failure --- src/json/centijson_value.c | 13 ++++----- tests/unittests.c | 55 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index 458fc48..bc01a31 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1623,6 +1623,7 @@ json_value_dict_remove_( int path_len = 0; int cmp; int ret; + int ret2; if (node == NULL) return -1; @@ -1740,24 +1741,24 @@ json_value_dict_remove_( else d->order_tail = node->order_prev; } + /* The node is already unlinked, so finalize both members best-effort, then + * always free it and decrement the size to keep the dictionary consistent. */ ret = json_value_fini( #ifdef WOLFSENTRY WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), #endif &node->key); - if (ret < 0) - return ret; - ret = json_value_fini( + ret2 = json_value_fini( #ifdef WOLFSENTRY WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), #endif &node->json_value); - if (ret < 0) - return ret; + if (ret == 0) + ret = ret2; free(node); d->size--; - return 0; + return ret; } WOLFSENTRY_API int diff --git a/tests/unittests.c b/tests/unittests.c index f3f9cee..3b293e0 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5817,6 +5817,58 @@ static int test_cjvalue_compatible_float_to_int(WOLFSENTRY_CONTEXT_ARGS_IN_EX(st return 0; } +static int test_cjvalue_dict_remove_orphan(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE outer; + JSON_VALUE nested_copy; + JSON_VALUE *nested; + JSON_VALUE *slot; + char keybuf[8]; + int i; + int ret; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer) < 0); + + nested = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer, (const unsigned char *)"a", 1); + WOLFSENTRY_EXIT_ON_TRUE(nested == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), nested) < 0); + for (i = 0; i < 4; ++i) { + (void)snprintf(keybuf, sizeof keybuf, "k%03d", i); + slot = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), nested, (const unsigned char *)keybuf, strlen(keybuf)); + WOLFSENTRY_EXIT_ON_TRUE(slot == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), slot, (int32_t)i) < 0); + } + + slot = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer, (const unsigned char *)"b", 1); + WOLFSENTRY_EXIT_ON_TRUE(slot == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), slot, (int32_t)1) < 0); + + WOLFSENTRY_EXIT_ON_FALSE(json_value_dict_size(&outer) == 2); + + /* Keep an alias to the nested dictionary so the test can reclaim it after + * removal frees the owning node (see below). */ + nested = json_value_dict_get(&outer, (const unsigned char *)"a"); + WOLFSENTRY_EXIT_ON_TRUE(nested == NULL); + memcpy(&nested_copy, nested, sizeof nested_copy); + + /* Force value finalization (nested dictionary cleanup) to fail during removal. */ + test_cjv_budget = 0; + ret = json_value_dict_remove_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer, (const unsigned char *)"a", 1); + test_cjv_budget = -1; + WOLFSENTRY_EXIT_ON_FALSE(ret < 0); + + /* The removed node must not remain counted in the dictionary. */ + WOLFSENTRY_EXIT_ON_FALSE(json_value_dict_size(&outer) == 1); + + /* Reclaim the nested dictionary, whose finalization the fault interrupted. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &nested_copy) < 0); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5844,6 +5896,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_compatible_float_to_int(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_dict_remove_orphan(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 67dfd9652022677ddd7e8811a71bb24a02942cc8 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:33:04 -0700 Subject: [PATCH 08/15] F-11603 - https://fenrir.wolfssl.com/finding/11603 - keep dictionary retryable by not freeing a still-linked node on cleanup failure --- src/json/centijson_value.c | 2 -- tests/unittests.c | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index bc01a31..9f7c4a1 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1863,7 +1863,6 @@ json_value_dict_clean( #endif &node->key); if (ret < 0) { - free(node); free((void *)stack); return ret; } @@ -1873,7 +1872,6 @@ json_value_dict_clean( #endif &node->json_value); if (ret < 0) { - free(node); free((void *)stack); return ret; } diff --git a/tests/unittests.c b/tests/unittests.c index 3b293e0..0f83483 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5869,6 +5869,40 @@ static int test_cjvalue_dict_remove_orphan(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct return 0; } +static int test_cjvalue_dict_clean_retry(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE outer; + JSON_VALUE *nested; + JSON_VALUE *slot; + char keybuf[8]; + int i; + int ret; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer) < 0); + nested = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer, (const unsigned char *)"a", 1); + WOLFSENTRY_EXIT_ON_TRUE(nested == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), nested) < 0); + for (i = 0; i < 4; ++i) { + (void)snprintf(keybuf, sizeof keybuf, "k%03d", i); + slot = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), nested, (const unsigned char *)keybuf, strlen(keybuf)); + WOLFSENTRY_EXIT_ON_TRUE(slot == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), slot, (int32_t)i) < 0); + } + + /* First cleanup allocates the outer traversal stack, then fails on the + * nested dictionary's stack allocation. */ + test_cjv_budget = 1; + ret = json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer); + test_cjv_budget = -1; + WOLFSENTRY_EXIT_ON_FALSE(ret < 0); + + /* A retry must succeed: no node may have been freed while still linked. */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5899,6 +5933,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_dict_remove_orphan(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_dict_clean_retry(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 3992d02b94af12c2453999f1e960f2f6406e2359 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:33:47 -0700 Subject: [PATCH 09/15] F-10499 - https://fenrir.wolfssl.com/finding/10499 - skip zero-size stack allocation when cleaning or cloning an empty dictionary --- src/json/centijson_value.c | 21 ++++++++++++++++----- tests/unittests.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index 9f7c4a1..f47621c 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1847,9 +1847,15 @@ json_value_dict_clean( if(d == NULL) return 0; - stack = (RBTREE **)malloc(rbtree_stack_size_needed(d)); - if (stack == NULL) - return JSON_ERR_OUTOFMEMORY; + /* An empty tree needs no stack; a conforming allocator may return NULL for a + * zero-size request, so do not treat that as an error. */ + if(d->root != NULL) { + stack = (RBTREE **)malloc(rbtree_stack_size_needed(d)); + if (stack == NULL) + return JSON_ERR_OUTOFMEMORY; + } else { + stack = NULL; + } stack_size = json_value_dict_leftmost_path(stack, d->root); @@ -2015,8 +2021,13 @@ json_value_clone(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allo } } else { int stack_size; - RBTREE **stack = (RBTREE **)malloc(rbtree_stack_size_needed(src_dict)); /* put this on the heap to avoid runaway growth of stack on deep JSON trees. */ - if (! stack) { + RBTREE **stack = NULL; + /* put the stack on the heap to avoid runaway growth of the C + * stack on deep JSON trees; an empty tree needs none, and a + * zero-size allocation may legitimately return NULL. */ + if (src_dict->root != NULL) + stack = (RBTREE **)malloc(rbtree_stack_size_needed(src_dict)); + if ((src_dict->root != NULL) && (stack == NULL)) { ret = JSON_ERR_OUTOFMEMORY; } else { stack_size = json_value_dict_leftmost_path(stack, src_dict->root); diff --git a/tests/unittests.c b/tests/unittests.c index 0f83483..3d6bd33 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5903,6 +5903,34 @@ static int test_cjvalue_dict_clean_retry(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wo return 0; } +static int test_cjvalue_empty_dict(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE d; + JSON_VALUE clone; + int ret; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &d) < 0); + json_value_init_null(&clone); + + /* Cloning an empty dictionary must not require a zero-size allocation. */ + test_cjv_zero_null = 1; + ret = json_value_clone(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &d, &clone); + test_cjv_zero_null = 0; + WOLFSENTRY_EXIT_ON_FALSE(ret >= 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_type(&clone) == JSON_VALUE_DICT); + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &clone) < 0); + + /* Cleaning up an empty dictionary must not require a zero-size allocation. */ + test_cjv_zero_null = 1; + ret = json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &d); + test_cjv_zero_null = 0; + WOLFSENTRY_EXIT_ON_FALSE(ret >= 0); + + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5936,6 +5964,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_dict_clean_retry(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_empty_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From e35340afb8152ecd8dad4fcb8af0d57c860ec880 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Wed, 21 Oct 2026 15:34:30 -0700 Subject: [PATCH 10/15] F-8754 - https://fenrir.wolfssl.com/finding/8754 - free array buffer directly when shrinking to zero capacity --- src/json/centijson_value.c | 9 +++++++++ tests/unittests.c | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index f47621c..f3c4c6c 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -944,6 +944,15 @@ json_value_array_realloc( { JSON_VALUE* json_value_buf; + /* Shrinking to zero: free the buffer directly rather than via realloc(p, 0), + * which is allowed to free and return NULL and would leave a dangling buffer. */ + if(alloc == 0) { + free(a->json_value_buf); + a->json_value_buf = NULL; + a->alloc = 0; + return 0; + } + json_value_buf = (JSON_VALUE*) realloc(a->json_value_buf, alloc * sizeof(JSON_VALUE)); if(json_value_buf == NULL) return -1; diff --git a/tests/unittests.c b/tests/unittests.c index 3d6bd33..cda9f31 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5931,6 +5931,36 @@ static int test_cjvalue_empty_dict(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsent return 0; } +static int test_cjvalue_array_shrink_to_zero(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE arr; + JSON_VALUE *elem; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_array(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr) < 0); + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, (int32_t)7) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_array_size(&arr) == 1); + + /* Model an allocator for which realloc(ptr, 0) frees and returns NULL. + * Removing the last element shrinks the backing store to zero capacity; the + * array must stay usable (no dangling buffer) and finalize cleanly. */ + test_cjv_zero_null = 1; + WOLFSENTRY_EXIT_ON_TRUE(json_value_array_remove(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr, 0) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_array_size(&arr) == 0); + + elem = json_value_array_append(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr); + WOLFSENTRY_EXIT_ON_TRUE(elem == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), elem, (int32_t)9) < 0); + WOLFSENTRY_EXIT_ON_FALSE(json_value_int32(json_value_array_get(&arr, 0)) == 9); + + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &arr) < 0); + test_cjv_zero_null = 0; + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + static wolfsentry_errcode_t test_cjvalue_fixes(void) { struct wolfsentry_allocator test_allocator; WOLFSENTRY_THREAD_HEADER_CHECKED(WOLFSENTRY_THREAD_FLAG_NONE); @@ -5967,6 +5997,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_empty_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_array_shrink_to_zero(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + WOLFSENTRY_EXIT_ON_FAILURE(WOLFSENTRY_THREAD_TAILER(WOLFSENTRY_THREAD_FLAG_NONE)); WOLFSENTRY_RETURN_OK; From 4c0ab7f7d59b4c3401c3ea6da2dd277e94c5b6fa Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 14:01:28 -0700 Subject: [PATCH 11/15] F-10491 - https://fenrir.wolfssl.com/finding/10491 - Build the 2^53 test constants without C99 long long literals --- tests/unittests.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unittests.c b/tests/unittests.c index cda9f31..719d683 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5769,12 +5769,12 @@ static int test_cjvalue_compatible_lossy(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wo WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); /* 2^53 + 1 is not exactly representable in a double. */ - WOLFSENTRY_EXIT_ON_TRUE(json_value_init_uint64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (uint64_t)9007199254740993ULL) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_uint64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (((uint64_t)1 << 53) + 1)) < 0); WOLFSENTRY_EXIT_ON_TRUE(json_value_is_compatible(&v, JSON_VALUE_DOUBLE)); WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); /* 2^53 + 2 is exactly representable in a double and must stay compatible. */ - WOLFSENTRY_EXIT_ON_TRUE(json_value_init_uint64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (uint64_t)9007199254740994ULL) < 0); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_uint64(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v, (((uint64_t)1 << 53) + 2)) < 0); WOLFSENTRY_EXIT_ON_FALSE(json_value_is_compatible(&v, JSON_VALUE_DOUBLE)); WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &v) < 0); From 7d8e4bad5c3ff8375d8df157d6ee548d3d99629a Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 14:01:34 -0700 Subject: [PATCH 12/15] F-8754 - https://fenrir.wolfssl.com/finding/8754 - Refuse an array growth whose doubled capacity wraps instead of shrinking to zero --- src/json/centijson_value.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index f3c4c6c..baf28b6 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1017,16 +1017,21 @@ json_value_array_insert( JSON_VALUE* v, size_t index) { ARRAY* a = json_value_array_payload(v); + size_t new_alloc; if(a == NULL || index > a->size) return NULL; if(a->size >= a->alloc) { + new_alloc = (a->alloc > 0) ? a->alloc * 2 : 1; + /* a wrapped doubling must not reach the shrink-to-zero path */ + if(new_alloc <= a->size) + return NULL; if(json_value_array_realloc( #ifdef WOLFSENTRY WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), #endif - a, (a->alloc > 0) ? a->alloc * 2 : 1) != 0) + a, new_alloc) != 0) return NULL; } From 6803446088c2197bb9222e0d7458e95cf4e3391e Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 14:01:39 -0700 Subject: [PATCH 13/15] F-10499 - https://fenrir.wolfssl.com/finding/10499 - Skip the dictionary stack walk when no stack was allocated for an empty tree --- src/json/centijson_value.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index baf28b6..a155b31 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1871,7 +1871,7 @@ json_value_dict_clean( stack = NULL; } - stack_size = json_value_dict_leftmost_path(stack, d->root); + stack_size = (stack != NULL) ? json_value_dict_leftmost_path(stack, d->root) : 0; while(stack_size > 0) { node = stack[--stack_size]; @@ -2044,7 +2044,7 @@ json_value_clone(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allo if ((src_dict->root != NULL) && (stack == NULL)) { ret = JSON_ERR_OUTOFMEMORY; } else { - stack_size = json_value_dict_leftmost_path(stack, src_dict->root); + stack_size = (stack != NULL) ? json_value_dict_leftmost_path(stack, src_dict->root) : 0; while(stack_size > 0) { src_dict_node = stack[--stack_size]; From c9c7109af7531b1faf1829661646ee834e17daa8 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 14:01:40 -0700 Subject: [PATCH 14/15] Move the documented analyzer false positives to their new centijson_value lines --- tests/filter-clang-analyzer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/filter-clang-analyzer.py b/tests/filter-clang-analyzer.py index f4e199f..72bdbea 100644 --- a/tests/filter-clang-analyzer.py +++ b/tests/filter-clang-analyzer.py @@ -8,9 +8,9 @@ ALLOWED_RESULTS = { - ("core.uninitialized.Assign", "src/json/centijson_value.c", 1285), - ("core.NullDereference", "src/json/centijson_value.c", 1483), - ("core.NullDereference", "src/json/centijson_value.c", 1520), + ("core.uninitialized.Assign", "src/json/centijson_value.c", 1366), + ("core.NullDereference", "src/json/centijson_value.c", 1564), + ("core.NullDereference", "src/json/centijson_value.c", 1601), } From 5530e19f3cca44fae77dea647233bdbbf493ee1f Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Fri, 23 Oct 2026 18:23:03 -0700 Subject: [PATCH 15/15] F-11603 - https://fenrir.wolfssl.com/finding/11603 - Unlink each dictionary node before freeing it so a failed clean can be retried --- src/json/centijson_value.c | 16 +++++++++++++++ tests/unittests.c | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/json/centijson_value.c b/src/json/centijson_value.c index a155b31..27d9e61 100644 --- a/src/json/centijson_value.c +++ b/src/json/centijson_value.c @@ -1895,6 +1895,22 @@ json_value_dict_clean( free((void *)stack); return ret; } + /* unlink before freeing so a failed clean leaves only live nodes for a + * retry; size is kept, as it still bounds the retry's stack */ + if(stack_size > 0) + stack[stack_size - 1]->left = right; + else + d->root = right; + if(v->data.data_bytes[0] & HAS_ORDERLIST) { + if(node->order_prev != NULL) + node->order_prev->order_next = node->order_next; + else + d->order_head = node->order_next; + if(node->order_next != NULL) + node->order_next->order_prev = node->order_prev; + else + d->order_tail = node->order_prev; + } free(node); stack_size += json_value_dict_leftmost_path(stack + stack_size, right); diff --git a/tests/unittests.c b/tests/unittests.c index 719d683..08cd566 100644 --- a/tests/unittests.c +++ b/tests/unittests.c @@ -5869,6 +5869,43 @@ static int test_cjvalue_dict_remove_orphan(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct return 0; } +static int test_cjvalue_dict_clean_retry_later_node(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) +{ + JSON_VALUE outer; + JSON_VALUE *nested; + JSON_VALUE *slot; + char keybuf[8]; + int i; + int ret; + + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer) < 0); + for (i = 0; i < 3; ++i) { + (void)snprintf(keybuf, sizeof keybuf, "k%03d", i); + slot = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer, (const unsigned char *)keybuf, strlen(keybuf)); + WOLFSENTRY_EXIT_ON_TRUE(slot == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), slot, (int32_t)i) < 0); + } + nested = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer, (const unsigned char *)"zz", 2); + WOLFSENTRY_EXIT_ON_TRUE(nested == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), nested) < 0); + slot = json_value_dict_add_(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), nested, (const unsigned char *)"in", 2); + WOLFSENTRY_EXIT_ON_TRUE(slot == NULL); + WOLFSENTRY_EXIT_ON_TRUE(json_value_init_int32(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), slot, 7) < 0); + + /* the earlier keys are freed before the nested dictionary's stack + * allocation fails on the last key. */ + test_cjv_budget = 1; + ret = json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer); + test_cjv_budget = -1; + WOLFSENTRY_EXIT_ON_FALSE(ret < 0); + + /* a retry must not reach the nodes freed before the failure */ + WOLFSENTRY_EXIT_ON_TRUE(json_value_fini(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(allocator), &outer) < 0); + WOLFSENTRY_EXIT_ON_FALSE(test_cjv_live == 0); + + return 0; +} + static int test_cjvalue_dict_clean_retry(WOLFSENTRY_CONTEXT_ARGS_IN_EX(struct wolfsentry_allocator *allocator)) { JSON_VALUE outer; @@ -5994,6 +6031,9 @@ static wolfsentry_errcode_t test_cjvalue_fixes(void) { test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_dict_clean_retry(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); + WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_dict_clean_retry_later_node(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0); + test_cjv_reset(); WOLFSENTRY_EXIT_ON_TRUE(test_cjvalue_empty_dict(WOLFSENTRY_CONTEXT_ARGS_OUT_EX(&test_allocator)) != 0);