Merge pull request #383 from lilith/chore/fuzz-dict-and-target

chore: replace decode_image fuzz target with comprehensive decoder exerciser
This commit is contained in:
Aurelia Molzer authored and GitHub committed 2026-04-09 22:26:34 +00:00
commit cf9eee64ad
6 files changed
+193 -82

No files matched your search

+1 -1
View File
@@ -55,4 +55,4 @@ This crate uses [cargo-fuzz](https://github.com/rust-fuzz/cargo-fuzz) in order t
After installing it with `cargo install cargo-fuzz` on a nightly rustc, the
fuzzing harness can be run with recommended settings using
`cargo fuzz run decode_image -snone -- -timeout=5`.
`cargo fuzz run decode_comprehensive -snone -- -dict=fuzz/tiff.dict -timeout=5`.
-1
View File
@@ -1,4 +1,3 @@
target
corpus
artifacts
coverage
-6
View File
@@ -18,12 +18,6 @@ path = ".."
[workspace]
members = ["."]
[[bin]]
name = "decode_image"
path = "fuzz_targets/decode_image.rs"
test = false
doc = false
[[bin]]
name = "decode_comprehensive"
path = "fuzz_targets/decode_comprehensive.rs"
+77 -31
View File
@@ -1,50 +1,96 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use std::io::{Cursor, Read, Seek};
use tiff::decoder::{Decoder, Limits};
use tiff::tags::Tag;
fuzz_target!(|data: &[u8]| {
let cursor = std::io::Cursor::new(data);
// Standalone header parse (separate code path from Decoder::open)
let _ = tiff::decoder::TiffHeader::parse(Cursor::new(data));
// Path 1: TiffHeader::parse (probe)
{
let _ = tiff::decoder::TiffHeader::parse(std::io::Cursor::new(data));
}
// Path 2: Full decode with strict limits
let mut decoder = match tiff::decoder::Decoder::open(cursor) {
let mut decoder = match Decoder::open(Cursor::new(data)) {
Ok(d) => d,
Err(_) => return,
};
let mut limits = tiff::decoder::Limits::default();
let mut limits = Limits::default();
limits.decoding_buffer_size = 512 * 1024;
limits.ifd_value_size = 64 * 1024;
limits.intermediate_buffer_size = 512 * 1024;
decoder = decoder.with_limits(limits);
// Probe metadata — dimensions() triggers image validation,
// must succeed before get_chunk_type/chunk_dimensions (they unwrap image)
let dims_ok = decoder.dimensions().is_ok();
let _ = decoder.colortype();
if dims_ok {
let _ = decoder.get_chunk_type();
let _ = decoder.chunk_dimensions();
}
let _ = decoder.more_images();
// Probe first IFD (opened but image not yet parsed)
probe_directory(&mut decoder);
// Read chunks individually if available
let _ = decoder.read_chunk(0);
// Read full image
let _ = decoder.read_image();
// Iterate through all IFDs
while decoder.more_images() {
if decoder.next_image().is_err() {
// Linear IFD iteration via next_directory — tolerates invalid
// intermediate IFDs that aren't decodable as images
let mut ifd_count = 1u32;
while decoder.more_images() && ifd_count < 8 {
if decoder.next_directory().is_err() {
break;
}
let _ = decoder.dimensions();
let _ = decoder.colortype();
let _ = decoder.read_image();
probe_directory(&mut decoder);
ifd_count += 1;
}
// Random-access traversal via seek (different code path: find_nth_ifd
// re-walks the IFD chain from the start each time)
for i in 0..ifd_count.min(4) as usize {
if decoder.seek_to_directory(i).is_ok() {
probe_directory(&mut decoder);
}
// seek_to_image eagerly parses the directory as an Image
let _ = decoder.seek_to_image(i);
}
});
fn probe_directory<R: Read + Seek>(decoder: &mut Decoder<R>) {
// Tag iteration — exercises value parsing for every tag in the IFD
for result in decoder.current_ifd().tag_iter() {
let _ = result;
}
// SubIFD / EXIF / GPS directory traversal — exercises read_directory
// with potentially adversarial offsets
if let Ok(Some(val)) = decoder.current_ifd().find_tag(Tag::SubIfd) {
if let Ok(ptrs) = val.into_ifd_vec() {
for ptr in ptrs.into_iter().take(4) {
let _ = decoder.read_directory(ptr);
}
}
}
for tag in [Tag::ExifDirectory, Tag::GpsDirectory] {
if let Ok(Some(val)) = decoder.current_ifd().find_tag(tag) {
if let Ok(ptr) = val.into_ifd_pointer() {
let _ = decoder.read_directory(ptr);
}
}
}
// Image-level probing — dimensions() triggers ensure_image,
// which lazily parses the directory as an Image
if decoder.dimensions().is_ok() {
let _ = decoder.colortype();
let _ = decoder.color_map();
let _ = decoder.get_chunk_type();
let _ = decoder.chunk_dimensions();
let _ = decoder.strip_count();
let _ = decoder.tile_count();
let _ = decoder.image_buffer_layout();
let _ = decoder.image_chunk_buffer_layout(0);
// Chunk reads at boundaries — first and last exercise
// different offset calculations
let _ = decoder.read_chunk(0);
let chunk_count = decoder
.strip_count()
.or_else(|_| decoder.tile_count())
.unwrap_or(0);
if chunk_count > 1 {
let _ = decoder.read_chunk(chunk_count - 1);
}
// Full image decode
let _ = decoder.read_image();
}
}
-29
View File
@@ -1,29 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let mut decoder = if let Ok(d) = tiff::decoder::Decoder::open(std::io::Cursor::new(data)) {
d
} else {
return;
};
let mut limits = tiff::decoder::Limits::default();
limits.decoding_buffer_size = 1_000_000;
limits.ifd_value_size = 1_000_000;
limits.intermediate_buffer_size = 1_000_000;
decoder = decoder.with_limits(limits);
loop {
if !decoder.more_images() {
break;
}
if let Err(_) = decoder.next_directory() {
break;
}
let _ = decoder.read_image();
}
});
+115 -14
View File
@@ -1,38 +1,139 @@
# TIFF byte order and magic
# === File headers ===
# Classic TIFF (LE, BE)
"\x49\x49\x2A\x00"
"\x4D\x4D\x00\x2A"
# BigTIFF (LE, BE) — version 43, 8-byte offsets, different parsing paths
"\x49\x49\x2B\x00\x08\x00\x00\x00"
"\x4D\x4D\x00\x2B\x00\x08\x00\x00"
# Common IFD tags as 2-byte LE pairs
# 256 = ImageWidth
# === IFD field type codes (2-byte LE) ===
# Decoder switches on these; wrong types exercise error paths
"\x01\x00"
"\x02\x00"
"\x03\x00"
"\x04\x00"
"\x05\x00"
"\x06\x00"
"\x07\x00"
"\x08\x00"
"\x09\x00"
"\x0A\x00"
"\x0B\x00"
"\x0C\x00"
"\x0D\x00"
"\x10\x00"
"\x11\x00"
"\x12\x00"
# === IFD tag numbers (2-byte LE) ===
# 256=ImageWidth 257=ImageLength 258=BitsPerSample
"\x00\x01"
# 257 = ImageLength
"\x01\x01"
# 258 = BitsPerSample
"\x02\x01"
# 259 = Compression
# 259=Compression 262=PhotometricInterpretation
"\x03\x01"
# 262 = PhotometricInterpretation
"\x06\x01"
# 273 = StripOffsets
# 273=StripOffsets 274=Orientation
"\x11\x01"
# 277 = SamplesPerPixel
"\x12\x01"
# 277=SamplesPerPixel 278=RowsPerStrip 279=StripByteCounts
"\x15\x01"
# 278 = RowsPerStrip
"\x16\x01"
# 279 = StripByteCounts
"\x17\x01"
# 282 = XResolution
# 282=XResolution 283=YResolution 284=PlanarConfiguration
"\x1A\x01"
# 283 = YResolution
"\x1B\x01"
"\x1C\x01"
# 317=Predictor 320=ColorMap
"\x3D\x01"
"\x40\x01"
# 322=TileWidth 323=TileLength 324=TileOffsets 325=TileByteCounts
"\x42\x01"
"\x43\x01"
"\x44\x01"
"\x45\x01"
# 330=SubIfd 338=ExtraSamples 339=SampleFormat
"\x4A\x01"
"\x52\x01"
"\x53\x01"
# 347=JPEGTables 530=ChromaSubsampling
"\x5B\x01"
"\x12\x02"
# 34665=ExifDirectory 34675=IccProfile 34853=GpsDirectory
"\x69\x87"
"\x73\x87"
"\x25\x88"
# === IFD tag numbers (2-byte BE) ===
# Common IFD tags as 2-byte BE pairs
"\x01\x00"
"\x01\x01"
"\x01\x02"
"\x01\x03"
"\x01\x06"
"\x01\x11"
"\x01\x12"
"\x01\x15"
"\x01\x16"
"\x01\x17"
"\x01\x1A"
"\x01\x1B"
"\x01\x1C"
"\x01\x3D"
"\x01\x40"
"\x01\x42"
"\x01\x43"
"\x01\x44"
"\x01\x45"
"\x01\x4A"
"\x01\x52"
"\x01\x53"
"\x01\x5B"
"\x02\x12"
"\x87\x69"
"\x87\x73"
"\x88\x25"
# === Compression codes (4-byte LE inline SHORT values) ===
# These are the values stored in the IFD entry for tag 259
# 1=None 2=Huffman 3=Fax3 4=Fax4 5=LZW
"\x01\x00\x00\x00"
"\x02\x00\x00\x00"
"\x03\x00\x00\x00"
"\x04\x00\x00\x00"
"\x05\x00\x00\x00"
# 6=OldJPEG 7=ModernJPEG 8=Deflate
"\x06\x00\x00\x00"
"\x07\x00\x00\x00"
"\x08\x00\x00\x00"
# 0x8005=PackBits 0x80B2=OldDeflate
"\x05\x80\x00\x00"
"\xB2\x80\x00\x00"
# === Useful small values (4-byte LE) ===
# BitsPerSample=16, =32 (8 already covered as Deflate compression above)
"\x10\x00\x00\x00"
"\x20\x00\x00\x00"
# === Minimal IFD entries (12-byte LE: tag + type SHORT + count 1 + value) ===
# These let the fuzzer splice complete valid entries into an IFD
# ImageWidth=1
"\x00\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# ImageLength=1
"\x01\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# BitsPerSample=8
"\x02\x01\x03\x00\x01\x00\x00\x00\x08\x00\x00\x00"
# Compression=None
"\x03\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# PhotometricInterpretation=BlackIsZero
"\x06\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# SamplesPerPixel=1
"\x15\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"
# RowsPerStrip=1
"\x16\x01\x03\x00\x01\x00\x00\x00\x01\x00\x00\x00"