Merge PR #3386: Never crash: strip byte counts near u64::MAX don't overflow their sum in ORF, SRW, ARW and PEF

docs/whats-new.md: both entries under 2026-10-30
This commit is contained in:
aiongg
2026-10-31 02:10:25 +00:00
6 changed files with 92 additions and 5 deletions
+34
View File
@@ -246,6 +246,40 @@ pub(crate) fn declare_strip_len(file: &mut [u8], len: u32, declared: u32) {
#[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<u8>) -> Vec<u8> {
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() {
+10 -1
View File
@@ -628,7 +628,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
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.
@@ -906,6 +906,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`]).
+10 -1
View File
@@ -206,7 +206,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
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.
+14 -2
View File
@@ -267,14 +267,14 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
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::<u64>().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);
}
}
}
}
+18 -1
View File
@@ -97,7 +97,7 @@ pub(crate) fn decode(bytes: &[u8], mode: Mode) -> Result<RawImage> {
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)",
@@ -358,6 +358,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<u8> {
let mut levels = vec![0i64; 4];
+6
View File
@@ -11,6 +11,11 @@
- Every pixel of a render goes through every red eye correction, so a sidecar, preset or agent with thousands of
them could stall a preview or export for hours. A photo now renders its first 64, and Red Eye refuses a 65th.
### 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-29
### The histogram's clipping buttons read in light themes
@@ -53,6 +58,7 @@
### The histogram stays in view
- The histogram stays at the top of the Edit panel while its sliders scroll under it (issue #2679). It used to scroll
away with them, so it was out of sight while using the sliders further down the panel.
### No maze pattern in noisy raws
- Flat areas of high-ISO raws, such as a dim wall or a blurred background, could show short horizontal and vertical
strokes instead of grain, often orange and blue under warm light. Noise reduction made them stand out more, because