mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 21:48:53 -06:00
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:
@@ -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() {
|
||||
|
||||
Vendored
+10
-1
@@ -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`]).
|
||||
|
||||
Vendored
+10
-1
@@ -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.
|
||||
|
||||
Vendored
+14
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+18
-1
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user