Skip to content

test/oss-fuzz: add openexr_exrgaps_fuzzer#2401

Open
jortles wants to merge 3 commits into
AcademySoftwareFoundation:mainfrom
jortles:add-exrgaps-fuzzer
Open

test/oss-fuzz: add openexr_exrgaps_fuzzer#2401
jortles wants to merge 3 commits into
AcademySoftwareFoundation:mainfrom
jortles:add-exrgaps-fuzzer

Conversation

@jortles
Copy link
Copy Markdown

@jortles jortles commented May 9, 2026

Summary

Adds a third oss-fuzz fuzzer (openexr_exrgaps_fuzzer) that complements the existing openexr_exrcheck_fuzzer and openexr_exrcorecheck_fuzzer by 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:

for (int ylevel = 0; ylevel < numYLevels; ++ylevel)
    for (int xlevel = 0; xlevel < numXLevels; ++xlevel)
        for (int y = 0; y < in.numYTiles(ylevel); ++y)
            for (int x = 0; x < in.numXTiles(xlevel); ++x)
                in.readTile(x, y, xlevel, ylevel);

This new fuzzer adds four modes that exercise paths outside that loop:

Mode Path
0 TiledInputPart::readTile(dx, dy, lx, ly) with adversarial coordinate sequencing — stresses tile cache eviction and ripmap level resolution in non-linear access patterns
1 TiledInputPart::readTiles(dx1, dx2, dy1, dy2, lx, ly) with attacker-controlled ranges instead of the natural (0..numXTiles-1) extent
2 DeepTiledInputPart::readTile(dx, dy, lx, ly) with attacker-controlled coordinates
3 MultiPartInputFile::header(p) iteration in reverse part order, with per-attribute name lookup — exercises the part-cache eviction path and the typed-attribute path

Files changed

  • src/test/oss-fuzz/openexr_exrgaps_fuzzer.cc — new fuzzer (~270 lines, BSD-3-Clause header matching the sibling fuzzers, plain LLVMFuzzerTestOneInput)
  • src/test/oss-fuzz/CMakeLists.txt — adds openexr_exrgaps_fuzzer to the existing OPENEXR_FUZZERS list and oss_fuzz custom target's DEPENDS

The new fuzzer hooks into the existing foreach loop, so no changes to the build script are needed.

Style and conventions

Mirrors the sibling fuzzers exactly:

  • Plain LLVMFuzzerTestOneInput (not FuzzTest)
  • BSD-3-Clause header
  • Links only against OpenEXR::OpenEXR and OpenEXR::OpenEXRUtil (already available via the foreach)
  • Defensive try { ... } catch (...) { } around library calls — exception-safe, matches checkOpenEXRFile style

OSS-Fuzz integration

No change required to the upstream OSS-Fuzz projects/openexr/build.sh — the oss_fuzz custom target already builds the new fuzzer once it's in the OPENEXR_FUZZERS list.

Testing

Built locally with afl-clang-fast++ + ASAN and run on the openexr-images TestImages / MultiResolution / Tiles / DisplayWindow / Damaged corpora. Plain clang++ -fsanitize=address build also works (matches the OSS-Fuzz CFLAGS pattern).

Copilot AI review requested due to automatic review settings May 9, 2026 13:11
@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented May 9, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.cc with 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);
Comment on lines +81 to +85
{
IMF::MultiPartInputFile mpif (path.c_str ());
int n_parts = std::min (mpif.parts (), kMaxParts);
for (int p = 0; p < n_parts; p++)
{
Comment on lines +88 to +89
auto type_it = hdr.find ("type");
if (type_it != hdr.end ()) continue;
Comment on lines +142 to +153
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);
}
Comment thread src/test/oss-fuzz/CMakeLists.txt Outdated
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
Comment on lines +54 to +66
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);
jortles added a commit to jortles/openexr that referenced this pull request May 10, 2026
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.
@jortles
Copy link
Copy Markdown
Author

jortles commented May 10, 2026

Thanks for the review — pushed 038f0d1 addressing all six comments:

  1. read_u32() UB on shifted ints — each byte is now cast to uint32_t before the shifts.
  2. write_temp() short-write / EINTR — wraps write() in a loop, retries on EINTR, fails only on < 0 or 0 before completion.
  3. Missing <algorithm> — added explicitly (also <cerrno> for the new EINTR handling).
  4. Mode 0 always-skippingMultiPartInputFile auto-synthesizes a type attribute, so the previous find("type") != end() check skipped every part. Switched both Mode 0 and Mode 2 to checking the value via hdr.hasType() + hdr.type() == IMF::DEEPTILE (added #include <ImfPartType.h> for the constants).
  5. Mode 1 1024-tile blow-up — added kMaxRangeSide = 8 and clamped the range so a single readTiles() call requests at most kMaxRangeSide² = 64 tiles, down from 32×32 = 1024.
  6. Duplicated fuzzer list in CMakeLists.txt — replaced the literal list in DEPENDS with ${OPENEXR_FUZZERS}.

Smoke-checked the harness compiles against OpenEXR main. Happy to address anything else.

jortles added a commit to jortles/openexr that referenced this pull request May 10, 2026
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>
@jortles jortles force-pushed the add-exrgaps-fuzzer branch from 038f0d1 to 0aded40 Compare May 10, 2026 04:48
Copy link
Copy Markdown
Member

@cary-ilm cary-ilm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, thanks for the contribution!

Merging #2391 caused conflicts in CMakeLists.txt, can you rebase on main and resolve them? It should be straightforward.

jortles added 2 commits May 11, 2026 09:27
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>
@jortles jortles force-pushed the add-exrgaps-fuzzer branch from 0aded40 to 8835db0 Compare May 11, 2026 14:27
@jortles
Copy link
Copy Markdown
Author

jortles commented May 11, 2026

Rebased onto current main (8835db02) to resolve the CMakeLists.txt conflict from #2391. The new OPENEXR_FUZZERS list now includes all six fuzzers from #2391 plus openexr_exrgaps_fuzzer. CI re-running.

Copy link
Copy Markdown
Member

@cary-ilm cary-ilm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// Companion to openexr_exrcheck_fuzzer / openexr_exrcorecheck_fuzzer.

Comment on lines +9 to +10
// The two existing fuzzers call Imf::checkOpenEXRFile(), which iterates
// parts and tiles in linear order with in-bounds coordinates. This fuzzer
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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>
@jortles
Copy link
Copy Markdown
Author

jortles commented May 11, 2026

Comment refresh pushed, thanks for catching that. And yeah — "gaps" is just shorthand for coverage gaps. The existing checkOpenEXRFile-based fuzzers always walk parts and tiles in order with valid coordinates, so they don't really exercise things like out-of-order readTile calls, wide readTiles ranges, or reverse-order header iteration. That's what this one's poking at.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants