Skip to content

A minor OOM issue in harness fuzzing/cjson_read_fuzzer.c #992

@zchengchen

Description

@zchengchen

Summary

The file fuzzing/cjson_read_fuzzer.c in DaveGamble/cJSON leaks the parsed cJSON tree when malloc() fails in the minify block. Line 60 returns without calling cJSON_Delete(json).

Bug Description

Line 59–60 problem:

if(minify)
{
    copied = (unsigned char*)malloc(size);
    if(copied == NULL) return 0;  // BUG: json leaked — cJSON_Delete never called
    // ...
}

cJSON_Delete(json);  // never reached on the above path

cJSON_ParseWithOpts() succeeds at line 34, allocating a full JSON tree. When minify == 1 and malloc(size) returns NULL at line 59, the function exits immediately. The only cJSON_Delete(json) call is at line 69, which is skipped entirely. The entire parse tree (root node + children + strings) is leaked.

Evidence

PoC uses gcc -Wl,--wrap=malloc to inject NULL on the exact malloc call at line 59, then runs under Valgrind:

git clone --depth 1 https://github.com/DaveGamble/cJSON.git && cd cJSON
# place poc_leak.c in repo root (see below)
gcc -g -O0 poc_leak.c cJSON.c -lm -Wl,--wrap=malloc -o poc_leak
valgrind --leak-check=full ./poc_leak

Original (buggy):

==2987987== HEAP SUMMARY:
==2987987==     in use at exit: 140 bytes in 4 blocks
==2987987==   total heap usage: 20 allocs, 16 frees, 1,276 bytes allocated
==2987987==
==2987987== 140 (64 direct, 76 indirect) bytes in 1 blocks are definitely lost in loss record 4 of 4
==2987987==    at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==2987987==    by 0x109369: __wrap_malloc (poc_valgrind.c:36)
==2987987==    by 0x109D7F: cJSON_New_Item (cJSON.c:243)
==2987987==    by 0x10B54C: cJSON_ParseWithLengthOpts (cJSON.c:1161)
==2987987==    by 0x10B46C: cJSON_ParseWithOpts (cJSON.c:1138)
==2987987==    by 0x1094CD: original_fuzzer (poc_valgrind.c:60)
==2987987==    by 0x109994: main (poc_valgrind.c:192)
==2987987==
==2987987== LEAK SUMMARY:
==2987987==    definitely lost: 64 bytes in 1 blocks
==2987987==    indirectly lost: 76 bytes in 3 blocks
==2987987==      possibly lost: 0 bytes in 0 blocks
==2987987==    still reachable: 0 bytes in 0 blocks
==2987987==         suppressed: 0 bytes in 0 blocks
==2987987==
==2987987== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Fixed (same malloc failure injected):

==2988135== HEAP SUMMARY:
==2988135==     in use at exit: 0 bytes in 0 blocks
==2988135==   total heap usage: 14 allocs, 14 frees, 864 bytes allocated
==2988135==
==2988135== All heap blocks were freed -- no leaks are possible
==2988135==
==2988135== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Metric Original Fixed
allocs / frees 20 / 16 14 / 14
definitely lost 64 bytes 0 bytes
indirectly lost 76 bytes 0 bytes

Fix

--- a/fuzzing/cjson_read_fuzzer.c
+++ b/fuzzing/cjson_read_fuzzer.c
@@ -57,7 +57,11 @@
     if(minify)
     {
         copied = (unsigned char*)malloc(size);
-        if(copied == NULL) return 0;
+        if(copied == NULL)
+        {
+            cJSON_Delete(json);
+            return 0;
+        }

         memcpy(copied, data, size);
poc_leak.c (click to expand)
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "cJSON.h"

extern void *__real_malloc(size_t size);
static int g_malloc_count = 0;
static int g_fail_at = -1;

void *__wrap_malloc(size_t size)
{
    g_malloc_count++;
    if (g_fail_at > 0 && g_malloc_count == g_fail_at) {
        fprintf(stderr, "  [wrap] malloc #%d (size=%zu) -> NULL\n",
                g_malloc_count, size);
        return NULL;
    }
    return __real_malloc(size);
}

static int buggy_fuzzer(const uint8_t* data, size_t size)
{
    cJSON *json;
    size_t offset = 4;
    unsigned char *copied;
    char *printed_json = NULL;
    int minify, require_termination, formatted, buffered;

    if(size <= offset) return 0;
    if(data[size-1] != '\0') return 0;
    if(data[0] != '1' && data[0] != '0') return 0;
    if(data[1] != '1' && data[1] != '0') return 0;
    if(data[2] != '1' && data[2] != '0') return 0;
    if(data[3] != '1' && data[3] != '0') return 0;

    minify              = data[0] == '1' ? 1 : 0;
    require_termination = data[1] == '1' ? 1 : 0;
    formatted           = data[2] == '1' ? 1 : 0;
    buffered            = data[3] == '1' ? 1 : 0;

    json = cJSON_ParseWithOpts((const char*)data + offset, NULL, require_termination);
    if(json == NULL) return 0;

    if(buffered)
        printed_json = cJSON_PrintBuffered(json, 1, formatted);
    else if(formatted)
        printed_json = cJSON_Print(json);
    else
        printed_json = cJSON_PrintUnformatted(json);

    if(printed_json != NULL) free(printed_json);

    if(minify)
    {
        copied = (unsigned char*)malloc(size);
        if(copied == NULL) return 0;  /* BUG */
        memcpy(copied, data, size);
        cJSON_Minify((char*)copied + offset);
        free(copied);
    }

    cJSON_Delete(json);
    return 0;
}

int main(void)
{
    const char payload[] = "{\"key\":\"value\"}";
    size_t total = 4 + strlen(payload) + 1;
    uint8_t *input = (uint8_t *)__real_malloc(total);
    memcpy(input, "1000", 4);
    memcpy(input + 4, payload, strlen(payload));
    input[total - 1] = '\0';

    /* dry run to count mallocs */
    g_fail_at = -1; g_malloc_count = 0;
    buggy_fuzzer(input, total);
    int n = g_malloc_count;

    /* fail the last malloc (line 59) */
    g_fail_at = n; g_malloc_count = 0;
    buggy_fuzzer(input, total);

    free(input);
    return 0;
}

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