Skip to content

Commit 1e878d0

Browse files
authored
test/oss-fuzz: add openexr_exrgaps_fuzzer (#2401)
* test/oss-fuzz: add openexr_exrgaps_fuzzer Adds a third oss-fuzz fuzzer that complements the existing two (openexr_exrcheck_fuzzer and openexr_exrcorecheck_fuzzer) by exercising API surfaces those two cannot reach: - TiledInputPart::readTile with adversarial coordinate sequencing, stressing the tile cache and ripmap level resolution beyond what checkOpenEXRFile()'s lexicographic loop covers. - TiledInputPart::readTiles with attacker-controlled (dx1,dx2,dy1,dy2) ranges rather than the natural full-image range. - DeepTiledInputPart::readTile with attacker-controlled coordinates. - MultiPartInputFile::header(p) iteration in reverse part order, with per-attribute name lookup, exercising the part-cache and the typed-attribute path. The harness shares the same plain LLVMFuzzerTestOneInput pattern as the existing fuzzers, the same BSD-3-Clause header, and links against the same OpenEXR / OpenEXRUtil targets via the existing foreach loop in CMakeLists.txt. Signed-off-by: Anthony Hurtado <amhurtado@pm.me> * test/oss-fuzz: address Copilot review on openexr_exrgaps_fuzzer Six fixes from the Copilot review of #2401: 1. read_u32(): cast each byte to uint32_t before shifting/ORing. The previous form left-shifted promoted-to-int values, which is undefined behavior in C++ when the shifted value would set the sign bit. 2. write_temp(): handle short writes and EINTR. The single write() call could legally return early or be interrupted, causing valid inputs to be silently dropped. Loop until the full buffer is written, retrying on EINTR. 3. Add explicit #include <algorithm> for std::min, which was previously relying on transitive inclusion. Also add <cerrno>. 4. Mode 0 (test_tiled_random_coords): replace `if (hdr.find("type") != hdr.end()) continue;` with a check on the actual type value. MultiPartInputFile auto-synthesizes a "type" attribute when missing, so the previous check effectively skipped every part. Mirror the fix in test_deep_tiled to require hdr.type() == DEEPTILE explicitly. Use Imf::DEEPTILE constants from ImfPartType.h. 5. Mode 1 (test_tiled_range): clamp the (dx2-dx1+1)*(dy2-dy1+1) product so a single readTiles() call requests at most kMaxRangeSide^2 = 64 tiles, down from the prior 32x32 = 1024. Improves fuzz throughput on valid tiled inputs without losing coverage of small-range and boundary cases. 6. CMakeLists.txt: replace the duplicated fuzzer list in the oss_fuzz custom target's DEPENDS with ${OPENEXR_FUZZERS}, so the list is maintained in one place. Signed-off-by: Anthony Hurtado <amhurtado@pm.me> * test/oss-fuzz: refresh exrgaps fuzzer header comment Drop the now-stale "companion to exrcheck/exrcorecheck" framing since more fuzzers have landed since this was written, and reword the leading sentence to refer to all checkOpenEXRFile-based fuzzers collectively. Signed-off-by: Anthony Hurtado <amhurtado@pm.me> --------- Signed-off-by: Anthony Hurtado <amhurtado@pm.me>
1 parent b428ba5 commit 1e878d0

2 files changed

Lines changed: 298 additions & 2 deletions

File tree

src/test/oss-fuzz/CMakeLists.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ message(status "CXX_FLAGS=$ENV{CXX_FLAGS}")
2424
message(status "CC=$ENV{CC}")
2525
message(status "CC_FLAGS=$ENV{CC_FLAGS}")
2626

27-
set(OPENEXR_FUZZERS openexr_exrcheck_fuzzer openexr_exrcorecheck_fuzzer openexr_encoding_fuzzer openexr_attribute_fuzzer openexr_htj2k_fuzzer openexr_roundtrip_fuzzer)
27+
set(OPENEXR_FUZZERS openexr_exrcheck_fuzzer openexr_exrcorecheck_fuzzer openexr_encoding_fuzzer openexr_attribute_fuzzer openexr_htj2k_fuzzer openexr_roundtrip_fuzzer openexr_exrgaps_fuzzer)
2828
add_custom_target(oss_fuzz ALL
29-
DEPENDS openexr_exrcheck_fuzzer openexr_exrcorecheck_fuzzer openexr_encoding_fuzzer openexr_attribute_fuzzer openexr_htj2k_fuzzer openexr_roundtrip_fuzzer
29+
DEPENDS ${OPENEXR_FUZZERS}
3030
)
3131

3232
if ($ENV{FUZZING_ENGINE} STREQUAL "afl")
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
//
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
// Copyright (c) Contributors to the OpenEXR Project.
4+
//
5+
6+
//
7+
// The Imf::checkOpenEXRFile()-based fuzzers iterate
8+
// parts and tiles in linear order with in-bounds coordinates. This fuzzer
9+
// targets four paths that complementary code coverage misses:
10+
//
11+
// Mode 0: TiledInputPart::readTile(dx, dy, lx, ly) with adversarial
12+
// coordinate sequencing, stressing tile-cache eviction and
13+
// ripmap level resolution.
14+
// Mode 1: TiledInputPart::readTiles(dx1, dx2, dy1, dy2, lx, ly) with
15+
// attacker-controlled ranges (rather than the natural
16+
// (0..numXTiles-1, 0..numYTiles-1) range).
17+
// Mode 2: DeepTiledInputPart::readTile(dx, dy, lx, ly) with
18+
// attacker-controlled coordinates.
19+
// Mode 3: MultiPartInputFile::header(p) iteration in reverse part
20+
// order, with per-attribute name lookup, exercising the
21+
// part-cache and the typed-attribute path.
22+
//
23+
// The harness writes the input to a temporary file because the public
24+
// C++ MultiPartInputFile/TiledInputPart API operates on file paths.
25+
//
26+
27+
#include <algorithm>
28+
#include <cerrno>
29+
#include <cstdint>
30+
#include <cstddef>
31+
#include <cstdio>
32+
#include <cstdlib>
33+
#include <cstring>
34+
#include <string>
35+
36+
#include <unistd.h>
37+
38+
#include <ImfMultiPartInputFile.h>
39+
#include <ImfTiledInputPart.h>
40+
#include <ImfDeepTiledInputPart.h>
41+
#include <ImfHeader.h>
42+
#include <ImfFrameBuffer.h>
43+
#include <ImfDeepFrameBuffer.h>
44+
#include <ImfTileDescriptionAttribute.h>
45+
#include <ImfPartType.h>
46+
47+
namespace IMF = OPENEXR_IMF_NAMESPACE;
48+
49+
namespace {
50+
51+
constexpr size_t kMaxInput = 4 * 1024 * 1024;
52+
constexpr int kMaxTilesPerCall = 16;
53+
constexpr int kMaxParts = 8;
54+
// Bound on (dx2-dx1+1)*(dy2-dy1+1) for readTiles() to avoid pathological
55+
// 1024-tile calls slowing fuzz throughput. 8x8 = 64 tiles per call covers
56+
// boundary and small-range cases without burning iteration time.
57+
constexpr int kMaxRangeSide = 8;
58+
59+
std::string write_temp (const uint8_t* data, size_t size)
60+
{
61+
char path[] = "/tmp/exr_gaps_XXXXXX";
62+
int fd = mkstemp (path);
63+
if (fd < 0) return std::string ();
64+
size_t total = 0;
65+
while (total < size)
66+
{
67+
ssize_t n = write (fd, data + total, size - total);
68+
if (n < 0)
69+
{
70+
if (errno == EINTR) continue;
71+
close (fd);
72+
unlink (path);
73+
return std::string ();
74+
}
75+
if (n == 0) break;
76+
total += static_cast<size_t> (n);
77+
}
78+
close (fd);
79+
if (total != size)
80+
{
81+
unlink (path);
82+
return std::string ();
83+
}
84+
return std::string (path);
85+
}
86+
87+
uint32_t read_u32 (const uint8_t* p, size_t size, size_t off)
88+
{
89+
if (off + 4 > size) return 0;
90+
return static_cast<uint32_t> (p[off])
91+
| (static_cast<uint32_t> (p[off + 1]) << 8)
92+
| (static_cast<uint32_t> (p[off + 2]) << 16)
93+
| (static_cast<uint32_t> (p[off + 3]) << 24);
94+
}
95+
96+
void
97+
test_tiled_random_coords (
98+
const std::string& path, const uint8_t* params, size_t param_size)
99+
{
100+
if (param_size < 8) return;
101+
try
102+
{
103+
IMF::MultiPartInputFile mpif (path.c_str ());
104+
int n_parts = std::min (mpif.parts (), kMaxParts);
105+
for (int p = 0; p < n_parts; p++)
106+
{
107+
const IMF::Header& hdr = mpif.header (p);
108+
if (!hdr.hasTileDescription ()) continue;
109+
// TiledInputPart is for non-deep tiled parts. Mode 2 covers
110+
// deep tiled. MultiPartInputFile auto-synthesizes a "type"
111+
// attribute, so we must check the value rather than presence.
112+
if (hdr.hasType () && hdr.type () == IMF::DEEPTILE) continue;
113+
try
114+
{
115+
IMF::TiledInputPart tip (mpif, p);
116+
IMF::FrameBuffer fb;
117+
tip.setFrameBuffer (fb);
118+
for (int t = 0; t < kMaxTilesPerCall; t++)
119+
{
120+
uint32_t r =
121+
read_u32 (params, param_size, (t * 8) % param_size);
122+
uint32_t s =
123+
read_u32 (params, param_size, ((t * 8) + 4) % param_size);
124+
int dx = r & 0xff;
125+
int dy = (r >> 8) & 0xff;
126+
int lx = s & 0x0f;
127+
int ly = (s >> 4) & 0x0f;
128+
try
129+
{
130+
tip.readTile (dx, dy, lx, ly);
131+
}
132+
catch (...)
133+
{
134+
}
135+
}
136+
}
137+
catch (...)
138+
{
139+
}
140+
}
141+
}
142+
catch (...)
143+
{
144+
}
145+
}
146+
147+
void
148+
test_tiled_range (
149+
const std::string& path, const uint8_t* params, size_t param_size)
150+
{
151+
if (param_size < 8) return;
152+
try
153+
{
154+
IMF::MultiPartInputFile mpif (path.c_str ());
155+
int n_parts = std::min (mpif.parts (), kMaxParts);
156+
for (int p = 0; p < n_parts; p++)
157+
{
158+
const IMF::Header& hdr = mpif.header (p);
159+
if (!hdr.hasTileDescription ()) continue;
160+
try
161+
{
162+
IMF::TiledInputPart tip (mpif, p);
163+
IMF::FrameBuffer fb;
164+
tip.setFrameBuffer (fb);
165+
uint32_t r = read_u32 (params, param_size, 0);
166+
uint32_t s = read_u32 (params, param_size, 4);
167+
int dx1 = r & 0xff;
168+
int dy1 = (r >> 16) & 0xff;
169+
// Clamp range size so total tile count stays <= kMaxRangeSide^2.
170+
int dx2 = dx1 + (((r >> 8) & 0x1f) % kMaxRangeSide);
171+
int dy2 = dy1 + (((r >> 24) & 0x1f) % kMaxRangeSide);
172+
int lx = s & 0x0f;
173+
int ly = (s >> 4) & 0x0f;
174+
try
175+
{
176+
tip.readTiles (dx1, dx2, dy1, dy2, lx, ly);
177+
}
178+
catch (...)
179+
{
180+
}
181+
}
182+
catch (...)
183+
{
184+
}
185+
}
186+
}
187+
catch (...)
188+
{
189+
}
190+
}
191+
192+
void
193+
test_deep_tiled (
194+
const std::string& path, const uint8_t* params, size_t param_size)
195+
{
196+
if (param_size < 4) return;
197+
try
198+
{
199+
IMF::MultiPartInputFile mpif (path.c_str ());
200+
int n_parts = std::min (mpif.parts (), kMaxParts);
201+
for (int p = 0; p < n_parts; p++)
202+
{
203+
const IMF::Header& hdr = mpif.header (p);
204+
// DeepTiledInputPart only accepts deep tiled parts.
205+
if (!hdr.hasType () || hdr.type () != IMF::DEEPTILE) continue;
206+
try
207+
{
208+
IMF::DeepTiledInputPart dtip (mpif, p);
209+
IMF::DeepFrameBuffer fb;
210+
dtip.setFrameBuffer (fb);
211+
for (int t = 0; t < kMaxTilesPerCall; t++)
212+
{
213+
uint32_t r =
214+
read_u32 (params, param_size, (t * 4) % param_size);
215+
int dx = r & 0xff;
216+
int dy = (r >> 8) & 0xff;
217+
int lx = (r >> 16) & 0x0f;
218+
int ly = (r >> 20) & 0x0f;
219+
try
220+
{
221+
dtip.readTile (dx, dy, lx, ly);
222+
}
223+
catch (...)
224+
{
225+
}
226+
}
227+
}
228+
catch (...)
229+
{
230+
}
231+
}
232+
}
233+
catch (...)
234+
{
235+
}
236+
}
237+
238+
void
239+
test_part_iteration (
240+
const std::string& path, const uint8_t* params, size_t param_size)
241+
{
242+
(void) params;
243+
(void) param_size;
244+
try
245+
{
246+
IMF::MultiPartInputFile mpif (path.c_str ());
247+
int n_parts = std::min (mpif.parts (), kMaxParts);
248+
for (int p = n_parts - 1; p >= 0; p--)
249+
{
250+
const IMF::Header& hdr = mpif.header (p);
251+
for (auto it = hdr.begin (); it != hdr.end (); ++it)
252+
{
253+
(void) it.name ();
254+
try
255+
{
256+
(void) hdr.findTypedAttribute<
257+
IMF::TileDescriptionAttribute> (it.name ());
258+
}
259+
catch (...)
260+
{
261+
}
262+
}
263+
}
264+
}
265+
catch (...)
266+
{
267+
}
268+
}
269+
270+
} // namespace
271+
272+
extern "C" int LLVMFuzzerTestOneInput (const uint8_t* data, size_t size)
273+
{
274+
if (size < 4 || size > kMaxInput) return 0;
275+
276+
uint8_t mode = data[0] % 4;
277+
size_t param_len = std::min<size_t> (32, size - 1);
278+
const uint8_t* params = data + 1;
279+
if (size < 1 + param_len + 256) return 0;
280+
const uint8_t* exr_blob = data + 1 + param_len;
281+
size_t exr_size = size - 1 - param_len;
282+
283+
std::string path = write_temp (exr_blob, exr_size);
284+
if (path.empty ()) return 0;
285+
286+
switch (mode)
287+
{
288+
case 0: test_tiled_random_coords (path, params, param_len); break;
289+
case 1: test_tiled_range (path, params, param_len); break;
290+
case 2: test_deep_tiled (path, params, param_len); break;
291+
case 3: test_part_iteration (path, params, param_len); break;
292+
}
293+
294+
unlink (path.c_str ());
295+
return 0;
296+
}

0 commit comments

Comments
 (0)