Skip to content

libyaml_dumper_fuzzer: parser leak on emitter init failure #324

@zchengchen

Description

@zchengchen

Summary

projects/libyaml/libyaml_dumper_fuzzer.c in google/oss-fuzz leaks the entire yaml_parser_t (68 KB, 8 internal buffers) when yaml_emitter_initialize() fails. Line 227 returns without calling yaml_parser_delete(). All five other fuzzers in the same project that use this init pattern handle it correctly.

Bug Description

Line 222–227 problem:

if (!yaml_parser_initialize(&parser))    // line 222 — allocates 8 internal buffers
    return 0;

yaml_parser_set_input_string(&parser, data, size);
if (!yaml_emitter_initialize(&emitter))
    return 0;  // BUG: parser (68 KB) leaked — yaml_parser_delete never called

yaml_parser_initialize() allocates 8 internal buffers totaling 68 KB. If yaml_emitter_initialize() then fails, the function returns immediately without cleanup.

All five other fuzzers in the same project handle this correctly:

// libyaml_emitter_fuzzer.c (line 228–231):
if (!yaml_emitter_initialize(&emitter)) {
    yaml_parser_delete(&parser);
    return 0;
}

// libyaml_deconstructor_fuzzer.c (line 49–52):
if (!yaml_emitter_initialize(&emitter)) {
    yaml_parser_delete(&parser);
    return 1;
}

// libyaml_deconstructor_alt_fuzzer.c (line 51–54):
if (!yaml_emitter_initialize(&emitter)) {
    yaml_parser_delete(&parser);
    return 1;
}

// libyaml_reformatter_fuzzer.c (line 45–46):
if (!yaml_emitter_initialize(&emitter))
    goto cleanup_parser;  // jumps to yaml_parser_delete

// libyaml_reformatter_alt_fuzzer.c (line 45–46):
if (!yaml_emitter_initialize(&emitter))
    goto cleanup_parser;  // jumps to yaml_parser_delete

libyaml_dumper_fuzzer.c is the only one that forgot the cleanup.

Evidence

PoC uses gcc -Wl,--wrap=malloc to fail the first malloc of yaml_emitter_initialize (call #9), so yaml_parser_initialize succeeds but yaml_emitter_initialize returns 0. The same failure is applied to both the buggy and fixed versions.

git clone --depth 1 https://github.com/yaml/libyaml && cd libyaml
mkdir build && cd build && cmake -DBUILD_SHARED_LIBS=OFF .. && make -j$(nproc)
# place poc_dumper_leak.c here
gcc -g -O0 poc_dumper_leak.c libyaml.a -I../include -Wl,--wrap=malloc -o poc_dumper_leak
valgrind --leak-check=full --show-leak-kinds=all ./poc_dumper_leak

Original (buggy):

==3002666== HEAP SUMMARY:
==3002666==     in use at exit: 68,224 bytes in 8 blocks
==3002666==   total heap usage: 24 allocs, 16 frees, 204,672 bytes allocated
==3002666==
==3002666== 16,384 bytes in 1 blocks are definitely lost in loss record 7 of 8
==3002666==    at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==3002666==    by 0x109359: __wrap_malloc (poc_dumper_leak.c:31)
==3002666==    by 0x10976A: yaml_malloc (api.c:33)
==3002666==    by 0x109CD9: yaml_parser_initialize (api.c:182)
==3002666==    by 0x109396: buggy_dumper_init (poc_dumper_leak.c:40)
==3002666==    by 0x109627: main (poc_dumper_leak.c:111)
==3002666==
==3002666== 49,152 bytes in 1 blocks are definitely lost in loss record 8 of 8
==3002666==    at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==3002666==    by 0x109359: __wrap_malloc (poc_dumper_leak.c:31)
==3002666==    by 0x10976A: yaml_malloc (api.c:33)
==3002666==    by 0x109D65: yaml_parser_initialize (api.c:184)
==3002666==    by 0x109396: buggy_dumper_init (poc_dumper_leak.c:40)
==3002666==    by 0x109627: main (poc_dumper_leak.c:111)
==3002666==
==3002666== LEAK SUMMARY:
==3002666==    definitely lost: 68,224 bytes in 8 blocks
==3002666==    indirectly lost: 0 bytes in 0 blocks
==3002666==      possibly lost: 0 bytes in 0 blocks
==3002666==    still reachable: 0 bytes in 0 blocks
==3002666==         suppressed: 0 bytes in 0 blocks
==3002666==
==3002666== ERROR SUMMARY: 8 errors from 8 contexts (suppressed: 0 from 0)

Fixed (same malloc failure injected):

==3003496== HEAP SUMMARY:
==3003496==     in use at exit: 0 bytes in 0 blocks
==3003496==   total heap usage: 16 allocs, 16 frees, 136,448 bytes allocated
==3003496==
==3003496== All heap blocks were freed -- no leaks are possible
==3003496==
==3003496== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Metric Original Fixed
allocs / frees 24 / 16 16 / 16
definitely lost 68,224 bytes in 8 blocks 0 bytes

All 8 leaked blocks are parser internal buffers allocated by yaml_parser_initialize inside buggy_dumper_init, confirming the missing yaml_parser_delete.

Fix

--- a/projects/libyaml/libyaml_dumper_fuzzer.c
+++ b/projects/libyaml/libyaml_dumper_fuzzer.c
@@ -224,7 +224,10 @@

   yaml_parser_set_input_string(&parser, data, size);
-  if (!yaml_emitter_initialize(&emitter))
-    return 0;
+  if (!yaml_emitter_initialize(&emitter)) {
+    yaml_parser_delete(&parser);
+    return 0;
+  }

Consistent with how all five other fuzzers in the same project handle this identical pattern.

poc_dumper_leak.c (click to expand)
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "yaml.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_dumper_init(const uint8_t *data, size_t size)
{
    yaml_parser_t parser;
    yaml_emitter_t emitter;

    if (!yaml_parser_initialize(&parser))
        return 0;
    yaml_parser_set_input_string(&parser, data, size);
    if (!yaml_emitter_initialize(&emitter))
        return 0;  /* BUG: parser leaked */

    yaml_emitter_delete(&emitter);
    yaml_parser_delete(&parser);
    return 0;
}

static int fixed_dumper_init(const uint8_t *data, size_t size)
{
    yaml_parser_t parser;
    yaml_emitter_t emitter;

    if (!yaml_parser_initialize(&parser))
        return 0;
    yaml_parser_set_input_string(&parser, data, size);
    if (!yaml_emitter_initialize(&emitter)) {
        yaml_parser_delete(&parser);  /* FIX */
        return 0;
    }

    yaml_emitter_delete(&emitter);
    yaml_parser_delete(&parser);
    return 0;
}

int main(void)
{
    const uint8_t data[] = "00key: value\n";
    size_t size = sizeof(data) - 1;

    /* Count mallocs for parser init */
    g_fail_at = -1; g_malloc_count = 0;
    { yaml_parser_t p; yaml_parser_initialize(&p); yaml_parser_delete(&p); }
    int n = g_malloc_count;

    /* Fail first malloc of yaml_emitter_initialize */
    g_fail_at = n + 1; g_malloc_count = 0;
    buggy_dumper_init(data, size);

    g_fail_at = n + 1; g_malloc_count = 0;
    fixed_dumper_init(data, size);

    return 0;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions