From f466f84adc4416a860fb7dce192671ceae20cc82 Mon Sep 17 00:00:00 2001 From: RM LargestBoy Date: Tue, 5 May 2026 23:14:16 -0400 Subject: [PATCH] Add OSS-Fuzz harnesses for additional cJSON APIs Add OSS-Fuzz harnesses for additional cJSON APIs Co-authored-by: Dan Gui --- fuzzing/README.md | 123 +++ fuzzing/cjson_add_to_object_fuzzer.c | 740 ++++++++++++++ fuzzing/cjson_compare_fuzzer.c | 911 ++++++++++++++++++ fuzzing/cjson_create_fuzzer.c | 909 +++++++++++++++++ .../cjson_delete_item_from_object_fuzzer.c | 540 +++++++++++ fuzzing/cjson_duplicate_fuzzer.c | 782 +++++++++++++++ fuzzing/cjson_print_preallocated_fuzzer.c | 606 ++++++++++++ fuzzing/cjson_replace_item_in_object_fuzzer.c | 309 ++++++ fuzzing/ossfuzz.sh | 33 +- 9 files changed, 4946 insertions(+), 7 deletions(-) create mode 100644 fuzzing/README.md create mode 100644 fuzzing/cjson_add_to_object_fuzzer.c create mode 100644 fuzzing/cjson_compare_fuzzer.c create mode 100644 fuzzing/cjson_create_fuzzer.c create mode 100644 fuzzing/cjson_delete_item_from_object_fuzzer.c create mode 100644 fuzzing/cjson_duplicate_fuzzer.c create mode 100644 fuzzing/cjson_print_preallocated_fuzzer.c create mode 100644 fuzzing/cjson_replace_item_in_object_fuzzer.c diff --git a/fuzzing/README.md b/fuzzing/README.md new file mode 100644 index 00000000..c8501995 --- /dev/null +++ b/fuzzing/README.md @@ -0,0 +1,123 @@ +# Additional cJSON OSS-Fuzz Harnesses + +This directory contains additional fuzz targets for cJSON. + +The goal is to test more public cJSON APIs, not only JSON parsing. These +harnesses build small cJSON objects, run API operations on them, and check simple +expected behavior. + +The harnesses are meant to help OSS-Fuzz find: + +- memory safety bugs, +- undefined behavior, +- incorrect API behavior, +- ownership mistakes, +- buffer boundary errors. + +They do not claim to fully verify cJSON. + +## Added Fuzz Targets + +| Fuzzer | What it tests | +| --- | --- | +| `cjson_create_fuzzer.c` | cJSON create APIs, array helpers, string/raw creation, and reference creation | +| `cjson_add_to_object_fuzzer.c` | Adding numbers, strings, arrays, objects, raw values, and references to objects | +| `cjson_delete_item_from_object_fuzzer.c` | Deleting and detaching items from objects and arrays | +| `cjson_replace_item_in_object_fuzzer.c` | Replacing object items with case-sensitive and case-insensitive lookup | +| `cjson_duplicate_fuzzer.c` | Recursive and non-recursive duplication | +| `cjson_compare_fuzzer.c` | Strict and loose comparison behavior | +| `cjson_print_preallocated_fuzzer.c` | Printing into caller-provided buffers | + +## What the Harnesses Check + +The harnesses use simple API-level checks. Examples include: + +- successful add operations should make the key reachable, +- failed add operations should not change the object size, +- deleting an existing item should remove it, +- deleting a missing item should leave the object unchanged, +- replacing an existing key should succeed without changing object size, +- replacing a missing key should fail, +- duplicated trees should not share mutable nodes with the original, +- comparison should be reflexive and symmetric, +- preallocated printing should match normal cJSON printing, +- preallocated printing should not write past the provided buffer, +- invalid inputs such as `NULL` pointers should fail cleanly where expected. + +The harnesses also use sanitizers through OSS-Fuzz, so memory errors and +undefined behavior can still be reported normally. + +## Design Notes + +Most targets use small seeded cJSON trees. This makes it easier for the fuzzer +to reach useful API states, such as existing keys, nested objects, arrays, and +replacement targets. + +The fuzz input controls operation choices, keys, values, case sensitivity, +array indices, and buffer sizes. All loops, string sizes, array sizes, and +generated subtrees are bounded so that the fuzzers remain suitable for +continuous fuzzing. + +Some harnesses also parse part of the fuzz input as JSON after running their +structured tests. This keeps arbitrary parser-generated trees in scope while +still adding stronger API-specific checks. + +## Round-Trip Checks + +Some cJSON values are valid but are not stable under print/parse comparison. +Because of that, the harnesses avoid strict round-trip equality checks for: + +- raw nodes, +- `NaN` and infinity, +- ambiguous duplicate object keys. + +Those cases are still tested, but the harnesses use narrower checks such as +printability, exact output comparison, object size changes, or key reachability. + +## Ownership + +The harnesses follow cJSON ownership rules carefully. + +In general: + +- if an item is successfully added to a tree, the tree owns it, +- if an add or replace operation fails, the harness deletes the unused item, +- if an item is detached, the harness owns it and deletes it once, +- reference APIs are tested separately because they intentionally alias caller + storage or child pointers. + +This avoids false positives caused by bugs in the fuzz harness itself. + +## Project Results + +In our project evaluation, these harnesses increased runtime coverage of +`cJSON.c`: + +| Metric | Before | After | +| --- | ---: | ---: | +| Line coverage | 42.93% | 80.32% | +| Function coverage | 27.43% | 92.04% | +| Region coverage | 44.89% | 84.61% | + +Fuzz Introspector also reported: + +| Metric | Result | +| --- | ---: | +| Statically reachable functions | 236 / 245 | +| Statically reachable cyclomatic complexity | 1337 / 1366 | +| Runtime-covered functions | 218 / 245 | + +No confirmed ASan or UBSan crash was found during our project runs. + +These numbers are included as project evaluation results. They may differ from +future OSS-Fuzz production runs depending on corpus state, build settings, +fuzzing time, and sanitizer configuration. + +## Running Locally + +From an OSS-Fuzz checkout: + +```sh +python3 infra/helper.py build_image cjson +python3 infra/helper.py build_fuzzers --sanitizer address cjson +python3 infra/helper.py check_build cjson \ No newline at end of file diff --git a/fuzzing/cjson_add_to_object_fuzzer.c b/fuzzing/cjson_add_to_object_fuzzer.c new file mode 100644 index 00000000..f3a0f136 --- /dev/null +++ b/fuzzing/cjson_add_to_object_fuzzer.c @@ -0,0 +1,740 @@ +#include +#include +#include +#include + +#include "../cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static int read_u8(const uint8_t **data, size_t *size, uint8_t *out) { + if (*size < 1) { + return 0; + } + *out = **data; + (*data)++; + (*size)--; + return 1; +} + +static int read_bytes(const uint8_t **data, size_t *size, uint8_t *out, size_t n) { + if (*size < n) { + return 0; + } + memcpy(out, *data, n); + (*data) += n; + (*size) -= n; + return 1; +} + +static char *dup_cstr(const char *s) { + size_t n = strlen(s); + char *copy = (char *)malloc(n + 1); + if (copy == NULL) { + return NULL; + } + memcpy(copy, s, n + 1); + return copy; +} + +static char *read_bounded_string(const uint8_t **data, size_t *size, size_t max_len) { + uint8_t raw_len = 0; + size_t len = 0; + char *out = NULL; + + if (!read_u8(data, size, &raw_len)) { + return NULL; + } + + len = (size_t)(raw_len % (max_len + 1)); + if (*size < len) { + len = *size; + } + + out = (char *)malloc(len + 1); + if (out == NULL) { + return NULL; + } + + if (len > 0) { + memcpy(out, *data, len); + (*data) += len; + (*size) -= len; + } + out[len] = '\0'; + + for (size_t i = 0; i < len; ++i) { + if (out[i] == '\0') { + out[i] = 'A'; + } + } + + return out; +} + +static int read_double(const uint8_t **data, size_t *size, double *out) { + union { + uint64_t u64; + double d; + } conv; + + conv.u64 = 0; + + if (*size >= sizeof(uint64_t)) { + if (!read_bytes(data, size, (uint8_t *)&conv.u64, sizeof(uint64_t))) { + return 0; + } + } else { + size_t n = *size; + if (n > 0) { + memcpy(&conv.u64, *data, n); + (*data) += n; + (*size) -= n; + } + } + + *out = conv.d; + return 1; +} + +static char *choose_known_key(uint8_t selector) { + static const char *keys[] = { + "alpha", + "Bravo", + "CHARLIE", + "delta", + "Echo", + "foxtrot", + "nestedObj", + "childObj", + "items" + }; + + return dup_cstr(keys[selector % (sizeof(keys) / sizeof(keys[0]))]); +} + +static char *choose_add_key(const uint8_t **data, size_t *size) { + uint8_t mode = 0; + uint8_t sel = 0; + + if (!read_u8(data, size, &mode)) { + return NULL; + } + + if ((mode & 3) == 0) { + if (!read_u8(data, size, &sel)) { + return NULL; + } + return choose_known_key(sel); + } + + return read_bounded_string(data, size, 24); +} + +static void seed_object(cJSON *root, cJSON **containers, size_t *container_count) { + cJSON *nested = NULL; + cJSON *child = NULL; + cJSON *arr = NULL; + + if (root == NULL) { + return; + } + + cJSON_AddNumberToObject(root, "alpha", 1.0); + cJSON_AddStringToObject(root, "Bravo", "seed"); + cJSON_AddBoolToObject(root, "CHARLIE", 1); + cJSON_AddNullToObject(root, "delta"); + + nested = cJSON_AddObjectToObject(root, "nestedObj"); + if (nested != NULL) { + cJSON_AddNumberToObject(nested, "inner", 7.0); + } + + child = cJSON_AddObjectToObject(root, "childObj"); + if (child != NULL) { + cJSON_AddStringToObject(child, "name", "x"); + } + + arr = cJSON_AddArrayToObject(root, "items"); + if (arr != NULL) { + cJSON_AddItemToArray(arr, cJSON_CreateNumber(1.0)); + cJSON_AddItemToArray(arr, cJSON_CreateString("a")); + } + + if (containers != NULL && container_count != NULL) { + *container_count = 0; + containers[(*container_count)++] = root; + if (nested != NULL && *container_count < 8) { + containers[(*container_count)++] = nested; + } + if (child != NULL && *container_count < 8) { + containers[(*container_count)++] = child; + } + } +} + +static void clear_root_object(cJSON *root) { + while (root != NULL && root->child != NULL) { + cJSON *detached = cJSON_DetachItemViaPointer(root, root->child); + if (detached == NULL) { + break; + } + cJSON_Delete(detached); + } +} + +static void rebuild_object(cJSON *root, cJSON **containers, size_t *container_count) { + if (root == NULL) { + return; + } + + clear_root_object(root); + seed_object(root, containers, container_count); +} + +static void maybe_rebuild(const uint8_t **data, size_t *size, cJSON *root, + cJSON **containers, size_t *container_count) { + uint8_t control = 0; + if (!read_u8(data, size, &control)) { + return; + } + + if ((control & 7) == 0) { + rebuild_object(root, containers, container_count); + } +} + +static cJSON *create_leaf_item(const uint8_t **data, size_t *size, uint8_t kind) { + char *payload = NULL; + double number = 0.0; + uint8_t b = 0; + + switch (kind % 6) { + case 0: + (void)read_double(data, size, &number); + return cJSON_CreateNumber(number); + + case 1: + payload = read_bounded_string(data, size, 48); + if (payload == NULL) { + return cJSON_CreateString(""); + } + { + cJSON *item = cJSON_CreateString(payload); + free(payload); + return item; + } + + case 2: + payload = read_bounded_string(data, size, 48); + if (payload == NULL) { + return cJSON_CreateRaw("null"); + } + { + cJSON *item = cJSON_CreateRaw(payload); + free(payload); + return item; + } + + case 3: + (void)read_u8(data, size, &b); + return cJSON_CreateBool((b & 1) ? 1 : 0); + + case 4: + return cJSON_CreateTrue(); + + case 5: + default: + return cJSON_CreateNull(); + } +} + +static cJSON *create_add_item(const uint8_t **data, size_t *size, int depth) { + uint8_t kind = 0; + + if (!read_u8(data, size, &kind)) { + return cJSON_CreateNull(); + } + + if (depth <= 0) { + return create_leaf_item(data, size, kind); + } + + switch (kind % 8) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + return create_leaf_item(data, size, kind); + + case 6: { + cJSON *obj = cJSON_CreateObject(); + uint8_t count = 0; + if (obj == NULL) { + return NULL; + } + + (void)read_u8(data, size, &count); + for (size_t i = 0; i < (size_t)(count % 3); ++i) { + char *k = read_bounded_string(data, size, 16); + cJSON *v = NULL; + + if (k == NULL) { + break; + } + + v = create_add_item(data, size, depth - 1); + if (v == NULL || !cJSON_AddItemToObject(obj, k, v)) { + cJSON_Delete(v); + free(k); + cJSON_Delete(obj); + return NULL; + } + free(k); + } + return obj; + } + + case 7: + default: { + cJSON *arr = cJSON_CreateArray(); + uint8_t count = 0; + if (arr == NULL) { + return NULL; + } + + (void)read_u8(data, size, &count); + for (size_t i = 0; i < (size_t)(count % 4); ++i) { + cJSON *elem = create_add_item(data, size, depth - 1); + if (elem == NULL || !cJSON_AddItemToArray(arr, elem)) { + cJSON_Delete(elem); + cJSON_Delete(arr); + return NULL; + } + } + return arr; + } + } +} + +static void assert_add_postconditions(cJSON *target, const char *key, int valid_inputs, + int add_ok, int size_before, int size_after) { + if (!valid_inputs) { + if (add_ok || size_after != size_before) { + abort(); + } + return; + } + + if (!add_ok || size_after != (size_before + 1)) { + abort(); + } + + if (cJSON_GetObjectItemCaseSensitive(target, key) == NULL) { + abort(); + } +} + +static void op_add_number(const uint8_t **data, size_t *size, cJSON *target, const char *key, + int valid_inputs) { + double number = 0.0; + cJSON *added = NULL; + int size_before = 0; + int size_after = 0; + + (void)read_double(data, size, &number); + size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + added = cJSON_AddNumberToObject(target, key, number); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + + assert_add_postconditions(target, key, valid_inputs, added != NULL, size_before, size_after); +} + +static void op_add_string(const uint8_t **data, size_t *size, cJSON *target, const char *key, + int valid_inputs) { + char *payload = read_bounded_string(data, size, 48); + cJSON *added = NULL; + int size_before = 0; + int size_after = 0; + + if (payload == NULL) { + return; + } + + size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + added = cJSON_AddStringToObject(target, key, payload); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + + assert_add_postconditions(target, key, valid_inputs, added != NULL, size_before, size_after); + free(payload); +} + +static void op_add_raw(const uint8_t **data, size_t *size, cJSON *target, const char *key, + int valid_inputs) { + char *payload = read_bounded_string(data, size, 48); + cJSON *added = NULL; + int size_before = 0; + int size_after = 0; + + if (payload == NULL) { + return; + } + + size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + added = cJSON_AddRawToObject(target, key, payload); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + + assert_add_postconditions(target, key, valid_inputs, added != NULL, size_before, size_after); + free(payload); +} + +static void op_add_true(cJSON *target, const char *key, int valid_inputs) { + cJSON *added = NULL; + int size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + int size_after = 0; + + added = cJSON_AddTrueToObject(target, key); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + assert_add_postconditions(target, key, valid_inputs, added != NULL, size_before, size_after); +} + +static void op_add_false(cJSON *target, const char *key, int valid_inputs) { + cJSON *added = NULL; + int size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + int size_after = 0; + + added = cJSON_AddFalseToObject(target, key); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + assert_add_postconditions(target, key, valid_inputs, added != NULL, size_before, size_after); +} + +static void op_add_null(cJSON *target, const char *key, int valid_inputs) { + cJSON *added = NULL; + int size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + int size_after = 0; + + added = cJSON_AddNullToObject(target, key); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + assert_add_postconditions(target, key, valid_inputs, added != NULL, size_before, size_after); +} + +static void op_add_bool(const uint8_t **data, size_t *size, cJSON *target, const char *key, + int valid_inputs) { + uint8_t b = 0; + cJSON *added = NULL; + int size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + int size_after = 0; + + (void)read_u8(data, size, &b); + added = cJSON_AddBoolToObject(target, key, (b & 1) ? 1 : 0); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + assert_add_postconditions(target, key, valid_inputs, added != NULL, size_before, size_after); +} + +static void op_add_object(cJSON *target, const char *key, int valid_inputs, + cJSON **containers, size_t *container_count) { + cJSON *obj = NULL; + int size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + int size_after = 0; + + obj = cJSON_AddObjectToObject(target, key); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + assert_add_postconditions(target, key, valid_inputs, obj != NULL, size_before, size_after); + + if (obj != NULL && containers != NULL && container_count != NULL && *container_count < 8) { + containers[(*container_count)++] = obj; + } +} + +static void op_add_array(cJSON *target, const char *key, int valid_inputs) { + cJSON *arr = NULL; + int size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + int size_after = 0; + + arr = cJSON_AddArrayToObject(target, key); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + assert_add_postconditions(target, key, valid_inputs, arr != NULL, size_before, size_after); + + if (arr != NULL) { + cJSON_AddItemToArray(arr, cJSON_CreateNumber(8.0)); + cJSON_AddItemToArray(arr, cJSON_CreateTrue()); + } +} + +static void op_add_reference(cJSON *target, const char *key, int valid_inputs, + cJSON *shared_value) { + int add_ok = 0; + int size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + int size_after = 0; + + add_ok = cJSON_AddItemReferenceToObject(target, key, shared_value); + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + assert_add_postconditions(target, key, valid_inputs, add_ok, size_before, size_after); +} + +static void op_add_item_raw(const uint8_t **data, size_t *size, cJSON *target, const char *key, + int valid_inputs) { + cJSON *item = NULL; + int add_ok = 0; + int size_before = 0; + int size_after = 0; + + item = create_add_item(data, size, 1); + if (item == NULL) { + return; + } + + size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + add_ok = cJSON_AddItemToObject(target, key, item); + if (!add_ok) { + cJSON_Delete(item); + } + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + + assert_add_postconditions(target, key, valid_inputs, add_ok, size_before, size_after); +} + +static void op_add_item_cs(cJSON *target, const char *key, int valid_inputs) { + cJSON *item = cJSON_CreateNumber(7.0); + int add_ok = 0; + int size_before = 0; + int size_after = 0; + + if (item == NULL) { + return; + } + + size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + add_ok = cJSON_AddItemToObjectCS(target, key, item); + if (!add_ok) { + cJSON_Delete(item); + } + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + + assert_add_postconditions(target, key, valid_inputs, add_ok, size_before, size_after); +} + +static void op_add_detached(cJSON *root, cJSON *target, const char *key, int valid_inputs) { + cJSON *tmp = cJSON_CreateNumber(1.0); + cJSON *detached = NULL; + int add_ok = 0; + int size_before = 0; + int size_after = 0; + + if (tmp == NULL) { + return; + } + + if (!cJSON_AddItemToObject(root, "oldkey", tmp)) { + cJSON_Delete(tmp); + return; + } + + detached = cJSON_DetachItemFromObject(root, "oldkey"); + if (detached == NULL) { + abort(); + } + if (cJSON_GetObjectItemCaseSensitive(root, "oldkey") != NULL) { + cJSON_Delete(detached); + abort(); + } + + size_before = (target != NULL) ? cJSON_GetArraySize(target) : 0; + add_ok = cJSON_AddItemToObject(target, key, detached); + if (!add_ok) { + cJSON_Delete(detached); + } + size_after = (target != NULL) ? cJSON_GetArraySize(target) : 0; + + assert_add_postconditions(target, key, valid_inputs, add_ok, size_before, size_after); +} + +static void op_invalid_reference_array_call(const uint8_t **data, size_t *size, cJSON *shared_array_item) { + uint8_t control = 0; + cJSON *parent = NULL; + int size_before = 0; + int size_after = 0; + int add_ok = 0; + + if (!read_u8(data, size, &control)) { + return; + } + + if ((control & 1) != 0) { + parent = cJSON_CreateArray(); + } + + size_before = (parent != NULL) ? cJSON_GetArraySize(parent) : 0; + add_ok = cJSON_AddItemReferenceToArray(parent, shared_array_item); + size_after = (parent != NULL) ? cJSON_GetArraySize(parent) : 0; + + if (parent == NULL) { + if (add_ok) { + abort(); + } + } else { + if (!add_ok || size_after != (size_before + 1)) { + cJSON_Delete(parent); + abort(); + } + } + + cJSON_Delete(parent); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + cJSON *root = NULL; + cJSON *shared_value = NULL; + cJSON *shared_array_item = NULL; + cJSON *containers[8] = {0}; + size_t container_count = 0; + uint8_t raw_ops = 0; + size_t op_count = 0; + + if (data == NULL || size == 0) { + return 0; + } + + root = cJSON_CreateObject(); + if (root == NULL) { + return 0; + } + + seed_object(root, containers, &container_count); + + shared_value = cJSON_CreateString("shared"); + shared_array_item = cJSON_CreateNumber(1337.0); + if (shared_value == NULL || shared_array_item == NULL) { + cJSON_Delete(shared_value); + cJSON_Delete(shared_array_item); + cJSON_Delete(root); + return 0; + } + + if (!read_u8(&data, &size, &raw_ops)) { + cJSON_Delete(shared_value); + cJSON_Delete(shared_array_item); + cJSON_Delete(root); + return 0; + } + op_count = (size_t)(raw_ops % 32); + + for (size_t i = 0; i < op_count && size > 0; ++i) { + uint8_t opcode = 0; + uint8_t target_sel = 0; + uint8_t fail_mode = 0; + char *key_storage = NULL; + const char *key = NULL; + cJSON *target = NULL; + int valid_inputs = 0; + + if (!read_u8(&data, &size, &opcode)) { + break; + } + if (!read_u8(&data, &size, &target_sel)) { + break; + } + if (!read_u8(&data, &size, &fail_mode)) { + break; + } + + if (container_count > 0) { + target = containers[target_sel % container_count]; + } + + key_storage = choose_add_key(&data, &size); + key = key_storage; + + if ((fail_mode & 0x3) == 0) { + key = NULL; + } + if (((fail_mode >> 2) & 0x3) == 0) { + target = NULL; + } + + valid_inputs = (target != NULL && key != NULL); + + switch (opcode % 12) { + case 0: + op_add_number(&data, &size, target, key, valid_inputs); + break; + + case 1: + op_add_string(&data, &size, target, key, valid_inputs); + break; + + case 2: + op_add_raw(&data, &size, target, key, valid_inputs); + break; + + case 3: + op_add_true(target, key, valid_inputs); + break; + + case 4: + op_add_false(target, key, valid_inputs); + break; + + case 5: + op_add_null(target, key, valid_inputs); + break; + + case 6: + op_add_bool(&data, &size, target, key, valid_inputs); + break; + + case 7: + op_add_object(target, key, valid_inputs, containers, &container_count); + break; + + case 8: + op_add_array(target, key, valid_inputs); + break; + + case 9: + op_add_reference(target, key, valid_inputs, shared_value); + break; + + case 10: + op_add_item_raw(&data, &size, target, key, valid_inputs); + break; + + case 11: + default: + if ((opcode & 1) == 0) { + op_add_item_cs(target, key, valid_inputs); + } else { + op_add_detached(root, target, key, valid_inputs); + } + break; + } + + op_invalid_reference_array_call(&data, &size, shared_array_item); + maybe_rebuild(&data, &size, root, containers, &container_count); + free(key_storage); + } + + { + char *printed = cJSON_PrintUnformatted(root); + if (printed != NULL) { + cJSON *parsed = cJSON_Parse(printed); + if (parsed != NULL) { + cJSON_Delete(parsed); + } + free(printed); + } + } + + cJSON_Delete(shared_value); + cJSON_Delete(shared_array_item); + cJSON_Delete(root); + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/fuzzing/cjson_compare_fuzzer.c b/fuzzing/cjson_compare_fuzzer.c new file mode 100644 index 00000000..e253ccc3 --- /dev/null +++ b/fuzzing/cjson_compare_fuzzer.c @@ -0,0 +1,911 @@ +#include +#include +#include +#include +#include +#include + +#include "../cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + cJSON *left; + cJSON *right; +} compare_pair; + +static void fuzz_assert(int condition) { + if (!condition) { + abort(); + } +} + +static int read_u8(const uint8_t **data, size_t *size, uint8_t *out) { + if (*size < 1) { + return 0; + } + + *out = **data; + (*data)++; + (*size)--; + return 1; +} + +static int read_small_int(const uint8_t **data, size_t *size, int *out) { + uint8_t raw = 0; + + if (!read_u8(data, size, &raw)) { + return 0; + } + + *out = (int)(raw % 201) - 100; + return 1; +} + +static char *read_bounded_string(const uint8_t **data, size_t *size, size_t max_len) { + uint8_t raw_len = 0; + size_t len = 0; + char *out = NULL; + + if (!read_u8(data, size, &raw_len)) { + return NULL; + } + + len = (size_t)(raw_len % (max_len + 1)); + if (*size < len) { + len = *size; + } + + out = (char *)malloc(len + 1); + if (out == NULL) { + return NULL; + } + + if (len > 0) { + memcpy(out, *data, len); + (*data) += len; + (*size) -= len; + } + + out[len] = '\0'; + for (size_t i = 0; i < len; ++i) { + if (out[i] == '\0') { + out[i] = 'A'; + } + } + + return out; +} + +static size_t find_split(const uint8_t *data, size_t size) { + for (size_t i = 1; i + 1 < size; ++i) { + if (data[i] == '|' || data[i] == '\n') { + return i; + } + } + + return size / 2; +} + +static compare_pair make_empty_pair(void) { + compare_pair pair; + pair.left = NULL; + pair.right = NULL; + return pair; +} + +static void delete_pair(compare_pair *pair) { + if (pair == NULL) { + return; + } + + cJSON_Delete(pair->left); + cJSON_Delete(pair->right); + pair->left = NULL; + pair->right = NULL; +} + +static const char *select_raw_literal(uint8_t selector) { + static const char *literals[] = { + "null", + "true", + "false", + "0", + "42", + "\"raw\"", + "[]", + "{}" + }; + + return literals[selector % (sizeof(literals) / sizeof(literals[0]))]; +} + +static cJSON *create_seed_tree(void) { + cJSON *root = NULL; + cJSON *nested = NULL; + cJSON *meta = NULL; + cJSON *items = NULL; + + root = cJSON_CreateObject(); + if (root == NULL) { + return NULL; + } + + if (cJSON_AddNumberToObject(root, "number", 1.0) == NULL) { + goto fail; + } + if (cJSON_AddStringToObject(root, "text", "seed") == NULL) { + goto fail; + } + if (cJSON_AddBoolToObject(root, "flag", 1) == NULL) { + goto fail; + } + + nested = cJSON_AddObjectToObject(root, "nested"); + if (nested == NULL) { + goto fail; + } + if (cJSON_AddNumberToObject(nested, "value", 7.0) == NULL) { + goto fail; + } + if (cJSON_AddStringToObject(nested, "label", "alpha") == NULL) { + goto fail; + } + + meta = cJSON_AddObjectToObject(root, "meta"); + if (meta == NULL) { + goto fail; + } + if (cJSON_AddStringToObject(meta, "kind", "seed") == NULL) { + goto fail; + } + if (cJSON_AddFalseToObject(meta, "ready") == NULL) { + goto fail; + } + + items = cJSON_AddArrayToObject(root, "items"); + if (items == NULL) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateNumber(1.0))) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateString("x"))) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateFalse())) { + goto fail; + } + + return root; + +fail: + cJSON_Delete(root); + return NULL; +} + +static compare_pair create_seed_pair(void) { + compare_pair pair = make_empty_pair(); + + pair.left = create_seed_tree(); + if (pair.left == NULL) { + return pair; + } + + pair.right = cJSON_Duplicate(pair.left, 1); + if (pair.right == NULL) { + delete_pair(&pair); + } + + return pair; +} + +static compare_pair create_case_variant_pair(void) { + compare_pair pair = make_empty_pair(); + + pair.left = cJSON_CreateObject(); + pair.right = cJSON_CreateObject(); + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return make_empty_pair(); + } + + if ((cJSON_AddFalseToObject(pair.left, "False") == NULL) || + (cJSON_AddTrueToObject(pair.left, "true") == NULL) || + (cJSON_AddNumberToObject(pair.left, "number", 42.0) == NULL) || + (cJSON_AddFalseToObject(pair.right, "false") == NULL) || + (cJSON_AddTrueToObject(pair.right, "true") == NULL) || + (cJSON_AddNumberToObject(pair.right, "number", 42.0) == NULL)) { + delete_pair(&pair); + return make_empty_pair(); + } + + return pair; +} + +static compare_pair create_object_order_pair(void) { + compare_pair pair = make_empty_pair(); + cJSON *left_nested = NULL; + cJSON *right_nested = NULL; + + pair.left = cJSON_CreateObject(); + pair.right = cJSON_CreateObject(); + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return make_empty_pair(); + } + + if ((cJSON_AddNumberToObject(pair.left, "a", 1.0) == NULL) || + (cJSON_AddStringToObject(pair.left, "b", "x") == NULL)) { + delete_pair(&pair); + return make_empty_pair(); + } + left_nested = cJSON_AddObjectToObject(pair.left, "nested"); + if ((left_nested == NULL) || (cJSON_AddTrueToObject(left_nested, "ok") == NULL)) { + delete_pair(&pair); + return make_empty_pair(); + } + + right_nested = cJSON_AddObjectToObject(pair.right, "nested"); + if ((right_nested == NULL) || + (cJSON_AddTrueToObject(right_nested, "ok") == NULL) || + (cJSON_AddStringToObject(pair.right, "b", "x") == NULL) || + (cJSON_AddNumberToObject(pair.right, "a", 1.0) == NULL)) { + delete_pair(&pair); + return make_empty_pair(); + } + + return pair; +} + +static compare_pair create_array_order_pair(void) { + compare_pair pair = make_empty_pair(); + + pair.left = cJSON_CreateArray(); + pair.right = cJSON_CreateArray(); + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return make_empty_pair(); + } + + if (!cJSON_AddItemToArray(pair.left, cJSON_CreateNumber(1.0)) || + !cJSON_AddItemToArray(pair.left, cJSON_CreateNumber(2.0)) || + !cJSON_AddItemToArray(pair.left, cJSON_CreateString("x")) || + !cJSON_AddItemToArray(pair.right, cJSON_CreateNumber(2.0)) || + !cJSON_AddItemToArray(pair.right, cJSON_CreateNumber(1.0)) || + !cJSON_AddItemToArray(pair.right, cJSON_CreateString("x"))) { + delete_pair(&pair); + return make_empty_pair(); + } + + return pair; +} + +static compare_pair create_raw_pair(uint8_t left_selector, uint8_t right_selector) { + compare_pair pair = make_empty_pair(); + + pair.left = cJSON_CreateRaw(select_raw_literal(left_selector)); + pair.right = cJSON_CreateRaw(select_raw_literal(right_selector)); + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + } + + return pair; +} + +static cJSON *get_nested_value(cJSON *root) { + cJSON *nested = NULL; + + if (root == NULL) { + return NULL; + } + + nested = cJSON_GetObjectItemCaseSensitive(root, "nested"); + if (nested == NULL) { + return NULL; + } + + return cJSON_GetObjectItemCaseSensitive(nested, "value"); +} + +static cJSON *get_nested_label(cJSON *root) { + cJSON *nested = NULL; + + if (root == NULL) { + return NULL; + } + + nested = cJSON_GetObjectItemCaseSensitive(root, "nested"); + if (nested == NULL) { + return NULL; + } + + return cJSON_GetObjectItemCaseSensitive(nested, "label"); +} + +static void set_nested_number(cJSON *root, int value) { + cJSON *number = get_nested_value(root); + + fuzz_assert(number != NULL); + fuzz_assert(cJSON_IsNumber(number)); + cJSON_SetNumberValue(number, value); +} + +static void set_nested_label(cJSON *root, const char *value) { + cJSON *label = get_nested_label(root); + + fuzz_assert(label != NULL); + fuzz_assert(cJSON_IsString(label)); + fuzz_assert(cJSON_SetValuestring(label, value) != NULL); +} + +static cJSON_bool tree_contains_raw(const cJSON *item) { + const cJSON *child = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsRaw(item)) { + return 1; + } + + for (child = item->child; child != NULL; child = child->next) { + if (tree_contains_raw(child)) { + return 1; + } + } + + return 0; +} + +static cJSON_bool tree_contains_nonfinite_number(const cJSON *item) { + const cJSON *child = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsNumber(item) && !isfinite(item->valuedouble)) { + return 1; + } + + for (child = item->child; child != NULL; child = child->next) { + if (tree_contains_nonfinite_number(child)) { + return 1; + } + } + + return 0; +} + +static int keys_equal_case_insensitive(const char *left, const char *right) { + if ((left == NULL) || (right == NULL)) { + return 0; + } + + while ((*left != '\0') && (*right != '\0')) { + if (tolower((unsigned char)*left) != tolower((unsigned char)*right)) { + return 0; + } + + left++; + right++; + } + + return (*left == '\0') && (*right == '\0'); +} + +static cJSON_bool tree_has_ambiguous_object_keys(const cJSON *item) { + const cJSON *outer = NULL; + const cJSON *inner = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsObject(item)) { + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + + for (inner = outer->next; inner != NULL; inner = inner->next) { + if (keys_equal_case_insensitive(outer->string, inner->string)) { + return 1; + } + } + } + + return 0; + } + + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + } + + return 0; +} + +static void verify_reflexive(const cJSON *item) { + fuzz_assert(item != NULL); + fuzz_assert(cJSON_Compare(item, item, 1)); + fuzz_assert(cJSON_Compare(item, item, 0)); +} + +static void verify_null_and_invalid_behavior(const cJSON *item) { + cJSON invalid[1]; + + memset(invalid, 0, sizeof(invalid)); + + fuzz_assert(!cJSON_Compare(NULL, NULL, 1)); + fuzz_assert(!cJSON_Compare(NULL, NULL, 0)); + fuzz_assert(!cJSON_Compare(invalid, invalid, 1)); + fuzz_assert(!cJSON_Compare(invalid, invalid, 0)); + + invalid->type = cJSON_Number | cJSON_String; + fuzz_assert(!cJSON_Compare(invalid, invalid, 1)); + fuzz_assert(!cJSON_Compare(invalid, invalid, 0)); + + if (item != NULL) { + fuzz_assert(!cJSON_Compare(NULL, item, 1)); + fuzz_assert(!cJSON_Compare(item, NULL, 1)); + fuzz_assert(!cJSON_Compare(NULL, item, 0)); + fuzz_assert(!cJSON_Compare(item, NULL, 0)); + } +} + +static void verify_roundtrip_if_safe(const cJSON *item) { + char *printed = NULL; + cJSON *parsed = NULL; + + if ((item == NULL) || + tree_contains_raw(item) || + tree_contains_nonfinite_number(item) || + tree_has_ambiguous_object_keys(item)) { + return; + } + + printed = cJSON_PrintUnformatted(item); + if (printed == NULL) { + return; + } + + parsed = cJSON_Parse(printed); + fuzz_assert(parsed != NULL); + fuzz_assert(cJSON_Compare(item, parsed, 1)); + fuzz_assert(cJSON_Compare(item, parsed, 0)); + cJSON_Delete(parsed); + free(printed); +} + +static void verify_duplicate_equality(const cJSON *item) { + cJSON *dup = NULL; + + if ((item == NULL) || + tree_contains_nonfinite_number(item) || + tree_has_ambiguous_object_keys(item)) { + return; + } + + dup = cJSON_Duplicate(item, 1); + if (dup == NULL) { + return; + } + + fuzz_assert(cJSON_Compare(item, dup, 1)); + fuzz_assert(cJSON_Compare(dup, item, 1)); + fuzz_assert(cJSON_Compare(item, dup, 0)); + fuzz_assert(cJSON_Compare(dup, item, 0)); + + cJSON_Delete(dup); +} + +static void verify_pair_relation(const cJSON *left, + const cJSON *right, + cJSON_bool expect_strict, + cJSON_bool expect_loose) { + cJSON *left_dup = NULL; + cJSON *right_dup = NULL; + cJSON_bool strict_lr = 0; + cJSON_bool strict_rl = 0; + cJSON_bool loose_lr = 0; + cJSON_bool loose_rl = 0; + + fuzz_assert(left != NULL); + fuzz_assert(right != NULL); + + strict_lr = cJSON_Compare(left, right, 1); + strict_rl = cJSON_Compare(right, left, 1); + loose_lr = cJSON_Compare(left, right, 0); + loose_rl = cJSON_Compare(right, left, 0); + + fuzz_assert(strict_lr == strict_rl); + fuzz_assert(loose_lr == loose_rl); + fuzz_assert(strict_lr == expect_strict); + fuzz_assert(loose_lr == expect_loose); + fuzz_assert(!expect_strict || expect_loose); + + verify_reflexive(left); + verify_reflexive(right); + verify_duplicate_equality(left); + verify_duplicate_equality(right); + verify_roundtrip_if_safe(left); + verify_roundtrip_if_safe(right); + verify_null_and_invalid_behavior(left); + verify_null_and_invalid_behavior(right); + + left_dup = cJSON_Duplicate(left, 1); + right_dup = cJSON_Duplicate(right, 1); + if ((left_dup != NULL) && + (right_dup != NULL) && + !tree_contains_nonfinite_number(left) && + !tree_contains_nonfinite_number(right) && + !tree_has_ambiguous_object_keys(left) && + !tree_has_ambiguous_object_keys(right)) { + fuzz_assert(cJSON_Compare(left_dup, right_dup, 1) == expect_strict); + fuzz_assert(cJSON_Compare(right_dup, left_dup, 1) == expect_strict); + fuzz_assert(cJSON_Compare(left_dup, right_dup, 0) == expect_loose); + fuzz_assert(cJSON_Compare(right_dup, left_dup, 0) == expect_loose); + } + + cJSON_Delete(left_dup); + cJSON_Delete(right_dup); +} + +static void run_seed_equality_scenario(void) { + compare_pair pair = create_seed_pair(); + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + verify_pair_relation(pair.left, pair.right, 1, 1); + delete_pair(&pair); +} + +static void run_same_number_scenario(const uint8_t **data, size_t *size) { + compare_pair pair = create_seed_pair(); + int value = 0; + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + (void)read_small_int(data, size, &value); + set_nested_number(pair.left, value); + set_nested_number(pair.right, value); + + verify_pair_relation(pair.left, pair.right, 1, 1); + delete_pair(&pair); +} + +static void run_different_number_scenario(const uint8_t **data, size_t *size) { + compare_pair pair = create_seed_pair(); + int left_value = 0; + int right_value = 0; + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + (void)read_small_int(data, size, &left_value); + (void)read_small_int(data, size, &right_value); + if (left_value == right_value) { + right_value += 1; + } + + set_nested_number(pair.left, left_value); + set_nested_number(pair.right, right_value); + + verify_pair_relation(pair.left, pair.right, 0, 0); + delete_pair(&pair); +} + +static void run_same_string_scenario(const uint8_t **data, size_t *size) { + compare_pair pair = create_seed_pair(); + char *payload = NULL; + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + payload = read_bounded_string(data, size, 24); + if (payload == NULL) { + payload = (char *)malloc(sizeof("same")); + if (payload == NULL) { + delete_pair(&pair); + return; + } + memcpy(payload, "same", sizeof("same")); + } + + set_nested_label(pair.left, payload); + set_nested_label(pair.right, payload); + + verify_pair_relation(pair.left, pair.right, 1, 1); + + free(payload); + delete_pair(&pair); +} + +static void run_different_string_scenario(const uint8_t **data, size_t *size) { + compare_pair pair = create_seed_pair(); + char *left_payload = NULL; + char *right_payload = NULL; + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + left_payload = read_bounded_string(data, size, 24); + right_payload = read_bounded_string(data, size, 24); + if (left_payload == NULL) { + left_payload = (char *)malloc(sizeof("left")); + if (left_payload == NULL) { + delete_pair(&pair); + return; + } + memcpy(left_payload, "left", sizeof("left")); + } + if (right_payload == NULL) { + right_payload = (char *)malloc(sizeof("right")); + if (right_payload == NULL) { + free(left_payload); + delete_pair(&pair); + return; + } + memcpy(right_payload, "right", sizeof("right")); + } + if (strcmp(left_payload, right_payload) == 0) { + size_t len = strlen(right_payload); + char *replacement = (char *)malloc(len + 2); + + if (replacement == NULL) { + free(left_payload); + free(right_payload); + delete_pair(&pair); + return; + } + + memcpy(replacement, right_payload, len); + replacement[len] = '!'; + replacement[len + 1] = '\0'; + free(right_payload); + right_payload = replacement; + } + + set_nested_label(pair.left, left_payload); + set_nested_label(pair.right, right_payload); + + verify_pair_relation(pair.left, pair.right, 0, 0); + + free(left_payload); + free(right_payload); + delete_pair(&pair); +} + +static void run_case_sensitivity_scenario(void) { + compare_pair pair = create_case_variant_pair(); + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + verify_pair_relation(pair.left, pair.right, 0, 1); + delete_pair(&pair); +} + +static void run_object_order_scenario(void) { + compare_pair pair = create_object_order_pair(); + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + verify_pair_relation(pair.left, pair.right, 1, 1); + delete_pair(&pair); +} + +static void run_array_order_scenario(void) { + compare_pair pair = create_array_order_pair(); + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + verify_pair_relation(pair.left, pair.right, 0, 0); + delete_pair(&pair); +} + +static void run_subset_scenario(void) { + compare_pair pair = create_seed_pair(); + + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + cJSON_DeleteItemFromObjectCaseSensitive(pair.right, "meta"); + verify_pair_relation(pair.left, pair.right, 0, 0); + delete_pair(&pair); +} + +static void run_raw_scenario(const uint8_t **data, size_t *size) { + compare_pair pair; + uint8_t left_selector = 0; + uint8_t right_selector = 0; + cJSON_bool expect_equal = 0; + + (void)read_u8(data, size, &left_selector); + (void)read_u8(data, size, &right_selector); + + pair = create_raw_pair(left_selector, right_selector); + if ((pair.left == NULL) || (pair.right == NULL)) { + delete_pair(&pair); + return; + } + + expect_equal = (strcmp(select_raw_literal(left_selector), + select_raw_literal(right_selector)) == 0); + verify_pair_relation(pair.left, pair.right, expect_equal, expect_equal); + delete_pair(&pair); +} + +static void run_parsed_pair_scenario(const uint8_t *data, size_t size) { + char *left_text = NULL; + char *right_text = NULL; + cJSON *left = NULL; + cJSON *right = NULL; + size_t split = 0; + size_t left_len = 0; + size_t right_len = 0; + + if ((data == NULL) || (size < 2)) { + return; + } + + split = find_split(data, size); + if ((split == 0) || (split >= (size - 1))) { + return; + } + + left_len = split; + right_len = size - split - 1; + + left_text = (char *)malloc(left_len + 1); + right_text = (char *)malloc(right_len + 1); + if ((left_text == NULL) || (right_text == NULL)) { + free(left_text); + free(right_text); + return; + } + + memcpy(left_text, data, left_len); + left_text[left_len] = '\0'; + memcpy(right_text, data + split + 1, right_len); + right_text[right_len] = '\0'; + + left = cJSON_Parse(left_text); + right = cJSON_Parse(right_text); + + if (left != NULL) { + verify_reflexive(left); + verify_duplicate_equality(left); + verify_roundtrip_if_safe(left); + verify_null_and_invalid_behavior(left); + } + + if (right != NULL) { + verify_reflexive(right); + verify_duplicate_equality(right); + verify_roundtrip_if_safe(right); + verify_null_and_invalid_behavior(right); + } + + if ((left != NULL) && (right != NULL)) { + verify_pair_relation(left, + right, + cJSON_Compare(left, right, 1), + cJSON_Compare(left, right, 0)); + } + + cJSON_Delete(left); + cJSON_Delete(right); + free(left_text); + free(right_text); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + const uint8_t *payload_data = NULL; + size_t payload_size = 0; + uint8_t raw_cases = 0; + size_t case_count = 0; + + if ((data == NULL) || (size == 0)) { + return 0; + } + + verify_null_and_invalid_behavior(NULL); + + if (!read_u8(&data, &size, &raw_cases)) { + return 0; + } + + payload_data = data; + payload_size = size; + case_count = (size_t)(raw_cases % 24); + + for (size_t i = 0; (i < case_count) && (size > 0); ++i) { + uint8_t opcode = 0; + + if (!read_u8(&data, &size, &opcode)) { + break; + } + + switch (opcode % 9) { + case 0: + run_seed_equality_scenario(); + break; + + case 1: + run_same_number_scenario(&data, &size); + break; + + case 2: + run_different_number_scenario(&data, &size); + break; + + case 3: + run_same_string_scenario(&data, &size); + break; + + case 4: + run_different_string_scenario(&data, &size); + break; + + case 5: + run_case_sensitivity_scenario(); + break; + + case 6: + run_object_order_scenario(); + break; + + case 7: + run_array_order_scenario(); + break; + + case 8: + default: + if ((opcode & 1) == 0) { + run_subset_scenario(); + } else { + run_raw_scenario(&data, &size); + } + break; + } + } + + if (payload_size > 0) { + run_parsed_pair_scenario(payload_data, payload_size); + } + + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/fuzzing/cjson_create_fuzzer.c b/fuzzing/cjson_create_fuzzer.c new file mode 100644 index 00000000..b08c1572 --- /dev/null +++ b/fuzzing/cjson_create_fuzzer.c @@ -0,0 +1,909 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define MAX_ARRAY_COUNT 32 +#define MAX_STRING_LEN 32 + +static void fuzz_assert(int condition) { + if (!condition) { + abort(); + } +} + +static int read_u8(const uint8_t **data, size_t *size, uint8_t *out) { + if (*size < 1) { + return 0; + } + + *out = **data; + (*data)++; + (*size)--; + return 1; +} + +static int read_bytes(const uint8_t **data, size_t *size, uint8_t *out, size_t n) { + if (*size < n) { + return 0; + } + + memcpy(out, *data, n); + (*data) += n; + (*size) -= n; + return 1; +} + +static int read_i32(const uint8_t **data, size_t *size, int *out) { + union { + uint32_t u32; + int i32; + } conv; + + conv.u32 = 0; + + if (*size >= sizeof(uint32_t)) { + if (!read_bytes(data, size, (uint8_t *)&conv.u32, sizeof(uint32_t))) { + return 0; + } + } else { + size_t n = *size; + + if (n > 0) { + memcpy(&conv.u32, *data, n); + (*data) += n; + (*size) -= n; + } + } + + *out = conv.i32; + return 1; +} + +static int read_float(const uint8_t **data, size_t *size, float *out) { + union { + uint32_t u32; + float f; + } conv; + + conv.u32 = 0; + + if (*size >= sizeof(uint32_t)) { + if (!read_bytes(data, size, (uint8_t *)&conv.u32, sizeof(uint32_t))) { + return 0; + } + } else { + size_t n = *size; + + if (n > 0) { + memcpy(&conv.u32, *data, n); + (*data) += n; + (*size) -= n; + } + } + + *out = conv.f; + return 1; +} + +static int read_double(const uint8_t **data, size_t *size, double *out) { + union { + uint64_t u64; + double d; + } conv; + + conv.u64 = 0; + + if (*size >= sizeof(uint64_t)) { + if (!read_bytes(data, size, (uint8_t *)&conv.u64, sizeof(uint64_t))) { + return 0; + } + } else { + size_t n = *size; + + if (n > 0) { + memcpy(&conv.u64, *data, n); + (*data) += n; + (*size) -= n; + } + } + + *out = conv.d; + return 1; +} + +static char *read_bounded_string(const uint8_t **data, size_t *size) { + uint8_t raw_len = 0; + size_t len = 0; + char *out = NULL; + + if (!read_u8(data, size, &raw_len)) { + return NULL; + } + + len = (size_t)(raw_len % MAX_STRING_LEN); + if (*size < len) { + len = *size; + } + + out = (char *)malloc(len + 1); + if (out == NULL) { + return NULL; + } + + if (len > 0) { + memcpy(out, *data, len); + (*data) += len; + (*size) -= len; + } + + out[len] = '\0'; + for (size_t i = 0; i < len; ++i) { + if (out[i] == '\0') { + out[i] = 'A'; + } + } + + return out; +} + +static int number_to_valueint(double number) { + fuzz_assert(!isnan(number)); + + if (number >= INT_MAX) { + return INT_MAX; + } + if (number <= (double)INT_MIN) { + return INT_MIN; + } + return (int)number; +} + +static int keys_equal_case_insensitive(const char *left, const char *right) { + if ((left == NULL) || (right == NULL)) { + return 0; + } + + while ((*left != '\0') && (*right != '\0')) { + if (tolower((unsigned char)*left) != tolower((unsigned char)*right)) { + return 0; + } + left++; + right++; + } + + return (*left == '\0') && (*right == '\0'); +} + +static cJSON_bool tree_has_ambiguous_object_keys(const cJSON *item) { + const cJSON *outer = NULL; + const cJSON *inner = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsObject(item)) { + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + + for (inner = outer->next; inner != NULL; inner = inner->next) { + if (keys_equal_case_insensitive(outer->string, inner->string)) { + return 1; + } + } + } + + return 0; + } + + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + } + + return 0; +} + +static cJSON_bool tree_contains_raw(const cJSON *item) { + const cJSON *child = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsRaw(item)) { + return 1; + } + + for (child = item->child; child != NULL; child = child->next) { + if (tree_contains_raw(child)) { + return 1; + } + } + + return 0; +} + +static cJSON_bool tree_contains_nonfinite_number(const cJSON *item) { + const cJSON *child = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsNumber(item) && !isfinite(item->valuedouble)) { + return 1; + } + + for (child = item->child; child != NULL; child = child->next) { + if (tree_contains_nonfinite_number(child)) { + return 1; + } + } + + return 0; +} + +static cJSON_bool tree_is_roundtrip_safe(const cJSON *item) { + return (item != NULL) && + !tree_contains_raw(item) && + !tree_contains_nonfinite_number(item) && + !tree_has_ambiguous_object_keys(item); +} + +static void verify_roundtrip_if_safe(const cJSON *item) { + char *printed = NULL; + cJSON *parsed = NULL; + + if (!tree_is_roundtrip_safe(item)) { + return; + } + + printed = cJSON_PrintUnformatted(item); + if (printed == NULL) { + return; + } + + parsed = cJSON_Parse(printed); + fuzz_assert(parsed != NULL); + fuzz_assert(cJSON_Compare(item, parsed, 1)); + fuzz_assert(cJSON_Compare(item, parsed, 0)); + cJSON_Delete(parsed); + free(printed); +} + +static void verify_printed_text(const cJSON *item, const char *expected) { + char *printed = NULL; + + fuzz_assert(item != NULL); + fuzz_assert(expected != NULL); + + printed = cJSON_PrintUnformatted(item); + fuzz_assert(printed != NULL); + fuzz_assert(strcmp(printed, expected) == 0); + free(printed); +} + +static void verify_item_type(const cJSON *item, int base_type) { + fuzz_assert(item != NULL); + fuzz_assert((item->type & 0xFF) == base_type); +} + +static void verify_fresh_item(const cJSON *item, int base_type) { + verify_item_type(item, base_type); + fuzz_assert(item->next == NULL); + fuzz_assert(item->prev == NULL); +} + +static void verify_number_item(const cJSON *item, double expected) { + cJSON *reference = NULL; + + verify_item_type(item, cJSON_Number); + + if (!isfinite(expected)) { + fuzz_assert(!isfinite(cJSON_GetNumberValue(item))); + return; + } + + reference = cJSON_CreateNumber(expected); + if (reference != NULL) { + fuzz_assert(cJSON_Compare(item, reference, 1)); + fuzz_assert(cJSON_Compare(item, reference, 0)); + cJSON_Delete(reference); + } + + fuzz_assert(item->valueint == number_to_valueint(expected)); +} + +static void verify_global_invalid_behavior(void) { + int dummy_ints[1] = {0}; + float dummy_floats[1] = {0.0f}; + double dummy_doubles[1] = {0.0}; + const char *dummy_strings[1] = {"x"}; + + fuzz_assert(cJSON_CreateString(NULL) == NULL); + fuzz_assert(cJSON_CreateRaw(NULL) == NULL); + + fuzz_assert(cJSON_CreateIntArray(NULL, 10) == NULL); + fuzz_assert(cJSON_CreateFloatArray(NULL, 10) == NULL); + fuzz_assert(cJSON_CreateDoubleArray(NULL, 10) == NULL); + fuzz_assert(cJSON_CreateStringArray(NULL, 10) == NULL); + + fuzz_assert(cJSON_CreateIntArray(NULL, 0) == NULL); + fuzz_assert(cJSON_CreateFloatArray(NULL, 0) == NULL); + fuzz_assert(cJSON_CreateDoubleArray(NULL, 0) == NULL); + fuzz_assert(cJSON_CreateStringArray(NULL, 0) == NULL); + + fuzz_assert(cJSON_CreateIntArray(dummy_ints, -1) == NULL); + fuzz_assert(cJSON_CreateFloatArray(dummy_floats, -1) == NULL); + fuzz_assert(cJSON_CreateDoubleArray(dummy_doubles, -1) == NULL); + fuzz_assert(cJSON_CreateStringArray(dummy_strings, -1) == NULL); +} + +static void run_primitive_scenario(const uint8_t **data, size_t *size) { + uint8_t raw_bool = 0; + double number = 0.0; + cJSON *null_item = NULL; + cJSON *true_item = NULL; + cJSON *false_item = NULL; + cJSON *bool_item = NULL; + cJSON *number_item = NULL; + cJSON *array_item = NULL; + cJSON *object_item = NULL; + + (void)read_u8(data, size, &raw_bool); + (void)read_double(data, size, &number); + + null_item = cJSON_CreateNull(); + true_item = cJSON_CreateTrue(); + false_item = cJSON_CreateFalse(); + bool_item = cJSON_CreateBool((raw_bool & 1) ? 1 : 0); + number_item = cJSON_CreateNumber(number); + array_item = cJSON_CreateArray(); + object_item = cJSON_CreateObject(); + + if ((null_item == NULL) || + (true_item == NULL) || + (false_item == NULL) || + (bool_item == NULL) || + (number_item == NULL) || + (array_item == NULL) || + (object_item == NULL)) { + cJSON_Delete(null_item); + cJSON_Delete(true_item); + cJSON_Delete(false_item); + cJSON_Delete(bool_item); + cJSON_Delete(number_item); + cJSON_Delete(array_item); + cJSON_Delete(object_item); + return; + } + + verify_fresh_item(null_item, cJSON_NULL); + fuzz_assert(cJSON_IsNull(null_item)); + fuzz_assert(null_item->child == NULL); + verify_printed_text(null_item, "null"); + verify_roundtrip_if_safe(null_item); + + verify_fresh_item(true_item, cJSON_True); + fuzz_assert(cJSON_IsTrue(true_item)); + fuzz_assert(true_item->child == NULL); + verify_printed_text(true_item, "true"); + verify_roundtrip_if_safe(true_item); + + verify_fresh_item(false_item, cJSON_False); + fuzz_assert(cJSON_IsFalse(false_item)); + fuzz_assert(false_item->child == NULL); + verify_printed_text(false_item, "false"); + verify_roundtrip_if_safe(false_item); + + verify_fresh_item(bool_item, (raw_bool & 1) ? cJSON_True : cJSON_False); + fuzz_assert(cJSON_IsBool(bool_item)); + fuzz_assert(bool_item->child == NULL); + verify_printed_text(bool_item, (raw_bool & 1) ? "true" : "false"); + verify_roundtrip_if_safe(bool_item); + + verify_number_item(number_item, number); + fuzz_assert(number_item->child == NULL); + if (isfinite(number)) { + verify_roundtrip_if_safe(number_item); + } else { + verify_printed_text(number_item, "null"); + } + + verify_fresh_item(array_item, cJSON_Array); + fuzz_assert(cJSON_IsArray(array_item)); + fuzz_assert(array_item->child == NULL); + fuzz_assert(cJSON_GetArraySize(array_item) == 0); + verify_printed_text(array_item, "[]"); + verify_roundtrip_if_safe(array_item); + + verify_fresh_item(object_item, cJSON_Object); + fuzz_assert(cJSON_IsObject(object_item)); + fuzz_assert(object_item->child == NULL); + fuzz_assert(cJSON_GetArraySize(object_item) == 0); + verify_printed_text(object_item, "{}"); + verify_roundtrip_if_safe(object_item); + + cJSON_Delete(null_item); + cJSON_Delete(true_item); + cJSON_Delete(false_item); + cJSON_Delete(bool_item); + cJSON_Delete(number_item); + cJSON_Delete(array_item); + cJSON_Delete(object_item); +} + +static void run_string_and_raw_scenario(const uint8_t **data, size_t *size) { + static const char *raw_values[] = { + "null", + "true", + "false", + "0", + "\"raw\"", + "[]", + "{}" + }; + uint8_t raw_selector = 0; + char *string_payload = NULL; + char *raw_payload = NULL; + cJSON *string_item = NULL; + cJSON *raw_item = NULL; + + string_payload = read_bounded_string(data, size); + if ((string_payload == NULL) || (string_payload[0] == '\0')) { + free(string_payload); + string_payload = (char *)malloc(sizeof("seed")); + if (string_payload != NULL) { + memcpy(string_payload, "seed", sizeof("seed")); + } + } + if (string_payload == NULL) { + return; + } + + (void)read_u8(data, size, &raw_selector); + raw_payload = (char *)malloc(strlen(raw_values[raw_selector % (sizeof(raw_values) / sizeof(raw_values[0]))]) + 1); + if (raw_payload == NULL) { + free(string_payload); + return; + } + strcpy(raw_payload, raw_values[raw_selector % (sizeof(raw_values) / sizeof(raw_values[0]))]); + + string_item = cJSON_CreateString(string_payload); + raw_item = cJSON_CreateRaw(raw_payload); + if ((string_item == NULL) || (raw_item == NULL)) { + cJSON_Delete(string_item); + cJSON_Delete(raw_item); + free(string_payload); + free(raw_payload); + return; + } + + verify_fresh_item(string_item, cJSON_String); + fuzz_assert(cJSON_IsString(string_item)); + fuzz_assert(string_item->child == NULL); + fuzz_assert(cJSON_GetStringValue(string_item) != NULL); + fuzz_assert(strcmp(cJSON_GetStringValue(string_item), string_payload) == 0); + fuzz_assert(string_item->valuestring != string_payload); + verify_roundtrip_if_safe(string_item); + + string_payload[0] = (string_payload[0] == 'Z') ? 'Y' : 'Z'; + fuzz_assert(strcmp(cJSON_GetStringValue(string_item), string_payload) != 0); + + verify_fresh_item(raw_item, cJSON_Raw); + fuzz_assert(cJSON_IsRaw(raw_item)); + fuzz_assert(raw_item->child == NULL); + fuzz_assert(raw_item->valuestring != NULL); + fuzz_assert(strcmp(raw_item->valuestring, raw_payload) == 0); + fuzz_assert(raw_item->valuestring != raw_payload); + verify_printed_text(raw_item, raw_values[raw_selector % (sizeof(raw_values) / sizeof(raw_values[0]))]); + + raw_payload[0] = (raw_payload[0] == 'Z') ? 'Y' : 'Z'; + fuzz_assert(strcmp(raw_item->valuestring, raw_payload) != 0); + + cJSON_Delete(string_item); + cJSON_Delete(raw_item); + free(string_payload); + free(raw_payload); +} + +static void run_string_reference_scenario(const uint8_t **data, size_t *size) { + char *payload = NULL; + cJSON *string_ref = NULL; + + payload = read_bounded_string(data, size); + if ((payload == NULL) || (payload[0] == '\0')) { + free(payload); + payload = (char *)malloc(sizeof("ref")); + if (payload != NULL) { + memcpy(payload, "ref", sizeof("ref")); + } + } + if (payload == NULL) { + return; + } + + string_ref = cJSON_CreateStringReference(payload); + if (string_ref == NULL) { + free(payload); + return; + } + + verify_fresh_item(string_ref, cJSON_String); + fuzz_assert((string_ref->type & cJSON_IsReference) != 0); + fuzz_assert(string_ref->valuestring == payload); + fuzz_assert(cJSON_IsString(string_ref)); + verify_roundtrip_if_safe(string_ref); + + payload[0] = (payload[0] == 'Q') ? 'R' : 'Q'; + fuzz_assert(string_ref->valuestring[0] == payload[0]); + + cJSON_Delete(string_ref); + free(payload); +} + +static void run_container_reference_scenario(const uint8_t **data, size_t *size) { + int updated_value = 99; + cJSON *number = NULL; + cJSON *obj_ref = NULL; + cJSON *arr_ref = NULL; + cJSON *null_obj_ref = NULL; + cJSON *null_arr_ref = NULL; + + (void)read_i32(data, size, &updated_value); + + number = cJSON_CreateNumber(42.0); + if (number == NULL) { + return; + } + + obj_ref = cJSON_CreateObjectReference(number); + arr_ref = cJSON_CreateArrayReference(number); + null_obj_ref = cJSON_CreateObjectReference(NULL); + null_arr_ref = cJSON_CreateArrayReference(NULL); + if ((obj_ref == NULL) || (arr_ref == NULL) || (null_obj_ref == NULL) || (null_arr_ref == NULL)) { + cJSON_Delete(number); + cJSON_Delete(obj_ref); + cJSON_Delete(arr_ref); + cJSON_Delete(null_obj_ref); + cJSON_Delete(null_arr_ref); + return; + } + + verify_fresh_item(obj_ref, cJSON_Object); + fuzz_assert((obj_ref->type & cJSON_IsReference) != 0); + fuzz_assert(obj_ref->child == number); + + verify_fresh_item(arr_ref, cJSON_Array); + fuzz_assert((arr_ref->type & cJSON_IsReference) != 0); + fuzz_assert(arr_ref->child == number); + + verify_fresh_item(null_obj_ref, cJSON_Object); + fuzz_assert((null_obj_ref->type & cJSON_IsReference) != 0); + fuzz_assert(null_obj_ref->child == NULL); + + verify_fresh_item(null_arr_ref, cJSON_Array); + fuzz_assert((null_arr_ref->type & cJSON_IsReference) != 0); + fuzz_assert(null_arr_ref->child == NULL); + + cJSON_SetNumberValue(number, updated_value); + verify_number_item(obj_ref->child, (double)updated_value); + verify_number_item(arr_ref->child, (double)updated_value); + + cJSON_Delete(number); + cJSON_Delete(obj_ref); + cJSON_Delete(arr_ref); + cJSON_Delete(null_obj_ref); + cJSON_Delete(null_arr_ref); +} + +static void verify_int_array(cJSON *array, const int *values, int count) { + fuzz_assert(array != NULL); + verify_fresh_item(array, cJSON_Array); + fuzz_assert(cJSON_IsArray(array)); + fuzz_assert(cJSON_GetArraySize(array) == count); + + for (int i = 0; i < count; ++i) { + verify_number_item(cJSON_GetArrayItem(array, i), (double)values[i]); + } + + verify_roundtrip_if_safe(array); +} + +static void run_int_array_scenario(const uint8_t **data, size_t *size) { + uint8_t raw_count = 0; + int count = 0; + int dummy = 0; + int *values = NULL; + cJSON *array = NULL; + + (void)read_u8(data, size, &raw_count); + count = (int)(raw_count % MAX_ARRAY_COUNT); + + if (count > 0) { + values = (int *)calloc((size_t)count, sizeof(int)); + if (values == NULL) { + return; + } + for (int i = 0; i < count; ++i) { + (void)read_i32(data, size, &values[i]); + } + array = cJSON_CreateIntArray(values, count); + } else { + array = cJSON_CreateIntArray(&dummy, 0); + } + + if (array != NULL) { + verify_int_array(array, (count > 0) ? values : &dummy, count); + if (count > 0) { + int before = cJSON_GetArrayItem(array, 0)->valueint; + values[0] += 1; + fuzz_assert(cJSON_GetArrayItem(array, 0)->valueint == before); + } else { + verify_printed_text(array, "[]"); + } + } + + cJSON_Delete(array); + free(values); +} + +static void verify_float_array(cJSON *array, const float *values, int count) { + fuzz_assert(array != NULL); + verify_fresh_item(array, cJSON_Array); + fuzz_assert(cJSON_GetArraySize(array) == count); + + for (int i = 0; i < count; ++i) { + verify_number_item(cJSON_GetArrayItem(array, i), (double)values[i]); + } + + verify_roundtrip_if_safe(array); +} + +static void run_float_array_scenario(const uint8_t **data, size_t *size) { + uint8_t raw_count = 0; + int count = 0; + float dummy = 0.0f; + float *values = NULL; + cJSON *array = NULL; + + (void)read_u8(data, size, &raw_count); + count = (int)(raw_count % MAX_ARRAY_COUNT); + + if (count > 0) { + values = (float *)calloc((size_t)count, sizeof(float)); + if (values == NULL) { + return; + } + for (int i = 0; i < count; ++i) { + (void)read_float(data, size, &values[i]); + } + array = cJSON_CreateFloatArray(values, count); + } else { + array = cJSON_CreateFloatArray(&dummy, 0); + } + + if (array != NULL) { + verify_float_array(array, (count > 0) ? values : &dummy, count); + if (count > 0) { + char *before = cJSON_PrintUnformatted(array); + values[0] += 1.0f; + char *after = NULL; + fuzz_assert(before != NULL); + after = cJSON_PrintUnformatted(array); + fuzz_assert(after != NULL); + fuzz_assert(strcmp(before, after) == 0); + free(after); + free(before); + } else { + verify_printed_text(array, "[]"); + } + } + + cJSON_Delete(array); + free(values); +} + +static void verify_double_array(cJSON *array, const double *values, int count) { + fuzz_assert(array != NULL); + verify_fresh_item(array, cJSON_Array); + fuzz_assert(cJSON_GetArraySize(array) == count); + + for (int i = 0; i < count; ++i) { + verify_number_item(cJSON_GetArrayItem(array, i), values[i]); + } + + verify_roundtrip_if_safe(array); +} + +static void run_double_array_scenario(const uint8_t **data, size_t *size) { + uint8_t raw_count = 0; + int count = 0; + double dummy = 0.0; + double *values = NULL; + cJSON *array = NULL; + + (void)read_u8(data, size, &raw_count); + count = (int)(raw_count % MAX_ARRAY_COUNT); + + if (count > 0) { + values = (double *)calloc((size_t)count, sizeof(double)); + if (values == NULL) { + return; + } + for (int i = 0; i < count; ++i) { + (void)read_double(data, size, &values[i]); + } + array = cJSON_CreateDoubleArray(values, count); + } else { + array = cJSON_CreateDoubleArray(&dummy, 0); + } + + if (array != NULL) { + verify_double_array(array, (count > 0) ? values : &dummy, count); + if (count > 0) { + char *before = cJSON_PrintUnformatted(array); + values[0] += 1.0; + char *after = NULL; + fuzz_assert(before != NULL); + after = cJSON_PrintUnformatted(array); + fuzz_assert(after != NULL); + fuzz_assert(strcmp(before, after) == 0); + free(after); + free(before); + } else { + verify_printed_text(array, "[]"); + } + } + + cJSON_Delete(array); + free(values); +} + +static void run_string_array_scenario(const uint8_t **data, size_t *size) { + uint8_t raw_count = 0; + int count = 0; + const char *dummy_strings[1] = {"unused"}; + char **values = NULL; + cJSON *array = NULL; + + (void)read_u8(data, size, &raw_count); + count = (int)(raw_count % MAX_ARRAY_COUNT); + + if (count > 0) { + values = (char **)calloc((size_t)count, sizeof(char *)); + if (values == NULL) { + return; + } + + for (int i = 0; i < count; ++i) { + values[i] = read_bounded_string(data, size); + if ((values[i] == NULL) || (values[i][0] == '\0')) { + free(values[i]); + values[i] = (char *)malloc(sizeof("a")); + if (values[i] == NULL) { + count = i; + break; + } + memcpy(values[i], "a", sizeof("a")); + } + } + + if (count > 0) { + array = cJSON_CreateStringArray((const char * const *)values, count); + } + } else { + array = cJSON_CreateStringArray(dummy_strings, 0); + } + + if (array != NULL) { + verify_fresh_item(array, cJSON_Array); + fuzz_assert(cJSON_GetArraySize(array) == count); + + for (int i = 0; i < count; ++i) { + cJSON *entry = cJSON_GetArrayItem(array, i); + verify_item_type(entry, cJSON_String); + fuzz_assert(cJSON_IsString(entry)); + fuzz_assert(entry->valuestring != NULL); + fuzz_assert(strcmp(entry->valuestring, values[i]) == 0); + fuzz_assert(entry->valuestring != values[i]); + } + + verify_roundtrip_if_safe(array); + if (count > 0) { + char before = cJSON_GetArrayItem(array, 0)->valuestring[0]; + values[0][0] = (values[0][0] == 'Z') ? 'Y' : 'Z'; + fuzz_assert(cJSON_GetArrayItem(array, 0)->valuestring[0] == before); + } else { + verify_printed_text(array, "[]"); + } + } + + cJSON_Delete(array); + if (values != NULL) { + for (int i = 0; i < count; ++i) { + free(values[i]); + } + } + free(values); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + uint8_t raw_cases = 0; + size_t case_count = 0; + + if ((data == NULL) || (size == 0)) { + return 0; + } + + verify_global_invalid_behavior(); + + if (!read_u8(&data, &size, &raw_cases)) { + return 0; + } + + case_count = (size_t)(raw_cases % 24); + + for (size_t i = 0; (i < case_count) && (size > 0); ++i) { + uint8_t opcode = 0; + + if (!read_u8(&data, &size, &opcode)) { + break; + } + + switch (opcode % 8) { + case 0: + run_primitive_scenario(&data, &size); + break; + + case 1: + run_string_and_raw_scenario(&data, &size); + break; + + case 2: + run_string_reference_scenario(&data, &size); + break; + + case 3: + run_container_reference_scenario(&data, &size); + break; + + case 4: + run_int_array_scenario(&data, &size); + break; + + case 5: + run_float_array_scenario(&data, &size); + break; + + case 6: + run_double_array_scenario(&data, &size); + break; + + case 7: + default: + run_string_array_scenario(&data, &size); + break; + } + } + + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/fuzzing/cjson_delete_item_from_object_fuzzer.c b/fuzzing/cjson_delete_item_from_object_fuzzer.c new file mode 100644 index 00000000..94d09a47 --- /dev/null +++ b/fuzzing/cjson_delete_item_from_object_fuzzer.c @@ -0,0 +1,540 @@ +#include +#include +#include +#include +#include "../cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static int read_u8(const uint8_t **data, size_t *size, uint8_t *out) { + if (*size < 1) { + return 0; + } + *out = **data; + (*data)++; + (*size)--; + return 1; +} + +static int read_u8_as_int(const uint8_t **data, size_t *size, int *out) { + uint8_t b = 0; + if (!read_u8(data, size, &b)) { + return 0; + } + *out = (int)b; + return 1; +} + +static char *dup_cstr(const char *s) { + size_t n = strlen(s); + char *copy = (char *)malloc(n + 1); + if (copy == NULL) { + return NULL; + } + memcpy(copy, s, n + 1); + return copy; +} + +static char *read_key(const uint8_t **data, size_t *size) { + uint8_t raw_len = 0; + if (!read_u8(data, size, &raw_len)) { + return NULL; + } + + size_t key_len = (size_t)(raw_len % 64); + if (*size < key_len) { + key_len = *size; + } + + char *key = (char *)malloc(key_len + 1); + if (key == NULL) { + return NULL; + } + + if (key_len > 0) { + memcpy(key, *data, key_len); + (*data) += key_len; + (*size) -= key_len; + } + key[key_len] = '\0'; + + for (size_t i = 0; i < key_len; ++i) { + if (key[i] == '\0') { + key[i] = 'A'; + } + } + + return key; +} + +static void mutate_case(char *s, uint8_t mode) { + if (s == NULL) { + return; + } + + for (size_t i = 0; s[i] != '\0'; ++i) { + unsigned char c = (unsigned char)s[i]; + if (c >= 'a' && c <= 'z') { + if (mode == 1 || (mode == 2 && (i % 2 == 0))) { + s[i] = (char)(c - 'a' + 'A'); + } + } else if (c >= 'A' && c <= 'Z') { + if (mode == 3 || (mode == 2 && (i % 2 == 1))) { + s[i] = (char)(c - 'A' + 'a'); + } + } + } +} + +static cJSON *lookup_object_item(cJSON *object, const char *key, int case_sensitive) { + if (case_sensitive) { + return cJSON_GetObjectItemCaseSensitive(object, key); + } + return cJSON_GetObjectItem(object, key); +} + +static cJSON *snapshot_json(const cJSON *item) { + char *printed = NULL; + cJSON *copy = NULL; + + if (item == NULL) { + return NULL; + } + + printed = cJSON_PrintUnformatted((cJSON *)item); + if (printed == NULL) { + return NULL; + } + + copy = cJSON_Parse(printed); + free(printed); + return copy; +} + +static void assert_semantic_equivalence_if_snapshotted(cJSON *before, cJSON *after) { + if (before == NULL || after == NULL) { + return; + } + + /* Semantic/metamorphic oracle: a no-op delete/detach must not change structure. */ + if (!cJSON_Compare(before, after, 1)) { + abort(); + } +} + +static void seed_object(cJSON *root) { + cJSON *nested = NULL; + cJSON *arr = NULL; + cJSON *inner_arr = NULL; + + cJSON_AddNumberToObject(root, "alpha", 1.0); + cJSON_AddNumberToObject(root, "Bravo", 2.0); + cJSON_AddNumberToObject(root, "CHARLIE", 3.0); + cJSON_AddStringToObject(root, "delta", "x"); + cJSON_AddBoolToObject(root, "Echo", 1); + cJSON_AddNullToObject(root, "foxtrot"); + + nested = cJSON_CreateObject(); + if (nested != NULL) { + cJSON_AddStringToObject(nested, "innerKey", "value"); + cJSON_AddNumberToObject(nested, "InnerNum", 42.0); + cJSON_AddBoolToObject(nested, "innerFlag", 1); + + inner_arr = cJSON_CreateArray(); + if (inner_arr != NULL) { + cJSON_AddItemToArray(inner_arr, cJSON_CreateString("nestedA")); + cJSON_AddItemToArray(inner_arr, cJSON_CreateNumber(99)); + cJSON_AddItemToObject(nested, "innerArray", inner_arr); + } + + cJSON_AddItemToObject(root, "nestedObj", nested); + } + + arr = cJSON_CreateArray(); + if (arr != NULL) { + cJSON_AddItemToArray(arr, cJSON_CreateString("a")); + cJSON_AddItemToArray(arr, cJSON_CreateNumber(7)); + cJSON_AddItemToArray(arr, cJSON_CreateBool(1)); + cJSON_AddItemToObject(root, "ArrayKey", arr); + } +} + +static void clear_root_object(cJSON *root) { + while (root != NULL && root->child != NULL) { + cJSON *detached = cJSON_DetachItemViaPointer(root, root->child); + if (detached == NULL) { + break; + } + cJSON_Delete(detached); + } +} + +static void rebuild_object(cJSON *root) { + if (root == NULL) { + return; + } + clear_root_object(root); + seed_object(root); +} + +static char *choose_known_root_key(uint8_t selector) { + static const char *keys[] = { + "alpha", + "Bravo", + "CHARLIE", + "delta", + "Echo", + "foxtrot", + "nestedObj", + "ArrayKey" + }; + return dup_cstr(keys[selector % (sizeof(keys) / sizeof(keys[0]))]); +} + +static char *choose_known_nested_key(uint8_t selector) { + static const char *keys[] = { + "innerKey", + "InnerNum", + "innerFlag", + "innerArray" + }; + return dup_cstr(keys[selector % (sizeof(keys) / sizeof(keys[0]))]); +} + +static char *choose_lookup_key(const uint8_t **data, size_t *size, int nested_scope) { + uint8_t mode = 0; + if (!read_u8(data, size, &mode)) { + return NULL; + } + + if ((mode & 3) != 0) { + return read_key(data, size); + } + + { + uint8_t sel = 0; + if (!read_u8(data, size, &sel)) { + return NULL; + } + + if (nested_scope) { + return choose_known_nested_key(sel); + } + return choose_known_root_key(sel); + } +} + +static void maybe_rebuild(const uint8_t **data, size_t *size, cJSON *root) { + uint8_t control = 0; + if (!read_u8(data, size, &control)) { + return; + } + + if ((control & 7) == 0) { + rebuild_object(root); + } +} + +static void delete_from_root_object(const uint8_t **data, size_t *size, cJSON *root) { + char *lookup_key = NULL; + uint8_t case_mode = 0; + uint8_t which_api = 0; + int case_sensitive = 0; + cJSON *existing = NULL; + cJSON *root_before = NULL; + + lookup_key = choose_lookup_key(data, size, 0); + if (lookup_key == NULL) { + return; + } + if (!read_u8(data, size, &case_mode)) { + free(lookup_key); + return; + } + if (!read_u8(data, size, &which_api)) { + free(lookup_key); + return; + } + + mutate_case(lookup_key, (uint8_t)(case_mode % 4)); + case_sensitive = (which_api & 1) != 0; + existing = lookup_object_item(root, lookup_key, case_sensitive); + if (existing == NULL) { + root_before = snapshot_json(root); + } + + if (!case_sensitive) { + cJSON_DeleteItemFromObject(root, lookup_key); + } else { + cJSON_DeleteItemFromObjectCaseSensitive(root, lookup_key); + } + + /* Semantic/metamorphic oracle: a successful object delete must remove the key. */ + if (existing != NULL) { + if (lookup_object_item(root, lookup_key, case_sensitive) != NULL) { + cJSON_Delete(root_before); + abort(); + } + } else { + assert_semantic_equivalence_if_snapshotted(root_before, root); + } + + cJSON_Delete(root_before); + free(lookup_key); +} + +static void delete_from_nested_object(const uint8_t **data, size_t *size, cJSON *root) { + cJSON *nested = cJSON_GetObjectItem(root, "nestedObj"); + char *lookup_key = NULL; + uint8_t case_mode = 0; + uint8_t which_api = 0; + int case_sensitive = 0; + cJSON *existing = NULL; + cJSON *nested_before = NULL; + + if (!cJSON_IsObject(nested)) { + return; + } + + lookup_key = choose_lookup_key(data, size, 1); + if (lookup_key == NULL) { + return; + } + if (!read_u8(data, size, &case_mode)) { + free(lookup_key); + return; + } + if (!read_u8(data, size, &which_api)) { + free(lookup_key); + return; + } + + mutate_case(lookup_key, (uint8_t)(case_mode % 4)); + case_sensitive = (which_api & 1) != 0; + existing = lookup_object_item(nested, lookup_key, case_sensitive); + if (existing == NULL) { + nested_before = snapshot_json(nested); + } + + if (!case_sensitive) { + cJSON_DeleteItemFromObject(nested, lookup_key); + } else { + cJSON_DeleteItemFromObjectCaseSensitive(nested, lookup_key); + } + + /* Semantic/metamorphic oracle: nested object deletes must match key presence. */ + if (existing != NULL) { + if (lookup_object_item(nested, lookup_key, case_sensitive) != NULL) { + cJSON_Delete(nested_before); + abort(); + } + } else { + assert_semantic_equivalence_if_snapshotted(nested_before, nested); + } + + cJSON_Delete(nested_before); + free(lookup_key); +} + +static void delete_from_root_array(const uint8_t **data, size_t *size, cJSON *root) { + cJSON *arr = cJSON_GetObjectItem(root, "ArrayKey"); + int index = 0; + int size_before = 0; + int valid_index = 0; + int size_after = 0; + + if (!cJSON_IsArray(arr)) { + return; + } + if (!read_u8_as_int(data, size, &index)) { + return; + } + + size_before = cJSON_GetArraySize(arr); + index = (index % 8) - 2; + valid_index = (index >= 0 && index < size_before); + cJSON_DeleteItemFromArray(arr, index); + size_after = cJSON_GetArraySize(arr); + + /* Semantic/metamorphic oracle: valid array deletes shrink by one, invalid ones are no-ops. */ + if (valid_index) { + if (size_after != (size_before - 1)) { + abort(); + } + } else if (size_after != size_before) { + abort(); + } +} + +static void delete_from_nested_array(const uint8_t **data, size_t *size, cJSON *root) { + cJSON *nested = cJSON_GetObjectItem(root, "nestedObj"); + cJSON *inner_arr = NULL; + int index = 0; + int size_before = 0; + int valid_index = 0; + int size_after = 0; + + if (!cJSON_IsObject(nested)) { + return; + } + + inner_arr = cJSON_GetObjectItem(nested, "innerArray"); + if (!cJSON_IsArray(inner_arr)) { + return; + } + if (!read_u8_as_int(data, size, &index)) { + return; + } + + size_before = cJSON_GetArraySize(inner_arr); + index = (index % 8) - 2; + valid_index = (index >= 0 && index < size_before); + cJSON_DeleteItemFromArray(inner_arr, index); + size_after = cJSON_GetArraySize(inner_arr); + + /* Semantic/metamorphic oracle: nested array delete size changes must match index validity. */ + if (valid_index) { + if (size_after != (size_before - 1)) { + abort(); + } + } else if (size_after != size_before) { + abort(); + } +} + +static void detach_then_delete_from_root(const uint8_t **data, size_t *size, cJSON *root) { + char *lookup_key = NULL; + uint8_t case_mode = 0; + uint8_t which_api = 0; + cJSON *detached = NULL; + int case_sensitive = 0; + cJSON *existing = NULL; + cJSON *root_before = NULL; + + lookup_key = choose_lookup_key(data, size, 0); + if (lookup_key == NULL) { + return; + } + if (!read_u8(data, size, &case_mode)) { + free(lookup_key); + return; + } + if (!read_u8(data, size, &which_api)) { + free(lookup_key); + return; + } + + mutate_case(lookup_key, (uint8_t)(case_mode % 4)); + case_sensitive = (which_api & 1) != 0; + existing = lookup_object_item(root, lookup_key, case_sensitive); + if (existing == NULL) { + root_before = snapshot_json(root); + } + + if (!case_sensitive) { + detached = cJSON_DetachItemFromObject(root, lookup_key); + } else { + detached = cJSON_DetachItemFromObjectCaseSensitive(root, lookup_key); + } + + /* Semantic/metamorphic oracle: detach must remove an existing child from its parent. */ + if (existing != NULL) { + if (detached == NULL || lookup_object_item(root, lookup_key, case_sensitive) != NULL) { + cJSON_Delete(root_before); + cJSON_Delete(detached); + abort(); + } + } else { + assert_semantic_equivalence_if_snapshotted(root_before, root); + } + + cJSON_Delete(detached); + cJSON_Delete(root_before); + free(lookup_key); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (data == NULL || size == 0) { + return 0; + } + + cJSON *root = cJSON_CreateObject(); + if (root == NULL) { + return 0; + } + + seed_object(root); + + { + uint8_t raw_ops = 0; + if (!read_u8(&data, &size, &raw_ops)) { + cJSON_Delete(root); + return 0; + } + + { + size_t op_count = (size_t)(raw_ops % 48); + + for (size_t i = 0; i < op_count && size > 0; ++i) { + uint8_t op_kind = 0; + if (!read_u8(&data, &size, &op_kind)) { + break; + } + + switch (op_kind % 5) { + case 0: + delete_from_root_object(&data, &size, root); + break; + + case 1: + delete_from_nested_object(&data, &size, root); + break; + + case 2: + delete_from_root_array(&data, &size, root); + break; + + case 3: + delete_from_nested_array(&data, &size, root); + break; + + case 4: + detach_then_delete_from_root(&data, &size, root); + break; + + default: + break; + } + + maybe_rebuild(&data, &size, root); + } + } + } + + { + char *printed = cJSON_PrintUnformatted(root); + if (printed != NULL) { + cJSON *parsed = cJSON_Parse(printed); + if (parsed != NULL) { + /* Semantic/metamorphic oracle: print/parse round-trip should preserve structure. */ + if (!cJSON_Compare(root, parsed, 1)) { + free(printed); + cJSON_Delete(parsed); + cJSON_Delete(root); + abort(); + } + cJSON_Delete(parsed); + } + free(printed); + } + } + + cJSON_Delete(root); + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/fuzzing/cjson_duplicate_fuzzer.c b/fuzzing/cjson_duplicate_fuzzer.c new file mode 100644 index 00000000..526bd5eb --- /dev/null +++ b/fuzzing/cjson_duplicate_fuzzer.c @@ -0,0 +1,782 @@ +#include +#include +#include +#include +#include + +#include "../cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static void fuzz_assert(int condition) { + if (!condition) { + abort(); + } +} + +static int read_u8(const uint8_t **data, size_t *size, uint8_t *out) { + if (*size < 1) { + return 0; + } + + *out = **data; + (*data)++; + (*size)--; + return 1; +} + +static int choose_distinct_int_value(int old_value) { + return (old_value == 0) ? 1 : 0; +} + +static char *read_bounded_string(const uint8_t **data, size_t *size, size_t max_len) { + uint8_t raw_len = 0; + size_t len = 0; + char *out = NULL; + + if (!read_u8(data, size, &raw_len)) { + return NULL; + } + + len = (size_t)(raw_len % (max_len + 1)); + if (*size < len) { + len = *size; + } + + out = (char *)malloc(len + 1); + if (out == NULL) { + return NULL; + } + + if (len > 0) { + memcpy(out, *data, len); + (*data) += len; + (*size) -= len; + } + + out[len] = '\0'; + for (size_t i = 0; i < len; ++i) { + if (out[i] == '\0') { + out[i] = 'A'; + } + } + + return out; +} + +static int keys_equal_case_insensitive(const char *left, const char *right) { + if ((left == NULL) || (right == NULL)) { + return 0; + } + + while ((*left != '\0') && (*right != '\0')) { + if (tolower((unsigned char)*left) != tolower((unsigned char)*right)) { + return 0; + } + + left++; + right++; + } + + return (*left == '\0') && (*right == '\0'); +} + +static cJSON_bool tree_has_ambiguous_object_keys(const cJSON *item) { + const cJSON *outer = NULL; + const cJSON *inner = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsObject(item)) { + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + + for (inner = outer->next; inner != NULL; inner = inner->next) { + if (keys_equal_case_insensitive(outer->string, inner->string)) { + return 1; + } + } + } + + return 0; + } + + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + } + + return 0; +} + +static cJSON *create_seed_tree(void) { + cJSON *root = NULL; + cJSON *nested = NULL; + cJSON *items = NULL; + cJSON *const_item = NULL; + + root = cJSON_CreateObject(); + if (root == NULL) { + return NULL; + } + + if (cJSON_AddNumberToObject(root, "number", 1.0) == NULL) { + goto fail; + } + if (cJSON_AddStringToObject(root, "text", "seed") == NULL) { + goto fail; + } + if (cJSON_AddBoolToObject(root, "flag", 1) == NULL) { + goto fail; + } + + nested = cJSON_AddObjectToObject(root, "nested"); + if (nested == NULL) { + goto fail; + } + if (cJSON_AddNumberToObject(nested, "value", 7.0) == NULL) { + goto fail; + } + if (cJSON_AddStringToObject(nested, "label", "alpha") == NULL) { + goto fail; + } + + items = cJSON_AddArrayToObject(root, "items"); + if (items == NULL) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateNumber(1.0))) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateString("x"))) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateFalse())) { + goto fail; + } + + const_item = cJSON_CreateNumber(9.0); + if ((const_item == NULL) || !cJSON_AddItemToObjectCS(root, "constKey", const_item)) { + cJSON_Delete(const_item); + goto fail; + } + + return root; + +fail: + cJSON_Delete(root); + return NULL; +} + +static cJSON *seed_nested_value(cJSON *root) { + cJSON *nested = NULL; + + fuzz_assert(root != NULL); + nested = cJSON_GetObjectItemCaseSensitive(root, "nested"); + fuzz_assert(nested != NULL); + return cJSON_GetObjectItemCaseSensitive(nested, "value"); +} + +static cJSON *seed_nested_label(cJSON *root) { + cJSON *nested = NULL; + + fuzz_assert(root != NULL); + nested = cJSON_GetObjectItemCaseSensitive(root, "nested"); + fuzz_assert(nested != NULL); + return cJSON_GetObjectItemCaseSensitive(nested, "label"); +} + +static void verify_roundtrip_if_safe(const cJSON *item) { + char *printed = NULL; + cJSON *parsed = NULL; + + if ((item == NULL) || tree_has_ambiguous_object_keys(item)) { + return; + } + + printed = cJSON_PrintUnformatted(item); + if (printed == NULL) { + return; + } + + parsed = cJSON_Parse(printed); + fuzz_assert(parsed != NULL); + fuzz_assert(cJSON_Compare(item, parsed, 1)); + fuzz_assert(cJSON_Compare(item, parsed, 0)); + cJSON_Delete(parsed); + free(printed); +} + +static void expect_printed_text(const cJSON *item, const char *expected) { + char *printed = NULL; + + fuzz_assert(item != NULL); + fuzz_assert(expected != NULL); + + printed = cJSON_PrintUnformatted(item); + fuzz_assert(printed != NULL); + fuzz_assert(strcmp(printed, expected) == 0); + free(printed); +} + +static void verify_null_duplicate_behavior(void) { + fuzz_assert(cJSON_Duplicate(NULL, 0) == NULL); + fuzz_assert(cJSON_Duplicate(NULL, 1) == NULL); +} + +static void verify_top_level_duplicate_metadata(const cJSON *original, + const cJSON *dup, + cJSON_bool recurse) { + fuzz_assert(original != NULL); + fuzz_assert(dup != NULL); + fuzz_assert(original != dup); + fuzz_assert(dup->next == NULL); + fuzz_assert(dup->prev == NULL); + fuzz_assert((dup->type & cJSON_IsReference) == 0); + fuzz_assert((dup->type & 0xFF) == (original->type & 0xFF)); + + if (original->valuestring != NULL) { + fuzz_assert(dup->valuestring != NULL); + fuzz_assert(strcmp(original->valuestring, dup->valuestring) == 0); + fuzz_assert(original->valuestring != dup->valuestring); + } else { + fuzz_assert(dup->valuestring == NULL); + } + + if (original->string != NULL) { + fuzz_assert(dup->string != NULL); + fuzz_assert(strcmp(original->string, dup->string) == 0); + if ((original->type & cJSON_StringIsConst) != 0) { + fuzz_assert((dup->type & cJSON_StringIsConst) != 0); + fuzz_assert(original->string == dup->string); + } else { + fuzz_assert((dup->type & cJSON_StringIsConst) == 0); + fuzz_assert(original->string != dup->string); + } + } else { + fuzz_assert(dup->string == NULL); + } + + if (cJSON_IsNumber(original)) { + fuzz_assert(dup->valueint == original->valueint); + } + + if (recurse) { + if (original->child != NULL) { + fuzz_assert(dup->child != NULL); + fuzz_assert(dup->child != original->child); + } else { + fuzz_assert(dup->child == NULL); + } + } else { + fuzz_assert(dup->child == NULL); + } +} + +static void verify_tree_distinct(const cJSON *original, const cJSON *dup) { + fuzz_assert(original != NULL); + fuzz_assert(dup != NULL); + fuzz_assert(original != dup); + fuzz_assert((dup->type & cJSON_IsReference) == 0); + fuzz_assert((dup->type & 0xFF) == (original->type & 0xFF)); + + if (original->valuestring != NULL) { + fuzz_assert(dup->valuestring != NULL); + fuzz_assert(strcmp(original->valuestring, dup->valuestring) == 0); + fuzz_assert(original->valuestring != dup->valuestring); + } else { + fuzz_assert(dup->valuestring == NULL); + } + + if (original->string != NULL) { + fuzz_assert(dup->string != NULL); + fuzz_assert(strcmp(original->string, dup->string) == 0); + if ((original->type & cJSON_StringIsConst) != 0) { + fuzz_assert((dup->type & cJSON_StringIsConst) != 0); + fuzz_assert(original->string == dup->string); + } else { + fuzz_assert((dup->type & cJSON_StringIsConst) == 0); + fuzz_assert(original->string != dup->string); + } + } else { + fuzz_assert(dup->string == NULL); + } + + if (cJSON_IsNumber(original)) { + fuzz_assert(dup->valueint == original->valueint); + } + + if (original->child != NULL) { + fuzz_assert(dup->child != NULL); + fuzz_assert(original->child != dup->child); + verify_tree_distinct(original->child, dup->child); + } else { + fuzz_assert(dup->child == NULL); + } + + if (original->next != NULL) { + fuzz_assert(dup->next != NULL); + fuzz_assert(original->next != dup->next); + verify_tree_distinct(original->next, dup->next); + } else { + fuzz_assert(dup->next == NULL); + } +} + +static int mutate_first_number_or_string(const cJSON *original, cJSON *dup) { + cJSON *string_item = NULL; + const char *replacement = NULL; + int old_value = 0; + int new_value = 0; + const char *old_text = NULL; + + fuzz_assert(original != NULL); + fuzz_assert(dup != NULL); + + if (cJSON_IsNumber(original) && cJSON_IsNumber(dup)) { + old_value = original->valueint; + new_value = choose_distinct_int_value(old_value); + + cJSON_SetNumberValue(dup, (double)new_value); + fuzz_assert(original->valueint == old_value); + fuzz_assert(dup->valueint == new_value); + return 1; + } + + if (cJSON_IsString(original) && cJSON_IsString(dup)) { + string_item = dup; + old_text = original->valuestring; + replacement = (strcmp(old_text != NULL ? old_text : "", "changed") == 0) ? "changed!" : "changed"; + + fuzz_assert(cJSON_SetValuestring(string_item, replacement) != NULL); + fuzz_assert(strcmp(original->valuestring, old_text) == 0); + fuzz_assert(strcmp(string_item->valuestring, replacement) == 0); + return 1; + } + + if ((original->child != NULL) && (dup->child != NULL)) { + if (mutate_first_number_or_string(original->child, dup->child)) { + return 1; + } + } + + if ((original->next != NULL) && (dup->next != NULL)) { + if (mutate_first_number_or_string(original->next, dup->next)) { + return 1; + } + } + + return 0; +} + +static void run_recursive_seed_scenario(const uint8_t **data, size_t *size) { + cJSON *root = NULL; + cJSON *dup = NULL; + cJSON *dup2 = NULL; + cJSON *root_number = NULL; + cJSON *dup_number = NULL; + cJSON *root_label = NULL; + cJSON *dup2_label = NULL; + char *replacement = NULL; + int root_value_before = 0; + int mutated_value = 0; + + root = create_seed_tree(); + if (root == NULL) { + return; + } + + dup = cJSON_Duplicate(root, 1); + if (dup == NULL) { + cJSON_Delete(root); + return; + } + + verify_top_level_duplicate_metadata(root, dup, 1); + verify_tree_distinct(root, dup); + fuzz_assert(cJSON_Compare(root, dup, 1)); + fuzz_assert(cJSON_Compare(root, dup, 0)); + verify_roundtrip_if_safe(root); + verify_roundtrip_if_safe(dup); + + replacement = read_bounded_string(data, size, 24); + if (replacement == NULL) { + replacement = (char *)malloc(sizeof("mutated")); + if (replacement != NULL) { + memcpy(replacement, "mutated", sizeof("mutated")); + } + } + if (replacement == NULL) { + cJSON_Delete(dup); + cJSON_Delete(root); + return; + } + + if (strcmp(replacement, "alpha") == 0) { + char *tmp = (char *)malloc(sizeof("alpha!")); + if (tmp != NULL) { + memcpy(tmp, "alpha!", sizeof("alpha!")); + free(replacement); + replacement = tmp; + } + } + + root_number = seed_nested_value(root); + dup_number = seed_nested_value(dup); + root_label = seed_nested_label(root); + + root_value_before = root_number->valueint; + mutated_value = choose_distinct_int_value(root_value_before); + cJSON_SetNumberValue(dup_number, (double)mutated_value); + fuzz_assert(root_number->valueint == root_value_before); + fuzz_assert(cJSON_SetValuestring(dup ? seed_nested_label(dup) : NULL, replacement) != NULL); + fuzz_assert(strcmp(root_label->valuestring, "alpha") == 0); + fuzz_assert(!cJSON_Compare(root, dup, 1)); + fuzz_assert(!cJSON_Compare(root, dup, 0)); + + cJSON_Delete(dup); + dup = NULL; + + dup2 = cJSON_Duplicate(root, 1); + if (dup2 != NULL) { + verify_top_level_duplicate_metadata(root, dup2, 1); + verify_tree_distinct(root, dup2); + fuzz_assert(cJSON_Compare(root, dup2, 1)); + cJSON_SetNumberValue(seed_nested_value(root), (double)mutated_value); + fuzz_assert(seed_nested_value(dup2)->valueint == root_value_before); + fuzz_assert(!cJSON_Compare(root, dup2, 1)); + + dup2_label = seed_nested_label(dup2); + fuzz_assert(strcmp(dup2_label->valuestring, "alpha") == 0); + } + + free(replacement); + cJSON_Delete(dup2); + cJSON_Delete(root); +} + +static void run_nonrecursive_seed_scenario(void) { + cJSON *root = NULL; + cJSON *dup = NULL; + + root = create_seed_tree(); + if (root == NULL) { + return; + } + + dup = cJSON_Duplicate(root, 0); + if (dup == NULL) { + cJSON_Delete(root); + return; + } + + verify_top_level_duplicate_metadata(root, dup, 0); + fuzz_assert(cJSON_IsObject(dup)); + fuzz_assert(cJSON_GetArraySize(dup) == 0); + fuzz_assert(!cJSON_Compare(root, dup, 1)); + fuzz_assert(!cJSON_Compare(root, dup, 0)); + expect_printed_text(dup, "{}"); + verify_roundtrip_if_safe(dup); + + cJSON_Delete(dup); + cJSON_Delete(root); +} + +static void run_string_scalar_scenario(const uint8_t **data, size_t *size) { + cJSON *root = NULL; + cJSON *dup = NULL; + char *payload = NULL; + + payload = read_bounded_string(data, size, 32); + if (payload == NULL) { + payload = (char *)malloc(sizeof("scalar")); + if (payload != NULL) { + memcpy(payload, "scalar", sizeof("scalar")); + } + } + if (payload == NULL) { + return; + } + + root = cJSON_CreateString(payload); + if (root == NULL) { + free(payload); + return; + } + + dup = cJSON_Duplicate(root, 0); + if (dup != NULL) { + verify_top_level_duplicate_metadata(root, dup, 0); + fuzz_assert(cJSON_Compare(root, dup, 1)); + fuzz_assert(cJSON_Compare(root, dup, 0)); + fuzz_assert(cJSON_SetValuestring(dup, (strcmp(payload, "changed") == 0) ? "changed!" : "changed") != NULL); + fuzz_assert(strcmp(root->valuestring, payload) == 0); + fuzz_assert(!cJSON_Compare(root, dup, 1)); + verify_roundtrip_if_safe(dup); + } + + cJSON_Delete(dup); + cJSON_Delete(root); + free(payload); +} + +static void run_string_reference_scenario(const uint8_t **data, size_t *size) { + cJSON *root = NULL; + cJSON *dup = NULL; + char *payload = NULL; + + payload = read_bounded_string(data, size, 32); + if (payload == NULL) { + payload = (char *)malloc(sizeof("shared")); + if (payload != NULL) { + memcpy(payload, "shared", sizeof("shared")); + } + } + if (payload == NULL) { + return; + } + + root = cJSON_CreateStringReference(payload); + if (root == NULL) { + free(payload); + return; + } + + fuzz_assert((root->type & cJSON_IsReference) != 0); + dup = cJSON_Duplicate(root, 0); + if (dup != NULL) { + verify_top_level_duplicate_metadata(root, dup, 0); + fuzz_assert((dup->type & cJSON_IsReference) == 0); + fuzz_assert(cJSON_Compare(root, dup, 1)); + fuzz_assert(cJSON_Compare(root, dup, 0)); + fuzz_assert(cJSON_SetValuestring(dup, (strcmp(payload, "changed") == 0) ? "changed!" : "changed") != NULL); + fuzz_assert(strcmp(root->valuestring, payload) == 0); + fuzz_assert(!cJSON_Compare(root, dup, 1)); + verify_roundtrip_if_safe(dup); + } + + cJSON_Delete(dup); + cJSON_Delete(root); + free(payload); +} + +static void run_const_key_scenario(void) { + cJSON *root = NULL; + cJSON *dup = NULL; + cJSON *root_item = NULL; + cJSON *dup_item = NULL; + + root = cJSON_CreateObject(); + if (root == NULL) { + return; + } + + root_item = cJSON_CreateNumber(5.0); + if ((root_item == NULL) || !cJSON_AddItemToObjectCS(root, "CONSTKEY", root_item)) { + cJSON_Delete(root_item); + cJSON_Delete(root); + return; + } + + dup = cJSON_Duplicate(root, 1); + if (dup == NULL) { + cJSON_Delete(root); + return; + } + + verify_top_level_duplicate_metadata(root, dup, 1); + verify_tree_distinct(root, dup); + fuzz_assert(cJSON_Compare(root, dup, 1)); + + root_item = cJSON_GetObjectItemCaseSensitive(root, "CONSTKEY"); + dup_item = cJSON_GetObjectItemCaseSensitive(dup, "CONSTKEY"); + fuzz_assert(root_item != NULL); + fuzz_assert(dup_item != NULL); + fuzz_assert((root_item->type & cJSON_StringIsConst) != 0); + fuzz_assert((dup_item->type & cJSON_StringIsConst) != 0); + fuzz_assert(root_item->string == dup_item->string); + fuzz_assert(root_item != dup_item); + + cJSON_SetNumberValue(dup_item, 11.0); + fuzz_assert(root_item->valueint == 5); + fuzz_assert(!cJSON_Compare(root, dup, 1)); + + cJSON_Delete(dup); + cJSON_Delete(root); +} + +static void run_circular_failure_scenario(void) { + cJSON *root = NULL; + cJSON *a = NULL; + cJSON *b = NULL; + cJSON *dup = NULL; + + root = cJSON_CreateArray(); + a = cJSON_CreateArray(); + b = cJSON_CreateArray(); + if ((root == NULL) || (a == NULL) || (b == NULL)) { + cJSON_Delete(root); + cJSON_Delete(a); + cJSON_Delete(b); + return; + } + + fuzz_assert(cJSON_AddItemToArray(root, a)); + fuzz_assert(cJSON_AddItemToArray(a, b)); + fuzz_assert(cJSON_AddItemToArray(b, root)); + + fuzz_assert(cJSON_Duplicate(root, 1) == NULL); + + dup = cJSON_Duplicate(root, 0); + if (dup != NULL) { + verify_top_level_duplicate_metadata(root, dup, 0); + fuzz_assert(cJSON_IsArray(dup)); + fuzz_assert(cJSON_GetArraySize(dup) == 0); + expect_printed_text(dup, "[]"); + verify_roundtrip_if_safe(dup); + } + + fuzz_assert(cJSON_DetachItemFromArray(b, 0) == root); + cJSON_Delete(dup); + cJSON_Delete(root); +} + +static void run_parsed_duplicate_scenario(const uint8_t *data, size_t size) { + cJSON *root = NULL; + cJSON *dup_recursive = NULL; + cJSON *dup_shallow = NULL; + char *input = NULL; + if ((data == NULL) || (size == 0)) { + return; + } + + input = (char *)malloc(size + 1); + if (input == NULL) { + return; + } + + memcpy(input, data, size); + input[size] = '\0'; + + root = cJSON_Parse(input); + free(input); + + if (root == NULL) { + return; + } + + dup_shallow = cJSON_Duplicate(root, 0); + if (dup_shallow != NULL) { + verify_top_level_duplicate_metadata(root, dup_shallow, 0); + verify_roundtrip_if_safe(dup_shallow); + if (root->child != NULL) { + fuzz_assert(dup_shallow->child == NULL); + if (cJSON_IsObject(root)) { + expect_printed_text(dup_shallow, "{}"); + } else if (cJSON_IsArray(root)) { + expect_printed_text(dup_shallow, "[]"); + } + if (!tree_has_ambiguous_object_keys(root)) { + fuzz_assert(!cJSON_Compare(root, dup_shallow, 1)); + fuzz_assert(!cJSON_Compare(root, dup_shallow, 0)); + } + } else if (!tree_has_ambiguous_object_keys(root)) { + fuzz_assert(cJSON_Compare(root, dup_shallow, 1)); + fuzz_assert(cJSON_Compare(root, dup_shallow, 0)); + } + } + + dup_recursive = cJSON_Duplicate(root, 1); + if (dup_recursive != NULL) { + verify_top_level_duplicate_metadata(root, dup_recursive, 1); + verify_tree_distinct(root, dup_recursive); + verify_roundtrip_if_safe(dup_recursive); + + if (!tree_has_ambiguous_object_keys(root)) { + fuzz_assert(cJSON_Compare(root, dup_recursive, 1)); + fuzz_assert(cJSON_Compare(root, dup_recursive, 0)); + } + + if (mutate_first_number_or_string(root, dup_recursive) && + !tree_has_ambiguous_object_keys(root)) { + fuzz_assert(!cJSON_Compare(root, dup_recursive, 1)); + } + } + + cJSON_Delete(dup_recursive); + cJSON_Delete(dup_shallow); + cJSON_Delete(root); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + const uint8_t *payload = NULL; + size_t payload_size = 0; + uint8_t raw_cases = 0; + size_t case_count = 0; + + if ((data == NULL) || (size == 0)) { + return 0; + } + + verify_null_duplicate_behavior(); + + if (!read_u8(&data, &size, &raw_cases)) { + return 0; + } + + payload = data; + payload_size = size; + case_count = (size_t)(raw_cases % 20); + + for (size_t i = 0; (i < case_count) && (size > 0); ++i) { + uint8_t opcode = 0; + + if (!read_u8(&data, &size, &opcode)) { + break; + } + + switch (opcode % 5) { + case 0: + run_recursive_seed_scenario(&data, &size); + break; + + case 1: + run_nonrecursive_seed_scenario(); + break; + + case 2: + run_string_scalar_scenario(&data, &size); + break; + + case 3: + run_string_reference_scenario(&data, &size); + break; + + case 4: + default: + if ((opcode & 1) == 0) { + run_const_key_scenario(); + } else { + run_circular_failure_scenario(); + } + break; + } + } + + if (payload_size > 0) { + run_parsed_duplicate_scenario(payload, payload_size); + } + + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/fuzzing/cjson_print_preallocated_fuzzer.c b/fuzzing/cjson_print_preallocated_fuzzer.c new file mode 100644 index 00000000..2cbce5f1 --- /dev/null +++ b/fuzzing/cjson_print_preallocated_fuzzer.c @@ -0,0 +1,606 @@ +#include +#include +#include +#include +#include +#include + +#include "../cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static void fuzz_assert(int condition) { + if (!condition) { + abort(); + } +} + +static int read_u8(const uint8_t **data, size_t *size, uint8_t *out) { + if (*size < 1) { + return 0; + } + + *out = **data; + (*data)++; + (*size)--; + return 1; +} + +static int read_bytes(const uint8_t **data, size_t *size, uint8_t *out, size_t n) { + if (*size < n) { + return 0; + } + + memcpy(out, *data, n); + (*data) += n; + (*size) -= n; + return 1; +} + +static int read_double(const uint8_t **data, size_t *size, double *out) { + union { + uint64_t u64; + double d; + } conv; + + conv.u64 = 0; + + if (*size >= sizeof(uint64_t)) { + if (!read_bytes(data, size, (uint8_t *)&conv.u64, sizeof(uint64_t))) { + return 0; + } + } else { + size_t n = *size; + + if (n > 0) { + memcpy(&conv.u64, *data, n); + (*data) += n; + (*size) -= n; + } + } + + *out = conv.d; + return 1; +} + +static char *read_bounded_string(const uint8_t **data, size_t *size, size_t max_len) { + uint8_t raw_len = 0; + size_t len = 0; + char *out = NULL; + + if (!read_u8(data, size, &raw_len)) { + return NULL; + } + + len = (size_t)(raw_len % (max_len + 1)); + if (*size < len) { + len = *size; + } + + out = (char *)malloc(len + 1); + if (out == NULL) { + return NULL; + } + + if (len > 0) { + memcpy(out, *data, len); + (*data) += len; + (*size) -= len; + } + + out[len] = '\0'; + for (size_t i = 0; i < len; ++i) { + if (out[i] == '\0') { + out[i] = 'A'; + } + } + + return out; +} + +static int keys_equal_case_insensitive(const char *left, const char *right) { + if ((left == NULL) || (right == NULL)) { + return 0; + } + + while ((*left != '\0') && (*right != '\0')) { + if (tolower((unsigned char)*left) != tolower((unsigned char)*right)) { + return 0; + } + + left++; + right++; + } + + return (*left == '\0') && (*right == '\0'); +} + +static cJSON_bool tree_has_ambiguous_object_keys(const cJSON *item) { + const cJSON *outer = NULL; + const cJSON *inner = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsObject(item)) { + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + + for (inner = outer->next; inner != NULL; inner = inner->next) { + if (keys_equal_case_insensitive(outer->string, inner->string)) { + return 1; + } + } + } + + return 0; + } + + for (outer = item->child; outer != NULL; outer = outer->next) { + if (tree_has_ambiguous_object_keys(outer)) { + return 1; + } + } + + return 0; +} + +static cJSON_bool tree_contains_raw(const cJSON *item) { + const cJSON *child = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsRaw(item)) { + return 1; + } + + for (child = item->child; child != NULL; child = child->next) { + if (tree_contains_raw(child)) { + return 1; + } + } + + return 0; +} + +static cJSON_bool tree_contains_nonfinite_number(const cJSON *item) { + const cJSON *child = NULL; + + if (item == NULL) { + return 0; + } + + if (cJSON_IsNumber(item) && !isfinite(item->valuedouble)) { + return 1; + } + + for (child = item->child; child != NULL; child = child->next) { + if (tree_contains_nonfinite_number(child)) { + return 1; + } + } + + return 0; +} + +static cJSON_bool tree_is_roundtrip_safe(const cJSON *item) { + return (item != NULL) && + !tree_contains_raw(item) && + !tree_contains_nonfinite_number(item) && + !tree_has_ambiguous_object_keys(item); +} + +static cJSON *create_seed_tree(void) { + cJSON *root = NULL; + cJSON *nested = NULL; + cJSON *items = NULL; + + root = cJSON_CreateObject(); + if (root == NULL) { + return NULL; + } + + if (cJSON_AddNumberToObject(root, "number", 1.0) == NULL) { + goto fail; + } + if (cJSON_AddStringToObject(root, "text", "seed") == NULL) { + goto fail; + } + if (cJSON_AddBoolToObject(root, "flag", 1) == NULL) { + goto fail; + } + if (cJSON_AddNullToObject(root, "empty") == NULL) { + goto fail; + } + + nested = cJSON_AddObjectToObject(root, "nested"); + if (nested == NULL) { + goto fail; + } + if (cJSON_AddStringToObject(nested, "label", "alpha") == NULL) { + goto fail; + } + if (cJSON_AddNumberToObject(nested, "value", 7.0) == NULL) { + goto fail; + } + + items = cJSON_AddArrayToObject(root, "items"); + if (items == NULL) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateNumber(1.0))) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateString("x"))) { + goto fail; + } + if (!cJSON_AddItemToArray(items, cJSON_CreateFalse())) { + goto fail; + } + + return root; + +fail: + cJSON_Delete(root); + return NULL; +} + +static cJSON *create_scalar_item(const uint8_t **data, size_t *size) { + uint8_t kind = 0; + char *payload = NULL; + double number = 0.0; + cJSON *item = NULL; + + if (!read_u8(data, size, &kind)) { + return cJSON_CreateNull(); + } + + switch (kind % 5) { + case 0: + (void)read_double(data, size, &number); + return cJSON_CreateNumber(number); + + case 1: + payload = read_bounded_string(data, size, 32); + if (payload == NULL) { + return cJSON_CreateString(""); + } + item = cJSON_CreateString(payload); + free(payload); + return item; + + case 2: + return cJSON_CreateTrue(); + + case 3: + return cJSON_CreateFalse(); + + case 4: + default: + return cJSON_CreateNull(); + } +} + +static cJSON *create_raw_item(uint8_t selector) { + static const char *raw_values[] = { + "null", + "true", + "false", + "0", + "\"raw\"", + "[]", + "{}" + }; + + return cJSON_CreateRaw(raw_values[selector % (sizeof(raw_values) / sizeof(raw_values[0]))]); +} + +static cJSON *create_number_item(const uint8_t **data, size_t *size) { + double number = 0.0; + + (void)read_double(data, size, &number); + return cJSON_CreateNumber(number); +} + +static void verify_invalid_argument_behavior(cJSON *item) { + char buffer[8]; + + memset(buffer, 'Z', sizeof(buffer)); + + fuzz_assert(!cJSON_PrintPreallocated(NULL, buffer, (int)sizeof(buffer), 0)); + fuzz_assert(!cJSON_PrintPreallocated(NULL, buffer, (int)sizeof(buffer), 1)); + fuzz_assert(!cJSON_PrintPreallocated(item, NULL, 1, 0)); + fuzz_assert(!cJSON_PrintPreallocated(item, NULL, 1, 1)); + fuzz_assert(!cJSON_PrintPreallocated(item, buffer, -1, 0)); + fuzz_assert(!cJSON_PrintPreallocated(item, buffer, -1, 1)); +} + +static void verify_roundtrip_from_text(const cJSON *item, const char *text) { + cJSON *parsed = NULL; + + fuzz_assert(item != NULL); + fuzz_assert(text != NULL); + + if (!tree_is_roundtrip_safe(item)) { + return; + } + + parsed = cJSON_Parse(text); + fuzz_assert(parsed != NULL); + fuzz_assert(cJSON_Compare(item, parsed, 1)); + fuzz_assert(cJSON_Compare(item, parsed, 0)); + cJSON_Delete(parsed); +} + +static void verify_success_case(cJSON *item, cJSON_bool format, size_t buffer_length) { + char *expected = NULL; + char *after = NULL; + unsigned char *buffer = NULL; + size_t expected_len = 0; + size_t total_len = 0; + const size_t redzone = 32; + cJSON_bool result = 0; + + fuzz_assert(item != NULL); + + expected = format ? cJSON_Print(item) : cJSON_PrintUnformatted(item); + if (expected == NULL) { + return; + } + + expected_len = strlen(expected); + fuzz_assert(buffer_length >= (expected_len + 1)); + + total_len = buffer_length + redzone; + buffer = (unsigned char *)malloc(total_len); + if (buffer == NULL) { + free(expected); + return; + } + + memset(buffer, 0xA5, total_len); + + result = cJSON_PrintPreallocated(item, (char *)buffer, (int)buffer_length, format); + fuzz_assert(result); + fuzz_assert(strcmp((const char *)buffer, expected) == 0); + fuzz_assert(buffer[expected_len] == '\0'); + + for (size_t i = buffer_length; i < total_len; ++i) { + fuzz_assert(buffer[i] == 0xA5); + } + + after = format ? cJSON_Print(item) : cJSON_PrintUnformatted(item); + fuzz_assert(after != NULL); + fuzz_assert(strcmp(expected, after) == 0); + verify_roundtrip_from_text(item, (const char *)buffer); + + free(after); + free(buffer); + free(expected); +} + +static void verify_too_small_failure(cJSON *item, cJSON_bool format) { + char *expected = NULL; + char *after = NULL; + unsigned char *buffer = NULL; + size_t expected_len = 0; + size_t buffer_length = 0; + size_t total_len = 0; + const size_t redzone = 32; + cJSON_bool result = 0; + + fuzz_assert(item != NULL); + + expected = format ? cJSON_Print(item) : cJSON_PrintUnformatted(item); + if (expected == NULL) { + return; + } + + expected_len = strlen(expected); + if (expected_len == 0) { + free(expected); + return; + } + + buffer_length = expected_len; + total_len = buffer_length + redzone; + buffer = (unsigned char *)malloc(total_len); + if (buffer == NULL) { + free(expected); + return; + } + + memset(buffer, 0x5A, total_len); + result = cJSON_PrintPreallocated(item, (char *)buffer, (int)buffer_length, format); + fuzz_assert(!result); + + for (size_t i = buffer_length; i < total_len; ++i) { + fuzz_assert(buffer[i] == 0x5A); + } + + after = format ? cJSON_Print(item) : cJSON_PrintUnformatted(item); + fuzz_assert(after != NULL); + fuzz_assert(strcmp(expected, after) == 0); + + free(after); + free(buffer); + free(expected); +} + +static void verify_zero_length_failure(cJSON *item, cJSON_bool format) { + unsigned char buffer[32]; + + fuzz_assert(item != NULL); + + memset(buffer, 0xCC, sizeof(buffer)); + fuzz_assert(!cJSON_PrintPreallocated(item, (char *)buffer, 0, format)); + for (size_t i = 0; i < sizeof(buffer); ++i) { + fuzz_assert(buffer[i] == 0xCC); + } +} + +static void verify_exact_output_oracles(cJSON *item) { + char *formatted = NULL; + char *unformatted = NULL; + + fuzz_assert(item != NULL); + + verify_invalid_argument_behavior(item); + + formatted = cJSON_Print(item); + unformatted = cJSON_PrintUnformatted(item); + if ((formatted == NULL) || (unformatted == NULL)) { + free(formatted); + free(unformatted); + return; + } + + verify_success_case(item, 0, strlen(unformatted) + 5); + verify_success_case(item, 0, strlen(unformatted) + 17); + verify_success_case(item, 1, strlen(formatted) + 5); + verify_success_case(item, 1, strlen(formatted) + 17); + + verify_too_small_failure(item, 0); + verify_too_small_failure(item, 1); + verify_zero_length_failure(item, 0); + verify_zero_length_failure(item, 1); + + free(formatted); + free(unformatted); +} + +static void run_seed_tree_scenario(void) { + cJSON *root = create_seed_tree(); + + if (root == NULL) { + return; + } + + verify_exact_output_oracles(root); + cJSON_Delete(root); +} + +static void run_scalar_scenario(const uint8_t **data, size_t *size) { + cJSON *item = create_scalar_item(data, size); + + if (item == NULL) { + return; + } + + verify_exact_output_oracles(item); + cJSON_Delete(item); +} + +static void run_raw_scenario(uint8_t selector) { + cJSON *item = create_raw_item(selector); + + if (item == NULL) { + return; + } + + verify_exact_output_oracles(item); + cJSON_Delete(item); +} + +static void run_number_scenario(const uint8_t **data, size_t *size) { + cJSON *item = create_number_item(data, size); + + if (item == NULL) { + return; + } + + verify_exact_output_oracles(item); + cJSON_Delete(item); +} + +static void run_parsed_tree_scenario(const uint8_t *data, size_t size) { + cJSON *root = NULL; + char *input = NULL; + + if ((data == NULL) || (size == 0)) { + return; + } + + input = (char *)malloc(size + 1); + if (input == NULL) { + return; + } + + memcpy(input, data, size); + input[size] = '\0'; + + root = cJSON_Parse(input); + free(input); + + if (root == NULL) { + return; + } + + verify_exact_output_oracles(root); + cJSON_Delete(root); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + const uint8_t *payload = NULL; + size_t payload_size = 0; + uint8_t raw_cases = 0; + size_t case_count = 0; + + if ((data == NULL) || (size == 0)) { + return 0; + } + + if (!read_u8(&data, &size, &raw_cases)) { + return 0; + } + + payload = data; + payload_size = size; + case_count = (size_t)(raw_cases % 20); + + for (size_t i = 0; (i < case_count) && (size > 0); ++i) { + uint8_t opcode = 0; + uint8_t selector = 0; + + if (!read_u8(&data, &size, &opcode)) { + break; + } + + switch (opcode % 4) { + case 0: + run_seed_tree_scenario(); + break; + + case 1: + run_scalar_scenario(&data, &size); + break; + + case 2: + run_number_scenario(&data, &size); + break; + + case 3: + default: + (void)read_u8(&data, &size, &selector); + run_raw_scenario(selector); + break; + } + } + + if (payload_size > 0) { + run_parsed_tree_scenario(payload, payload_size); + } + + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/fuzzing/cjson_replace_item_in_object_fuzzer.c b/fuzzing/cjson_replace_item_in_object_fuzzer.c new file mode 100644 index 00000000..7c36db30 --- /dev/null +++ b/fuzzing/cjson_replace_item_in_object_fuzzer.c @@ -0,0 +1,309 @@ +#include +#include +#include +#include + +#include "../cJSON.h" + +#ifdef __cplusplus +extern "C" { +#endif + +static int read_u8(const uint8_t **data, size_t *size, uint8_t *out) { + if (*size < 1) { + return 0; + } + *out = **data; + (*data)++; + (*size)--; + return 1; +} + +static int read_bytes(const uint8_t **data, size_t *size, uint8_t *out, size_t n) { + if (*size < n) { + return 0; + } + memcpy(out, *data, n); + (*data) += n; + (*size) -= n; + return 1; +} + +static char *read_key(const uint8_t **data, size_t *size) { + uint8_t raw_len = 0; + if (!read_u8(data, size, &raw_len)) { + return NULL; + } + + size_t key_len = (size_t)(raw_len % 64); + if (*size < key_len) { + key_len = *size; + } + + char *key = (char *)malloc(key_len + 1); + if (key == NULL) { + return NULL; + } + + if (key_len > 0) { + memcpy(key, *data, key_len); + (*data) += key_len; + (*size) -= key_len; + } + key[key_len] = '\0'; + + for (size_t i = 0; i < key_len; ++i) { + if (key[i] == '\0') { + key[i] = 'A'; + } + } + + return key; +} + +static int read_double(const uint8_t **data, size_t *size, double *out) { + union { + uint64_t u64; + double d; + } conv; + + conv.u64 = 0; + + if (*size >= sizeof(uint64_t)) { + if (!read_bytes(data, size, (uint8_t *)&conv.u64, sizeof(uint64_t))) { + return 0; + } + } else { + size_t n = *size; + if (n > 0) { + memcpy(&conv.u64, *data, n); + (*data) += n; + (*size) -= n; + } + } + + *out = conv.d; + return 1; +} + +static void mutate_case(char *s, uint8_t mode) { + if (s == NULL) { + return; + } + + for (size_t i = 0; s[i] != '\0'; ++i) { + unsigned char c = (unsigned char)s[i]; + if (c >= 'a' && c <= 'z') { + if (mode == 1 || (mode == 2 && (i % 2 == 0))) { + s[i] = (char)(c - 'a' + 'A'); + } + } else if (c >= 'A' && c <= 'Z') { + if (mode == 3 || (mode == 2 && (i % 2 == 1))) { + s[i] = (char)(c - 'A' + 'a'); + } + } + } +} + +static cJSON *lookup_object_item(cJSON *object, const char *key, int case_sensitive) { + if (case_sensitive) { + return cJSON_GetObjectItemCaseSensitive(object, key); + } + return cJSON_GetObjectItem(object, key); +} + +static cJSON *make_leaf_item(const uint8_t **data, size_t *size, uint8_t kind) { + switch (kind % 4) { + case 0: { + double number = 0.0; + (void)read_double(data, size, &number); + return cJSON_CreateNumber(number); + } + case 1: { + char *s = read_key(data, size); + cJSON *item = cJSON_CreateString((s != NULL) ? s : ""); + free(s); + return item; + } + case 2: { + uint8_t b = 0; + (void)read_u8(data, size, &b); + return cJSON_CreateBool((b & 1) ? 1 : 0); + } + case 3: + default: + return cJSON_CreateNull(); + } +} + +static cJSON *make_replacement_item(const uint8_t **data, size_t *size, int depth) { + uint8_t kind = 0; + if (!read_u8(data, size, &kind)) { + return cJSON_CreateNull(); + } + + if (depth <= 0) { + return make_leaf_item(data, size, kind); + } + + switch (kind % 6) { + case 0: + case 1: + case 2: + case 3: + return make_leaf_item(data, size, kind); + + case 4: { + cJSON *arr = cJSON_CreateArray(); + if (arr == NULL) { + return NULL; + } + + uint8_t count = 0; + (void)read_u8(data, size, &count); + size_t n = (size_t)(count % 4); + + for (size_t i = 0; i < n; ++i) { + cJSON *elem = make_replacement_item(data, size, depth - 1); + if (elem == NULL || !cJSON_AddItemToArray(arr, elem)) { + cJSON_Delete(elem); + cJSON_Delete(arr); + return NULL; + } + } + return arr; + } + + case 5: + default: { + cJSON *obj = cJSON_CreateObject(); + if (obj == NULL) { + return NULL; + } + + uint8_t count = 0; + (void)read_u8(data, size, &count); + size_t n = (size_t)(count % 3); + + for (size_t i = 0; i < n; ++i) { + char *k = read_key(data, size); + if (k == NULL) { + break; + } + + cJSON *val = make_replacement_item(data, size, depth - 1); + if (val == NULL || !cJSON_AddItemToObject(obj, k, val)) { + cJSON_Delete(val); + free(k); + cJSON_Delete(obj); + return NULL; + } + free(k); + } + return obj; + } + } +} + +static void seed_object(cJSON *root) { + cJSON_AddNumberToObject(root, "alpha", 1.0); + cJSON_AddNumberToObject(root, "Bravo", 2.0); + cJSON_AddNumberToObject(root, "CHARLIE", 3.0); + cJSON_AddStringToObject(root, "delta", "x"); + cJSON_AddBoolToObject(root, "Echo", 1); + cJSON_AddNullToObject(root, "foxtrot"); +} + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + if (data == NULL || size == 0) { + return 0; + } + + cJSON *root = cJSON_CreateObject(); + if (root == NULL) { + return 0; + } + + seed_object(root); + + uint8_t raw_ops = *data++; + size--; + + size_t op_count = (size_t)(raw_ops % 32); + + for (size_t i = 0; i < op_count && size > 0; ++i) { + char *lookup_key = read_key(&data, &size); + int case_sensitive = 0; + cJSON *existing = NULL; + int size_before = 0; + int replace_ok = 0; + int size_after = 0; + cJSON *after_item = NULL; + if (lookup_key == NULL) { + break; + } + + uint8_t case_mode = 0; + (void)read_u8(&data, &size, &case_mode); + mutate_case(lookup_key, (uint8_t)(case_mode % 4)); + + cJSON *replacement = make_replacement_item(&data, &size, 1); + if (replacement == NULL) { + free(lookup_key); + break; + } + + uint8_t which_api = 0; + (void)read_u8(&data, &size, &which_api); + case_sensitive = (which_api & 1) != 0; + existing = lookup_object_item(root, lookup_key, case_sensitive); + size_before = cJSON_GetArraySize(root); + + if (!case_sensitive) { + replace_ok = cJSON_ReplaceItemInObject(root, lookup_key, replacement); + if (!replace_ok) { + cJSON_Delete(replacement); + } + } else { + replace_ok = cJSON_ReplaceItemInObjectCaseSensitive(root, lookup_key, replacement); + if (!replace_ok) { + cJSON_Delete(replacement); + } + } + + size_after = cJSON_GetArraySize(root); + after_item = lookup_object_item(root, lookup_key, case_sensitive); + + if (existing != NULL) { + /* Semantic/metamorphic oracle: successful replace preserves size and keeps the key reachable. */ + if (!replace_ok || size_after != size_before || after_item == NULL) { + free(lookup_key); + abort(); + } + } else { + /* Semantic/metamorphic oracle: replacing a missing key should fail without changing object size. */ + if (replace_ok || size_after != size_before) { + free(lookup_key); + abort(); + } + } + + free(lookup_key); + } + + char *printed = cJSON_PrintUnformatted(root); + if (printed != NULL) { + cJSON *parsed = cJSON_Parse(printed); + if (parsed != NULL) { + cJSON_Delete(parsed); + } + free(printed); + } + + cJSON_Delete(root); + return 0; +} + +#ifdef __cplusplus +} +#endif diff --git a/fuzzing/ossfuzz.sh b/fuzzing/ossfuzz.sh index a2da64bf..d1ffdadd 100755 --- a/fuzzing/ossfuzz.sh +++ b/fuzzing/ossfuzz.sh @@ -5,14 +5,33 @@ mkdir build cd build + cmake -DBUILD_SHARED_LIBS=OFF -DENABLE_CJSON_TEST=OFF .. -make -j$(nproc) +make -j"$(nproc)" + +FUZZERS=( + cjson_read_fuzzer + cjson_create_fuzzer + cjson_add_to_object_fuzzer + cjson_delete_item_from_object_fuzzer + cjson_replace_item_in_object_fuzzer + cjson_duplicate_fuzzer + cjson_compare_fuzzer + cjson_print_preallocated_fuzzer +) -$CXX $CXXFLAGS $SRC/cjson/fuzzing/cjson_read_fuzzer.c -I. \ - -o $OUT/cjson_read_fuzzer \ - $LIB_FUZZING_ENGINE $SRC/cjson/build/libcjson.a +for fuzzer in "${FUZZERS[@]}"; do + $CXX $CXXFLAGS "$SRC/cjson/fuzzing/${fuzzer}.c" -I. \ + -o "$OUT/${fuzzer}" \ + $LIB_FUZZING_ENGINE "$SRC/cjson/build/libcjson.a" -find $SRC/cjson/fuzzing/inputs -name "*" | \ - xargs zip $OUT/cjson_read_fuzzer_seed_corpus.zip + if [ -d "$SRC/cjson/fuzzing/inputs" ] && \ + [ -n "$(find "$SRC/cjson/fuzzing/inputs" -type f -print -quit)" ]; then + find "$SRC/cjson/fuzzing/inputs" -type f | \ + zip -q -@ "$OUT/${fuzzer}_seed_corpus.zip" + fi -cp $SRC/cjson/fuzzing/json.dict $OUT/cjson_read_fuzzer.dict + if [ -f "$SRC/cjson/fuzzing/json.dict" ]; then + cp "$SRC/cjson/fuzzing/json.dict" "$OUT/${fuzzer}.dict" + fi +done \ No newline at end of file