Files
uc2/tests/fuzz/fuzz_extract.c
Eremey Valetov ad923d7ea0
Some checks failed
Build / Linux (push) Has been cancelled
Build / Windows (MSVC) (push) Has been cancelled
Build / macOS (push) Has been cancelled
Build / libarchive plugin (push) Has been cancelled
Build / DOS (DJGPP) (push) Has been cancelled
Docs / build (push) Has been cancelled
Docs / deploy (push) Has been cancelled
fix heap overflow parsing a damaged central directory
A crafted archive could crash the reader with an out-of-bounds read in
the directory-skip path (uc2_finish_cdir -> uc2_read_cdir -> uc2_get_tag).

decompress_cdir allocates cdir_buf inside its decode loop but, on its
error paths (decode failure or a checksum mismatch), returned before
setting cdir_range.end -- leaving cdir_buf non-NULL with a stale end. A
later uc2_read_cdir/uc2_finish_cdir then saw cdir_buf != NULL, skipped
re-reading, and walked a range whose end pointed below its start, so
range_len wrapped and range_get handed out wild pointers. Free cdir_buf
on every error path so the invariant "cdir_buf != NULL iff cdir_range is
valid" holds, and make range_len report an empty range (rather than a
huge one) if end ever precedes ptr, as defense in depth for the whole
parser.

Also add a compression-ratio ceiling to the cdir decode: a tiny crafted
stream can expand via long matches, so abort once the output far
outgrows the compressed bytes consumed.

Found with a new libFuzzer harness (tests/fuzz/, not built by default).
Memory-safety is clean over sustained fuzzing after this change; 22/22
ctest on Release and ASan. A residual slow-input timeout via a separate
decode path is tracked for follow-up.
2026-06-13 10:53:49 -04:00

79 lines
2.1 KiB
C

/* libFuzzer harness for the UC2 read path.
*
* Feeds the fuzzer-provided bytes as a .uc2 archive through the full
* open -> read_cdir -> finish_cdir -> extract flow with an in-memory
* reader and a discard writer. The decoder must never read or write
* out of bounds on any input.
*
* Build (clang):
* clang -fsanitize=fuzzer,address -O1 -g -Ilib/include -Ilib/src \
* -I<builddir>/lib tests/fuzz/fuzz_extract.c lib/src/*.c \
* <builddir>/lib/super_data.S -o fuzz_extract
* Run: ./fuzz_extract -max_len=65536 corpus/
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <uc2/libuc2.h>
struct mem { const uint8_t *data; unsigned avail; };
static int mem_read(void *ctx, unsigned pos, void *buf, unsigned len)
{
struct mem *m = ctx;
if (pos >= m->avail)
return 0;
unsigned n = m->avail - pos;
if (n > len)
n = len;
memcpy(buf, m->data + pos, n);
return (int)n;
}
static void *mem_alloc(void *ctx, unsigned size) { (void)ctx; return malloc(size); }
static void mem_free(void *ctx, void *ptr) { (void)ctx; free(ptr); }
static int discard(void *ctx, const void *p, unsigned len)
{ (void)ctx; (void)p; (void)len; return 0; }
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
if (size > (1u << 20)) /* bound work; the format is small anyway */
return 0;
struct uc2_io io = { .read = mem_read, .alloc = mem_alloc, .free = mem_free };
struct mem m = { .data = data, .avail = (unsigned)size };
uc2_handle h = uc2_open(&io, &m);
if (!h)
return 0;
struct uc2_entry entries[64];
int n = 0;
for (int guard = 0; guard < 100000; guard++) {
struct uc2_entry e;
int ret = uc2_read_cdir(h, &e);
if (ret == UC2_End || ret < 0)
break;
while (ret == UC2_TaggedEntry) {
char *tag; void *d; unsigned sz;
ret = uc2_get_tag(h, &e, &tag, &d, &sz);
if (ret < 0)
break;
}
if (ret < 0)
break;
if (!e.is_dir && n < (int)(sizeof entries / sizeof *entries))
entries[n++] = e;
}
char label[12];
uc2_finish_cdir(h, label);
for (int i = 0; i < n; i++)
uc2_extract(h, &entries[i].xi, entries[i].size, discard, 0);
uc2_close(h);
return 0;
}