A minor OOM issue in harness fuzzing/cjson_read_fuzzer.c
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from DaveGamble/cJSON
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
DaveGamble/cJSON#1082 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
DaveGamble/cJSON#1081 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
DaveGamble/cJSON#1074 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
DaveGamble/cJSON#1071 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
DaveGamble/cJSON#1067 ·
All issues in DaveGamble/cJSON
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
BasedHardware/omi#15662 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
level/task module/gcp type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
Difficulty 1/5 Under an hour Newbie friendliness 86/100
hapostgres/pg_auto_failover#1190 ·