Skip to content

Fix Heap-Use-After-Free in cJSONUtils_MergePatch via Recursive Deletion - #1092

Open
PGZXB wants to merge 1 commit into
DaveGamble:masterfrom
PGZXB:fix-issue-1022
Open

PGZXB wants to merge 1 commit into
DaveGamble:masterfrom
PGZXB:fix-issue-1022

Conversation

@PGZXB

@PGZXB PGZXB commented Sep 24, 2026 •

Copy link
Copy Markdown

Fixes #1022

Fixes the memory-safety issue reported in #1022: Heap-Use-After-Free in cJSONUtils_MergePatch via Recursive Deletion.

Fix

Adds the missing bounds/validity check at the faulting site.

diff --git a/cJSON_Utils.c b/cJSON_Utils.c
index 4f3a9ed..be618ab 100644
--- a/cJSON_Utils.c
+++ b/cJSON_Utils.c
@@ -1318,6 +1318,23 @@ CJSON_PUBLIC(void) cJSONUtils_SortObjectCaseSensitive(cJSON * const object)
     sort_object(object, true);
 }
 
+/* Move the contents of 'replacement' into the caller-visible node 'target'
+ * and free the old contents of 'target'. The address of 'target' is kept
+ * stable so that a caller holding the original pointer never ends up with a
+ * dangling pointer when merge_patch has to replace a non-object target. */
+static void merge_patch_adopt(cJSON *target, cJSON *replacement)
+{
+    cJSON old_target = *target;
+
+    *target = *replacement;
+    *replacement = old_target;
+    /* The old contents are now owned by the throwaway node; clear its list
+     * links so that freeing it cannot drag in unrelated nodes. */
+    replacement->next = NULL;
+    replacement->prev = NULL;
+    cJSON_Delete(replacement);
+}
+
 static cJSON *merge_patch(cJSON *target, const cJSON * const patch, const cJSON_bool case_sensitive)
 {
     cJSON *patch_child = NULL;
@@ -1329,14 +1346,37 @@ static cJSON *merge_patch(cJSON *target, const cJSON * const patch, const cJSON_
          * otherwise cJSON_Delete(target) would free the patch memory
          * and the subsequent cJSON_Duplicate would read freed memory. */
         cJSON *duplicate = cJSON_Duplicate(patch, 1);
-        cJSON_Delete(target);
-        return duplicate;
+        if (duplicate == NULL)
+        {
+            cJSON_Delete(target);
+            return NULL;
+        }
+        if (target == NULL)
+        {
+            return duplicate;
+        }
+        /* Reuse the caller's node instead of freeing it, so callers that
+         * still hold the original pointer don't read freed memory. */
+        merge_patch_adopt(target, duplicate);
+        return target;
     }
 
     if (!cJSON_IsObject(target))
     {
-        cJSON_Delete(target);
-        target = cJSON_CreateObject();
+        cJSON *new_target = cJSON_CreateObject();
+        if (new_target == NULL)
+        {
+            return NULL;
+        }
+        if (target == NULL)
+        {
+            target = new_target;
+        }
+        else
+        {
+            /* Reuse the caller's node so that its pointer stays valid. */
+            merge_patch_adopt(target, new_target);
+        }
     }
 
     patch_child = patch->child;

Verification Before Fix

ead T0 here:
    #0 0x560dfff45f3a in free (/new_issue/poc_driver+0xc6f3a) (BuildId: 6d5c7c016a67e3f3b31e9bf1af690309f1dc02f9)
    #1 0x560dfff89c95 in cJSON_Delete /src/cjson/cJSON.c:273:9
    #2 0x560dfff88ec0 in merge_patch /src/cjson/cJSON_Utils.c:1338:9
    #3 0x560dfff89110 in cJSONUtils_MergePatch /src/cjson/cJSON_Utils.c:1387:12
    #4 0x560dfff847bb in main /new_issue/poc/issue_repro.c:7:5
    #5 0x7f6324c6d1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
    #6 0x7f6324c6d28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
    #7 0x560dffeab384 in _start (/new_issue/poc_driver+0x2c384) (BuildId: 6d5c7c016a67e3f3b31e9bf1af690309f1dc02f9)

previously allocated by thread T0 here:
    #0 0x560dfff461d3 in malloc (/new_issue/poc_driver+0xc71d3) (BuildId: 6d5c7c016a67e3f3b31e9bf1af690309f1dc02f9)
    #1 0x560dfff89a1c in cJSON_New_Item /src/cjson/cJSON.c:243:27
    #2 0x560dfff93373 in cJSON_CreateString /src/cjson/cJSON.c:2533:19
    #3 0x560dfff8478a in main /new_issue/poc/issue_repro.c:4:21
    #4 0x7f6324c6d1c9  (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
    #5 0x7f6324c6d28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: a4a7992a8e66555c8141ab2a08a8465ff6e0ea65)
    #6 0x560dffeab384 in _start (/new_issue/poc_driver+0x2c384) (BuildId: 6d5c7c016a67e3f3b31e9bf1af690309f1dc02f9)

SUMMARY: AddressSanitizer: heap-use-after-free /src/cjson/cJSON.c:2036:11 in add_item_to_array
Shadow bytes around the buggy address:
  0x505ffffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x505ffffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x505fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x505fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x506000000000: fa fa fa fa 00 00 00 00 00 00 07 fa fa fa fa fa
=>0x506000000080: fd fd[fd]fd fd fd fd fd fa fa fa fa 00 00 00 00
  0x506000000100: 00 00 00 00 fa fa fa fa 00 00 00 00 00 00 00 00
  0x506000000180: fa fa fa fa 00 00 00 00 00 00 00 00 fa fa fa fa
  0x506000000200: 00 00 00 00 00 00 00 00 fa fa fa fa 00 00 00 00
  0x506000000280: 00 00 00 00 fa fa fa fa fa fa fa fa fa fa fa fa
  0x506000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==37==ABORTING

Verification After Fix

The reproducer no longer triggers a sanitizer after applying the patch.

@PGZXB PGZXB changed the title Fix Heap-Use-After-Free in cJSONUtils_MergePatch via Recursive Deletion (#1022) Fix Heap-Use-After-Free in cJSONUtils_MergePatch via Recursive Deletion Sep 24, 2026
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.

Heap-Use-After-Free in cJSONUtils_MergePatch via Recursive Deletion

1 participant