test/oss-fuzz: add openexr_exrgaps_fuzzer#2401
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new OSS-Fuzz harness (openexr_exrgaps_fuzzer) to exercise OpenEXR tiled/deep-tiled and multipart header-access paths not covered by the existing checkOpenEXRFile()-based fuzzers.
Changes:
- Introduces
openexr_exrgaps_fuzzer.ccwith 4 fuzzing modes targeting tiled/deep-tiled read APIs and multipart header iteration. - Registers the new fuzzer target in the OSS-Fuzz CMake configuration so it’s built/installed with the existing fuzzers.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/test/oss-fuzz/openexr_exrgaps_fuzzer.cc | New fuzzer that writes input to a temp file and exercises tiled/deep-tiled reads plus multipart header iteration. |
| src/test/oss-fuzz/CMakeLists.txt | Adds openexr_exrgaps_fuzzer to the fuzzer target lists/depends. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| uint32_t read_u32 (const uint8_t* p, size_t size, size_t off) | ||
| { | ||
| if (off + 4 > size) return 0; | ||
| return p[off] | (p[off + 1] << 8) | (p[off + 2] << 16) | (p[off + 3] << 24); |
| { | ||
| IMF::MultiPartInputFile mpif (path.c_str ()); | ||
| int n_parts = std::min (mpif.parts (), kMaxParts); | ||
| for (int p = 0; p < n_parts; p++) | ||
| { |
| auto type_it = hdr.find ("type"); | ||
| if (type_it != hdr.end ()) continue; |
| uint32_t r = read_u32 (params, param_size, 0); | ||
| uint32_t s = read_u32 (params, param_size, 4); | ||
| int dx1 = r & 0xff; | ||
| int dx2 = dx1 + ((r >> 8) & 0x1f); | ||
| int dy1 = (r >> 16) & 0xff; | ||
| int dy2 = dy1 + ((r >> 24) & 0x1f); | ||
| int lx = s & 0x0f; | ||
| int ly = (s >> 4) & 0x0f; | ||
| try | ||
| { | ||
| tip.readTiles (dx1, dx2, dy1, dy2, lx, ly); | ||
| } |
| set(OPENEXR_FUZZERS openexr_exrcheck_fuzzer openexr_exrcorecheck_fuzzer openexr_exrgaps_fuzzer) | ||
| add_custom_target(oss_fuzz ALL | ||
| DEPENDS openexr_exrcheck_fuzzer openexr_exrcorecheck_fuzzer | ||
| DEPENDS openexr_exrcheck_fuzzer openexr_exrcorecheck_fuzzer openexr_exrgaps_fuzzer |
| std::string write_temp (const uint8_t* data, size_t size) | ||
| { | ||
| char path[] = "/tmp/exr_gaps_XXXXXX"; | ||
| int fd = mkstemp (path); | ||
| if (fd < 0) return std::string (); | ||
| ssize_t n = write (fd, data, size); | ||
| close (fd); | ||
| if (n != static_cast<ssize_t> (size)) | ||
| { | ||
| unlink (path); | ||
| return std::string (); | ||
| } | ||
| return std::string (path); |
Six fixes from the Copilot review of AcademySoftwareFoundation#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.
|
Thanks for the review — pushed
Smoke-checked the harness compiles against OpenEXR |
Six fixes from the Copilot review of AcademySoftwareFoundation#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>
038f0d1 to
0aded40
Compare
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>
Six fixes from the Copilot review of AcademySoftwareFoundation#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>
0aded40 to
8835db0
Compare
cary-ilm
left a comment
There was a problem hiding this comment.
The comments could use a refresh after the previous fuzzer PR got merged.
Also, just curious, what's the logic behind the "gaps" in the name?
| // | ||
|
|
||
| // | ||
| // Companion to openexr_exrcheck_fuzzer / openexr_exrcorecheck_fuzzer. |
There was a problem hiding this comment.
Minor nit, but since you wrote this, more fuzzers got added so the "two existing fuzzers" and the "companion" comment above are a bit confusing.
| // Companion to openexr_exrcheck_fuzzer / openexr_exrcorecheck_fuzzer. |
| // The two existing fuzzers call Imf::checkOpenEXRFile(), which iterates | ||
| // parts and tiles in linear order with in-bounds coordinates. This fuzzer |
There was a problem hiding this comment.
| // The two existing fuzzers call Imf::checkOpenEXRFile(), which iterates | |
| // parts and tiles in linear order with in-bounds coordinates. This fuzzer | |
| // The Imf::checkOpenEXRFile()-based fuzzers iterate | |
| // parts and tiles in linear order with in-bounds coordinates. This fuzzer |
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>
|
Comment refresh pushed, thanks for catching that. And yeah — "gaps" is just shorthand for coverage gaps. The existing |
Summary
Adds a third oss-fuzz fuzzer (
openexr_exrgaps_fuzzer) that complements the existingopenexr_exrcheck_fuzzerandopenexr_exrcorecheck_fuzzerby targeting four API surfaces those two do not exercise.What's covered that the existing fuzzers don't
Imf::checkOpenEXRFile()reads parts and tiles in linear order with in-bounds coordinates:This new fuzzer adds four modes that exercise paths outside that loop:
TiledInputPart::readTile(dx, dy, lx, ly)with adversarial coordinate sequencing — stresses tile cache eviction and ripmap level resolution in non-linear access patternsTiledInputPart::readTiles(dx1, dx2, dy1, dy2, lx, ly)with attacker-controlled ranges instead of the natural(0..numXTiles-1)extentDeepTiledInputPart::readTile(dx, dy, lx, ly)with attacker-controlled coordinatesMultiPartInputFile::header(p)iteration in reverse part order, with per-attribute name lookup — exercises the part-cache eviction path and the typed-attribute pathFiles changed
src/test/oss-fuzz/openexr_exrgaps_fuzzer.cc— new fuzzer (~270 lines, BSD-3-Clause header matching the sibling fuzzers, plainLLVMFuzzerTestOneInput)src/test/oss-fuzz/CMakeLists.txt— addsopenexr_exrgaps_fuzzerto the existingOPENEXR_FUZZERSlist andoss_fuzzcustom target's DEPENDSThe new fuzzer hooks into the existing
foreachloop, so no changes to the build script are needed.Style and conventions
Mirrors the sibling fuzzers exactly:
LLVMFuzzerTestOneInput(not FuzzTest)OpenEXR::OpenEXRandOpenEXR::OpenEXRUtil(already available via the foreach)try { ... } catch (...) { }around library calls — exception-safe, matchescheckOpenEXRFilestyleOSS-Fuzz integration
No change required to the upstream OSS-Fuzz
projects/openexr/build.sh— theoss_fuzzcustom target already builds the new fuzzer once it's in theOPENEXR_FUZZERSlist.Testing
Built locally with
afl-clang-fast+++ ASAN and run on the openexr-images TestImages / MultiResolution / Tiles / DisplayWindow / Damaged corpora. Plainclang++ -fsanitize=addressbuild also works (matches the OSS-Fuzz CFLAGS pattern).