diff --git a/crates/codecs/src/tiff_codec.rs b/crates/codecs/src/tiff_codec.rs index b077c214a..1bbfa8e6c 100644 --- a/crates/codecs/src/tiff_codec.rs +++ b/crates/codecs/src/tiff_codec.rs @@ -95,6 +95,11 @@ pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result { fn limits(opts: &DecodeOptions) -> Limits { let mut limits = Limits::default(); limits.decoding_buffer_size = opts.max_pixels.saturating_mul(4 * 8).min(usize::MAX as u64) as usize; + // A strip or tile whose declared byte count exceeds this is refused: the default 128 MiB refuses a + // single-strip uncompressed 16-bit export of ~22 MP (#1983). Raised to the decoded image's cap + // (32 bytes per allowed pixel), never lowered: a strip or tile can outgrow the pixels it shows + // (JPEG headers, padding to the tile size). + limits.intermediate_buffer_size = limits.intermediate_buffer_size.max(limits.decoding_buffer_size); limits.ifd_value_size = 32 << 20; limits } diff --git a/crates/codecs/tests/roundtrip.rs b/crates/codecs/tests/roundtrip.rs index 3391e3a91..d802366c3 100644 --- a/crates/codecs/tests/roundtrip.rs +++ b/crates/codecs/tests/roundtrip.rs @@ -263,6 +263,90 @@ fn tiff_cmyk_and_orientation() { assert!(red[0] > 0.99 && red[1] < 0.01 && red[2] < 0.01, "{red:?}"); } +/// An uncompressed 16-bit RGB export in a single strip, the shape of the file in #1983 (3923 × 5877, +/// 138 MB). Its one strip is larger than the `tiff` crate's default 128 MiB intermediate buffer, so +/// the thumbnail (and the full decode) failed with "decoder limits exceeded". +#[test] +fn tiff_single_strip_larger_than_128_mib() { + use tiff::encoder::{TiffEncoder, colortype}; + let (w, h) = (3923u32, 5877u32); + let mut cur = std::io::Cursor::new(Vec::new()); + { + let mut enc = TiffEncoder::new(&mut cur).unwrap(); + let mut im = enc.new_image::(w, h).unwrap(); + im.rows_per_strip(h).unwrap(); + im.write_data(&vec![0x8000u16; (w * h * 3) as usize]).unwrap(); + } + let bytes = cur.into_inner(); + assert!(bytes.len() > 128 << 20); + let t = decode_thumbnail(&bytes, 256).unwrap(); + assert_eq!((t.source_width, t.source_height), (w, h)); + let px = t.image.get(10, 10); + assert!(px[0].abs_diff(128) <= 2 && px[0] == px[1] && px[1] == px[2], "{px:?}"); +} + +/// A little-endian TIFF with one IFD of single-value `(tag, type, value)` entries, followed by `data`. +fn ifd_tiff(entries: &[(u16, u16, u32)], data: &[u8]) -> Vec { + let mut b = b"II*\0".to_vec(); + b.extend(8u32.to_le_bytes()); + b.extend((entries.len() as u16).to_le_bytes()); + for &(tag, ty, value) in entries { + b.extend(tag.to_le_bytes()); + b.extend(ty.to_le_bytes()); + b.extend(1u32.to_le_bytes()); + b.extend(value.to_le_bytes()); + } + b.extend(0u32.to_le_bytes()); + b.extend_from_slice(data); + b +} + +/// A strip or tile can be larger than the pixels it shows (a JPEG header, padding to the tile size), so +/// a small `max_pixels` must not shrink the intermediate buffer below the `tiff` crate's 128 MiB +/// default. Both files below were refused with "decoder limits exceeded" when the buffer was capped at +/// 32 bytes per allowed pixel. Fixtures from AlexChambers' review of #1989. +#[test] +fn tiff_small_pixel_budget_reads_headers_and_padded_tiles() { + let opts = |max_pixels| DecodeOptions { max_size: None, max_pixels }; + + // 1 × 1 gray, one JPEG-compressed strip (~400 bytes for one pixel), with a one-pixel budget. + let mut jpeg = Vec::new(); + jpeg_encoder::Encoder::new(&mut jpeg, 90).encode(&[128], 1, 1, jpeg_encoder::ColorType::Luma).unwrap(); + let at = 8 + 2 + 10 * 12 + 4; + let entries = [ + (256, 4, 1), + (257, 4, 1), + (258, 3, 8), + (259, 3, 7), + (262, 3, 1), + (273, 4, at), + (277, 3, 1), + (278, 4, 1), + (279, 4, jpeg.len() as u32), + (284, 3, 1), + ]; + let d = decode(&ifd_tiff(&entries, &jpeg), opts(1)).unwrap(); + assert_eq!((d.image.width, d.image.height), (1, 1)); + + // 16 × 16 gray stored in one 256 × 256 tile (64 KiB), with a budget of exactly 256 pixels. + let at = 8 + 2 + 11 * 12 + 4; + let entries = [ + (256, 4, 16), + (257, 4, 16), + (258, 3, 8), + (259, 3, 1), + (262, 3, 1), + (277, 3, 1), + (284, 3, 1), + (322, 4, 256), + (323, 4, 256), + (324, 4, at), + (325, 4, 65536), + ]; + let d = decode(&ifd_tiff(&entries, &[128; 65536]), opts(256)).unwrap(); + assert_eq!((d.image.width, d.image.height), (16, 16)); +} + /// A one-strip, uncompressed, single-sample TIFF (little-endian) whose samples are `bits` wide and /// already packed into `strip`, one padded row after another. fn packed_tiff(width: u16, height: u16, bits: u16, photometric: u16, strip: &[u8]) -> Vec {