Skip to content

Commit 0c30df0

Browse files
committed
ossfuzz: add zip_source_zip_fuzzer for partial-window extraction paths
The existing read harnesses (zip_read_fuzzer, zip_read_file_fuzzer, zip_read_metadata_fuzzer) reach entry data only through zip_fopen_index(), which always calls zip_source_zip_file_create() with flags = 0, start = 0 and len = -1. The partial-window and raw-extraction logic in zip_source_zip_new.c is therefore never exercised: - start > 0 / len >= 0 sub-range reads and their offset arithmetic and overflow / past-end-of-file checks, - ZIP_FL_COMPRESSED (raw compressed bytes wrapped in a window), - ZIP_FL_UNCHANGED dirent/stat selection, - the public zip_source_zip_create() entry point. Add a fuzz target that opens the input as an archive and, for every entry, creates a zip_source_zip for a range of flag/start/len combinations derived from the entry's stated size (in-range partial ranges as well as boundary and past-the-end ranges) and drains each source through the source interface so the window, decrypt, decompress and crc layers run.
1 parent 1478368 commit 0c30df0

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

ossfuzz/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ set(FUZZ_PROGRAMS
22
zip_read_file_fuzzer
33
zip_read_fuzzer
44
zip_read_metadata_fuzzer
5+
zip_source_zip_fuzzer
56
zip_write_encrypt_aes256_file_fuzzer
67
zip_write_encrypt_pkware_file_fuzzer
78
zip_write_roundtrip_fuzzer

ossfuzz/zip_source_zip_fuzzer.c

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
zip_source_zip_fuzzer.c -- fuzz the zip_source_zip windowing/extraction paths
3+
Copyright (C) 2026 The libzip Authors
4+
5+
SPDX-License-Identifier: BSD-3-Clause
6+
7+
Coverage gap addressed:
8+
zip_read_fuzzer.c and zip_read_file_fuzzer.c read entries through
9+
zip_fopen_index(), which always calls zip_source_zip_file_create()
10+
with the fixed arguments (flags = 0, start = 0, len = -1). As a
11+
result the partial-window and raw-extraction logic in
12+
zip_source_zip_new.c is never exercised by any harness:
13+
14+
- start > 0 / len >= 0 (reading a sub-range of an entry; the
15+
window source offset arithmetic and the
16+
overflow / past-end-of-file checks)
17+
- ZIP_FL_COMPRESSED (returning the raw compressed bytes wrapped
18+
in a window instead of decompressing)
19+
- ZIP_FL_UNCHANGED (stat/dirent selection for the original
20+
on-disk entry)
21+
- the public zip_source_zip_create() entry point itself
22+
23+
These paths combine attacker-controlled start/len with sizes parsed
24+
from the central directory and then layer window -> decrypt ->
25+
decompress -> crc sources on top, so they are worth fuzzing directly.
26+
27+
Strategy:
28+
Open the fuzz input as a read-only archive and, for every entry,
29+
create a zip_source_zip for several flag / start / len combinations
30+
derived from the entry's stated size (in-range partial ranges as well
31+
as boundary and past-the-end ranges). Each resulting source is then
32+
opened and drained through the source interface so the window,
33+
decrypt, decompress and crc layers actually run.
34+
*/
35+
36+
#include <stdint.h>
37+
#include <stddef.h>
38+
39+
#include <zip.h>
40+
41+
static const char *PASSWORD = "fuzzpw";
42+
43+
/* Create a zip_source_zip for the given parameters and, if it succeeds,
44+
drive it through the source interface so every layer is exercised. */
45+
static void
46+
exercise(zip_t *za, zip_uint64_t idx, zip_flags_t flags, zip_uint64_t start, zip_int64_t len) {
47+
zip_error_t error;
48+
zip_source_t *src;
49+
50+
zip_error_init(&error);
51+
src = zip_source_zip_create(za, idx, flags, start, len, &error);
52+
zip_error_fini(&error);
53+
if (src == NULL) {
54+
return;
55+
}
56+
57+
if (zip_source_open(src) == 0) {
58+
char buf[4096];
59+
while (zip_source_read(src, buf, sizeof(buf)) > 0) {
60+
;
61+
}
62+
zip_source_close(src);
63+
}
64+
65+
zip_source_free(src);
66+
}
67+
68+
int
69+
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
70+
zip_error_t error;
71+
zip_source_t *src;
72+
zip_t *za;
73+
zip_int64_t n, i;
74+
75+
zip_error_init(&error);
76+
77+
src = zip_source_buffer_create(data, size, 0, &error);
78+
if (src == NULL) {
79+
zip_error_fini(&error);
80+
return 0;
81+
}
82+
83+
za = zip_open_from_source(src, 0, &error);
84+
if (za == NULL) {
85+
zip_source_free(src);
86+
zip_error_fini(&error);
87+
return 0;
88+
}
89+
zip_error_fini(&error);
90+
91+
zip_set_default_password(za, PASSWORD);
92+
93+
n = zip_get_num_entries(za, 0);
94+
for (i = 0; i < n; i++) {
95+
zip_uint64_t idx = (zip_uint64_t)i;
96+
zip_stat_t st;
97+
zip_uint64_t entry_size = 0;
98+
99+
if (zip_stat_index(za, idx, 0, &st) == 0 && (st.valid & ZIP_STAT_SIZE)) {
100+
entry_size = st.size;
101+
}
102+
103+
/* Whole-entry variants: decode, raw compressed window, original entry. */
104+
exercise(za, idx, 0, 0, -1);
105+
exercise(za, idx, ZIP_FL_COMPRESSED, 0, -1);
106+
exercise(za, idx, ZIP_FL_UNCHANGED, 0, -1);
107+
108+
/* In-range partial windows: these drive the offset arithmetic. */
109+
if (entry_size >= 2) {
110+
zip_uint64_t half = entry_size / 2;
111+
exercise(za, idx, 0, 1, (zip_int64_t)half);
112+
exercise(za, idx, 0, half, (zip_int64_t)(entry_size - half));
113+
exercise(za, idx, 0, 0, (zip_int64_t)(entry_size - 1));
114+
}
115+
116+
/* Boundary and past-the-end ranges exercise the rejection paths. */
117+
exercise(za, idx, 0, entry_size, 1);
118+
exercise(za, idx, 0, entry_size + 1, -1);
119+
}
120+
121+
zip_discard(za);
122+
return 0;
123+
}

0 commit comments

Comments
 (0)