Merge PR #1989: TIFF: read strips and tiles larger than 128 MiB

This commit is contained in:
Brandon Thomas
2026-10-18 09:20:37 -04:00
2 changed files with 89 additions and 0 deletions
+5
View File
@@ -95,6 +95,11 @@ pub(crate) fn decode(bytes: &[u8], opts: &DecodeOptions) -> Result<Decoded> {
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
}
+84
View File
@@ -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::<colortype::RGB16>(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<u8> {
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<u8> {