-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Open
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 pathcJSON_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_leakOriginal (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;
}Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels