A minor OOM issue in harness fuzzing/cjson_read_fuzzer.c

Open Beginner friendly
#992 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
1/5
Estimated time
Under an hour
Newbie friendliness
70/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Stale
Tech stack
c
Domain
testing-qa

Research direction

Open fuzzing/cjson_read_fuzzer.c and inspect the minify branch around the malloc failure at line 59. Reproduce the failure with the malloc-wrapping Valgrind harness described in the issue, then verify that the parsed tree is released and Valgrind reports no leaks.

Written by the indexing model from the issue text.

Description

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;
}
Dominant language
C
Stars
13k
Forks
3.5k
PR merge metrics
No merged PRs in 30d

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from DaveGamble/cJSON

All issues in DaveGamble/cJSON

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.