Skip to content

Fix use-after-free in cJSONUtils_SortObject: restore child->prev tail invariant - #1091

Open
nvnzno-a11y wants to merge 1 commit into
DaveGamble:masterfrom
nvnzno-a11y:fix/sort-list-prev-tail-invariant
Open

nvnzno-a11y wants to merge 1 commit into
DaveGamble:masterfrom
nvnzno-a11y:fix/sort-list-prev-tail-invariant

Conversation

@nvnzno-a11y

Copy link
Copy Markdown

Issue

Fixes #1090 — use-after-free (8-byte pointer write) reported by ReisterJ with a minimal three-call PoC (cJSONUtils_SortObject → cJSON_DeleteItemFromObject → cJSON_AddItemToObject).

Root cause

sort_list() rebuilds an object's doubly linked child list with mergesort but never restores cJSON's list invariant that the head's prev pointer references the list tail. The parser establishes it (head->prev = current_item in cJSON.c) and add_item_to_array() consumes it via suffix_object(child->prev, item) for O(1) appends. After a sort, child->prev is stale — NULL or a live interior node. Deleting that interior node leaves child->prev dangling, and the next append writes prev->next = item into freed heap memory (and silently drops the new item when prev is NULL).

Fix

After the merge completes, walk to the end of the merged list once and set result->prev to the tail. One extra O(n) pass per sort_list call, negligible next to the O(n log n) sort itself. All other prev links are already maintained by the merge.

Verification

  • Reporter's PoC on current master (6d9f244): child->prev no longer equals the tail after cJSONUtils_SortObject, and a custom allocator harness (poison-on-free) observes the stale write into the freed node. On the patched build the invariant holds, the appended item lands at the tail, and no write touches a freed block.
  • New regression test sort_object_should_restore_prev_tail_invariant in tests/misc_utils_tests.c checks child->prev == tail after sorting and after a delete+add sequence. It fails on the unpatched build and passes with the fix.
  • Additional standalone battery verified sort correctness and full prev-chain integrity (forward and backward walks) across 10 cases: sorted/reversed/duplicate keys, case-sensitive and case-insensitive, nested objects, and append-after-sort.

The mergesort in sort_list() rebuilds the object's child list but never
restores cJSON's invariant that the head's prev pointer references the
list tail. After cJSONUtils_SortObject(), child->prev is either NULL or
a live interior node. Deleting that node leaves child->prev dangling,
and the next add_item_to_array() then performs suffix_object() ->
prev->next = item, an 8-byte write into freed heap memory (also drops
the appended item). Fixes DaveGamble#1090.
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.

[BUG] Use-After-Free (8-byte pointer write) in cJSON_Utils.c: sort_list() breaks the child->prev invariant

1 participant