diff --git a/README.md b/README.md index 62ff836..634b40a 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/fuzz/.gitignore b/fuzz/.gitignore index 1a45eee..a092511 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,4 +1,3 @@ target corpus artifacts -coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 90325f2..0563ca7 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -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" diff --git a/fuzz/fuzz_targets/decode_comprehensive.rs b/fuzz/fuzz_targets/decode_comprehensive.rs index b0920bc..8b35143 100644 --- a/fuzz/fuzz_targets/decode_comprehensive.rs +++ b/fuzz/fuzz_targets/decode_comprehensive.rs @@ -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(decoder: &mut Decoder) { + // 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(); + } +} diff --git a/fuzz/fuzz_targets/decode_image.rs b/fuzz/fuzz_targets/decode_image.rs deleted file mode 100644 index 7efea0d..0000000 --- a/fuzz/fuzz_targets/decode_image.rs +++ /dev/null @@ -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(); - } -}); diff --git a/fuzz/tiff.dict b/fuzz/tiff.dict index 3de14f5..00110af 100644 --- a/fuzz/tiff.dict +++ b/fuzz/tiff.dict @@ -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"