From 1c906ea3db250369860ba1357ed1dc50935bbf27 Mon Sep 17 00:00:00 2001 From: Bruno-Alumn <171815746+Bruno-Alumn@users.noreply.github.com> Date: Fri, 30 Oct 2026 13:37:16 +0100 Subject: [PATCH] Never crash: strip byte counts near u64::MAX don't overflow their sum in ORF, SRW, ARW and PEF The TIFF reader accepts LONG8 (type 16) StripByteCounts in classic TIFF, so a file can declare strips of u64::MAX and 1 bytes. The ORF, SRW, ARW and PEF decoders added the strip lengths with Iterator::sum::(), which panics on overflow in debug builds. They now add them with a saturating fold, as raw/src/lib.rs already does, so such a file is refused or read as far as its data go. The tests rewrite a synthetic file of each decoder into two strips with LONG8 counts [u64::MAX, 1] (vendor::tests::huge_strip_counts); PEF is checked for the Huffman-coded and the uncompressed path. --- crates/raw/src/vendor.rs | 34 ++++++++++++++++++++++++++++++++++ crates/raw/src/vendor/arw.rs | 11 ++++++++++- crates/raw/src/vendor/orf.rs | 11 ++++++++++- crates/raw/src/vendor/pef.rs | 16 ++++++++++++++-- crates/raw/src/vendor/srw.rs | 19 ++++++++++++++++++- docs/whats-new.md | 7 +++++++ 6 files changed, 93 insertions(+), 5 deletions(-) diff --git a/crates/raw/src/vendor.rs b/crates/raw/src/vendor.rs index b0f65f49e..07e8e42fc 100644 --- a/crates/raw/src/vendor.rs +++ b/crates/raw/src/vendor.rs @@ -228,6 +228,40 @@ fn solve4(mut a: [[f64; 4]; 4], mut b: [f64; 4]) -> Option<[f64; 4]> { #[cfg(test)] mod tests { use super::*; + use lightcraft_tiff::tags as t; + + /// `file`, whose image IFD holds one strip, as two strips at the same offset whose StripByteCounts are LONG8 (which + /// classic TIFF readers accept) `[u64::MAX, 1]`: their sum is past `u64::MAX`. + pub(super) fn huge_strip_counts(mut file: Vec) -> Vec { + let tiff = Tiff::parse(&file).unwrap(); + let o = tiff.order; + let ifd = tiff.all_ifds().into_iter().find(|i| i.contains(t::STRIP_OFFSETS)).unwrap(); + let (base, rows, offset) = (ifd.offset as usize, ifd.u32(t::IMAGE_LENGTH).unwrap(), ifd.u32(t::STRIP_OFFSETS).unwrap()); + assert!(rows >= 2); + let at = file.len() as u32; + // appended: the two strip offsets (LONG), then the two byte counts (LONG8) + let mut tail = Vec::new(); + o.put_u32(&mut tail, offset); + o.put_u32(&mut tail, offset); + o.put_u64(&mut tail, u64::MAX); + o.put_u64(&mut tail, 1); + for i in 0..usize::from(o.read_u16(&file, base).unwrap()) { + let e = base + 2 + 12 * i; + let (ty, count, value) = match o.read_u16(&file, e).unwrap() { + t::STRIP_OFFSETS => (4u16, 2u32, at), + t::ROWS_PER_STRIP => (4, 1, rows.div_ceil(2)), + t::STRIP_BYTE_COUNTS => (16, 2, at + 8), + _ => continue, + }; + let mut entry = Vec::new(); + o.put_u16(&mut entry, ty); + o.put_u32(&mut entry, count); + o.put_u32(&mut entry, value); + file[e + 2..e + 12].copy_from_slice(&entry); + } + file.extend_from_slice(&tail); + file + } #[test] fn black_columns_by_parity() { diff --git a/crates/raw/src/vendor/arw.rs b/crates/raw/src/vendor/arw.rs index 3938fa50d..76f13fa2d 100644 --- a/crates/raw/src/vendor/arw.rs +++ b/crates/raw/src/vendor/arw.rs @@ -628,7 +628,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { let bits = info.bits() as u32; let linear_rgb = info.compression == 7 && info.photometric == photometric::YCBCR; let chunks = info.chunks(bytes.len() as u64); - let strip_len: u64 = chunks.iter().map(|c| c.len).sum(); + let strip_len = chunks.iter().fold(0u64, |a, c| a.saturating_add(c.len)); // The row-per-w-bytes (ARW2) layout stores exactly one byte per pixel in a single strip; all 108 such files in the test // archive measure exactly width x height. A200/A230/A350 files carry a strip 4-23 % larger than that (and A290/A390 one // smaller), which is a different packing, so they are refused rather than read with the wrong layout. @@ -900,6 +900,15 @@ mod tests { TiffWriter::default().write(&[raw]).unwrap() } + /// Strip byte counts that add up past `u64::MAX` (LONG8 `[u64::MAX, 1]`) are an error or a picture, not an overflow. + #[test] + fn strip_byte_counts_past_u64_max_do_not_overflow() { + let file = crate::vendor::tests::huge_strip_counts(arw_with_strip(32 * 4)); + for mode in [Mode::Header, Mode::Full] { + let _ = super::decode(&file, mode); + } + } + /// The ILCE-7RM6's distortion table reaches `OpcodeList3`, in the headers-only probe and in the full decode, as /// one plane, also with a CA table (lateral CA is off by default); another body's tables don't (see /// [`super::super::arw_lens_rm6`]). diff --git a/crates/raw/src/vendor/orf.rs b/crates/raw/src/vendor/orf.rs index 92caeec26..172f83013 100644 --- a/crates/raw/src/vendor/orf.rs +++ b/crates/raw/src/vendor/orf.rs @@ -206,7 +206,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { let (w, h) = (info.width as usize, info.height as usize); let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("image too large"))?; let chunks = info.chunks(bytes.len() as u64); - let total: u64 = chunks.iter().map(|c| c.len).sum(); + let total = chunks.iter().fold(0u64, |a, c| a.saturating_add(c.len)); let stated = cfa_from_exif(&tiff); let mut two_field = false; let (mut data, bits) = if info.compression != 1 { @@ -394,6 +394,15 @@ mod tests { b } + /// Strip byte counts that add up past `u64::MAX` (LONG8 `[u64::MAX, 1]`) are an error or a picture, not an overflow. + #[test] + fn strip_byte_counts_past_u64_max_do_not_overflow() { + let file = crate::vendor::tests::huge_strip_counts(orf(4, 2, 16, vec![0; 16])); + for mode in [Mode::Header, Mode::Full] { + let _ = super::decode(&file, mode); + } + } + /// An old-style little-endian Olympus maker note (`OLYMP\0`, version 1, then the IFD) of SHORT (type 3) or /// UNDEFINED (type 7) entries. Values longer than four bytes follow the IFD; their offsets count from the TIFF /// header, so `at` is where the note sits in the file. diff --git a/crates/raw/src/vendor/pef.rs b/crates/raw/src/vendor/pef.rs index 9b7d92bc7..45efeec68 100644 --- a/crates/raw/src/vendor/pef.rs +++ b/crates/raw/src/vendor/pef.rs @@ -267,14 +267,14 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { let chunks = info.chunks(bytes.len() as u64); let first = chunks.first().ok_or_else(|| RawError::Corrupt("PEF without image data".into()))?; let mut c = *first; - c.len = chunks.iter().map(|c| c.len).sum::().max(first.len); + c.len = chunks.iter().fold(0u64, |a, c| a.saturating_add(c.len)).max(first.len); let src = chunk_bytes(bytes, &c) .or_else(|| bytes.get(first.offset as usize..)) .ok_or_else(|| RawError::Corrupt("PEF data outside file".into()))?; if read == Mode::Full { RawData::U16(decode_huffman(src, &huff, w, hgt, bits)?) } else { RawData::U16(Vec::new()) } } 1 => { - let strip: u64 = info.chunks(bytes.len() as u64).iter().map(|c| c.len).sum(); + let strip = info.chunks(bytes.len() as u64).iter().fold(0u64, |a, c| a.saturating_add(c.len)); let packing = if strip >= (w * hgt * 2) as u64 { Packing::Word16 } else { Packing::Msb }; read_image_in(read, bytes, &info, tiff.order, packing)? } @@ -707,4 +707,16 @@ mod tests { let file = pef_with_black_point(w, h, 64, |x, y| 64 + ((x * 7 + y * 13) % 5) as u16); assert_eq!(decode(&file, Mode::Full).unwrap().active_area, Rect::new(0, 0, w, h)); } + + /// Strip byte counts that add up past `u64::MAX` (LONG8 `[u64::MAX, 1]`) are an error or a picture, not an overflow, + /// for the Huffman-coded and the uncompressed samples. + #[test] + fn strip_byte_counts_past_u64_max_do_not_overflow() { + for compression in [65535u16, 1] { + let file = crate::vendor::tests::huge_strip_counts(pef_with_pattern(compression, [0, 1, 1, 2])); + for mode in [Mode::Header, Mode::Full] { + let _ = decode(&file, mode); + } + } + } } diff --git a/crates/raw/src/vendor/srw.rs b/crates/raw/src/vendor/srw.rs index b22faeec0..0931c208d 100644 --- a/crates/raw/src/vendor/srw.rs +++ b/crates/raw/src/vendor/srw.rs @@ -97,7 +97,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result { let (w, h) = (info.width as usize, info.height as usize); let n = w.checked_mul(h).filter(|n| *n > 0 && *n <= crate::MAX_SAMPLES).ok_or(RawError::Limit("image too large"))?; let chunks = info.chunks(bytes.len() as u64); - let strip: u64 = chunks.iter().map(|c| c.len).sum(); + let strip = chunks.iter().fold(0u64, |a, c| a.saturating_add(c.len)); let Some(coding) = coding(info.compression, w, h, strip).filter(|_| chunks.len() == 1) else { return Err(RawError::Unsupported(format!( "Samsung compressed raw (compression {}, {:.2} stored bits per sample)", @@ -355,6 +355,23 @@ mod tests { TiffWriter::new(lightcraft_tiff::ByteOrder::Big, false).write(&[ifd0]).unwrap() } + /// Strip byte counts that add up past `u64::MAX` (LONG8 `[u64::MAX, 1]`) are an error or a picture, not an overflow. + #[test] + fn strip_byte_counts_past_u64_max_do_not_overflow() { + let file = crate::vendor::tests::huge_strip_counts(srw(Spec { + w: 4, + h: 2, + compression: 32769, + strip: vec![0; 16], + pattern: None, + note: None, + picture: None, + })); + for mode in [Mode::Header, Mode::Full] { + let _ = super::decode(&file, mode); + } + } + /// The camera-stated description every test file carries: JPEG framing, white balance, levels. fn full_note(area: [u32; 4], white: i64, black: i64) -> Vec { let mut levels = vec![0i64; 4]; diff --git a/docs/whats-new.md b/docs/whats-new.md index c6c259200..b9076b3c1 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -1,5 +1,12 @@ # What's new in LightCraft +## 2026-10-30 + +### Reliability +- A damaged or crafted Olympus ORF, Samsung SRW, Sony ARW or Pentax PEF whose strip sizes add up past the largest + 64-bit number no longer crashes LightCraft (debug builds) when it is read: the file is refused, or read as far as + its data go. + ## 2026-10-24 ### Relative-date filters and smart albums follow the clock