chore: replace decode_image fuzz target with comprehensive decoder exerciser
Replace decode_image with decode_comprehensive, which exercises per-IFD via a probe_directory helper: - Tag iteration and value parsing for every tag - SubIFD, EXIF, and GPS directory traversal via read_directory - Image metadata (dimensions, colortype, color_map, chunk info) - Layout queries (image_buffer_layout, image_chunk_buffer_layout) - Per-chunk reads at first and last indices - Full image decode via read_image Three IFD traversal strategies: - Linear iteration via next_directory (tolerates invalid intermediate IFDs) - Random-access via seek_to_directory (find_nth_ifd code path) - Image-aware random-access via seek_to_image Dictionary covers classic TIFF and BigTIFF byte order marks, and common IFD tags in both LE and BE (image geometry, strips, tiles, SubIFD, EXIF, GPS, ColorMap). Strict resource limits (512 KB decode/intermediate, 64 KB IFD). Bounded iteration (max 8 linear, max 4 seek).
This commit is contained in:
1 parent
e8103509fa
commit
a59db23341
6 files changed
+193
-82
No files matched your search
@@ -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,4 +1,3 @@
|
||||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
Reference in new issue
Block a user