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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions