Skip to content

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

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;
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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