Skip to content

Fix: heap-use-after-free in merge_patch when target is not an object and contains patch (Variant of what #1060 fixed) - #1109

Open
Oculytic wants to merge 1 commit into
DaveGamble:masterfrom
Oculytic:master
Open

Oculytic wants to merge 1 commit into
DaveGamble:masterfrom
Oculytic:master

Conversation

@Oculytic

@Oculytic Oculytic commented Oct 19, 2026 •

Copy link
Copy Markdown

When cJSONUtils_MergePatch(target, patch) is called with an object patch that is a subtree of a non-object target (for example, target is the array [{"a":1}] and patch is its element), merge_patch() called cJSON_Delete(target), which also freed patch, and then read patch->child, triggering a heap-use-after-free.

This is the same situation as #1060, fixed in 6d9f244 for non-object patches; that fix did not cover the branch where the patch is an object and the target is not.

Fix: in that branch, duplicate the patch and create the new object before deleting target, then merge from the copy into the new object and free the copy. On allocation failure, free both and return NULL without touching target.

Reproducer:

    cJSON *patch = target->child;
    cJSON *out = cJSONUtils_MergePatch(target, patch);```

…and contains patch

When cJSONUtils_MergePatch(target, patch) is called with an object
patch that is a subtree of a non-object target (for example, target
is the array [{"a":1}] and patch is its element), merge_patch() called
cJSON_Delete(target), which also freed patch, and then read
patch->child, triggering a heap-use-after-free.

This is the same situation as DaveGamble#1060, fixed in 6d9f244 for non-object
patches; that fix did not cover the branch where the patch is an
object and the target is not.

Fix: in that branch, duplicate the patch and create the new object
before deleting target, then merge from the copy into the new object
and free the copy. On allocation failure, free both and return NULL
without touching target.

Reproducer:
    cJSON *target = cJSON_Parse("[{\"a\":1}]");
    cJSON *patch = target->child;
    cJSON *out = cJSONUtils_MergePatch(target, patch);
@w4ffl35

w4ffl35 commented Oct 26, 2026

Copy link
Copy Markdown

Independent validation: I checked out this PR head and ran an ASan reproducer for the exact variant (MergePatch with an object patch that is a subtree of an array target) -- ASan-clean, correct result {"a":1}, and the full ctest suite is 19/19 green. The duplicate-before-delete approach matches the merged #1065 pattern. Looks correct.

One suggestion: this has no regression test, so the variant could silently regress. The test from the #1065 follow-up pattern would be (tests/old_utils_tests.c):

cJSON *target = cJSON_Parse("[1, 2, {"a": 1}]");
cJSON patch = target->child->next->next;
result = cJSONUtils_MergePatch(target, patch);
/
expect non-NULL object result with a == 1 */

Happy to submit that as a companion PR if the author prefers.

@Oculytic

Copy link
Copy Markdown
Author

@w4ffl35 I'm happy with that too, thank you

@w4ffl35

w4ffl35 commented Oct 27, 2026

Copy link
Copy Markdown

Companion regression test opened as #1119 (from Capsize-Games/cJSON, test-only change to tests/old_utils_tests.c). Verified red on master at 6d9f244 (segfault, plain and ASan+UBSan) and green with this PR applied (7/7). Happy to rebase once this lands.

w4ffl35 added a commit to Capsize-Games/cJSON that referenced this pull request Oct 27, 2026
…026-29036)

decode_pointer_inplace advanced the write pointer on every loop iteration
but never copied passthrough bytes down, so any escape not at end of token
truncated the decoded name ('/a~1b' became 'a~'). Worse, the old code wrote
'/' to decoded[1] instead of decoded[0] for ~1. JSON Patch add/remove/replace
then targeted wrong keys or failed, enabling silent data corruption and
wrong-key deletion via crafted ~0/~1 sequences.

Rewrite compacts properly: escapes collapse two bytes into one, all other
bytes copied down; decoded never overtakes the read cursor. Invalid escapes
still abort as before. Regression cases 16/17 in cjson-utils-tests.json.

Red/green: new cases fail before (apply error 13), all 3 patch suites pass
after; full ctest 21/22 (old_utils_tests fails only on the unrelated
not-yet-merged DaveGamble#1109 variant, identical before/after this change).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants