mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #18 from storytold/pr/never-crash-psd
Never crash: PSD reader bounds overflow and input-sized allocations
This commit is contained in:
3 files changed
+93
-22
No files matched your search
@@ -118,7 +118,7 @@ pub(crate) fn read_id(r: &mut Reader) -> Result<String> {
|
||||
|
||||
pub(crate) fn read_unicode(r: &mut Reader) -> Result<String> {
|
||||
let n = r.u32()? as usize;
|
||||
let mut v = Vec::with_capacity(n);
|
||||
let mut v = Vec::with_capacity(n.min(r.remaining() / 2));
|
||||
for _ in 0..n {
|
||||
v.push(r.u16()?);
|
||||
}
|
||||
@@ -130,6 +130,12 @@ pub(crate) fn read_unicode(r: &mut Reader) -> Result<String> {
|
||||
|
||||
/// Read a descriptor (after its version field).
|
||||
pub fn read_descriptor(r: &mut Reader) -> Result<Descriptor> {
|
||||
read_descriptor_at(r, 0)
|
||||
}
|
||||
|
||||
/// [`read_descriptor`] nested `depth` values deep: the limit in [`read_value`] counts nested
|
||||
/// descriptors too, so a file can't recurse until the stack overflows.
|
||||
fn read_descriptor_at(r: &mut Reader, depth: usize) -> Result<Descriptor> {
|
||||
let class_name = read_unicode(r)?;
|
||||
let class_id = read_id(r)?;
|
||||
let n = r.u32()? as usize;
|
||||
@@ -140,7 +146,7 @@ pub fn read_descriptor(r: &mut Reader) -> Result<Descriptor> {
|
||||
for _ in 0..n {
|
||||
let key = read_id(r)?;
|
||||
let ty = r.tag()?;
|
||||
let v = read_value(r, &ty, 0)?;
|
||||
let v = read_value(r, &ty, depth)?;
|
||||
items.push((key, v));
|
||||
}
|
||||
Ok(Descriptor { class_name, class_id, items })
|
||||
@@ -151,7 +157,7 @@ fn read_value(r: &mut Reader, ty: &[u8; 4], depth: usize) -> Result<DValue> {
|
||||
return Err(Error::Invalid("descriptor nesting".into()));
|
||||
}
|
||||
Ok(match ty {
|
||||
b"Objc" | b"GlbO" => DValue::Descriptor(read_descriptor(r)?),
|
||||
b"Objc" | b"GlbO" => DValue::Descriptor(read_descriptor_at(r, depth + 1)?),
|
||||
b"VlLs" => {
|
||||
let n = r.u32()? as usize;
|
||||
if n > 1_000_000 {
|
||||
@@ -166,7 +172,7 @@ fn read_value(r: &mut Reader, ty: &[u8; 4], depth: usize) -> Result<DValue> {
|
||||
}
|
||||
b"ObAr" => {
|
||||
let n = r.u32()?;
|
||||
DValue::ObjectArray(n, read_descriptor(r)?)
|
||||
DValue::ObjectArray(n, read_descriptor_at(r, depth + 1)?)
|
||||
}
|
||||
b"doub" => DValue::Double(r.f64()?),
|
||||
b"UntF" => {
|
||||
@@ -416,6 +422,16 @@ impl Descriptor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deeply_nested_descriptors_error_instead_of_overflowing_the_stack() {
|
||||
// Each level: empty class name, a 4-byte class id, one item `key` of type `Objc`. The
|
||||
// depth limit restarted at every nested descriptor, so 100 000 levels (2.4 MB) recursed
|
||||
// until the stack overflowed.
|
||||
let level: Vec<u8> = [&0u32.to_be_bytes()[..], &0u32.to_be_bytes(), b"null", &1u32.to_be_bytes(), &0u32.to_be_bytes(), b"key ", b"Objc"].concat();
|
||||
let bytes = level.repeat(100_000);
|
||||
assert!(matches!(read_descriptor(&mut Reader::new(&bytes)), Err(Error::Invalid(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let d = Descriptor::new("null")
|
||||
|
||||
+39
-18
@@ -7,6 +7,8 @@
|
||||
//! out as straight (unpremultiplied) RGBA `f32` in 0..1, display-encoded (32-bit documents,
|
||||
//! which are linear, are encoded to sRGB).
|
||||
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod descriptor;
|
||||
pub mod engine_data;
|
||||
pub mod placed;
|
||||
@@ -41,6 +43,19 @@ impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// The most pixels one decoded image (the composite, a layer or a mask) may have: 2^28, e.g.
|
||||
/// 16 384 × 16 384, 4 GiB as float RGBA. Bounds read from a file are untrusted, so a few bytes
|
||||
/// can't make the reader allocate more than that.
|
||||
const MAX_DECODE_PIXELS: u64 = 1 << 28;
|
||||
|
||||
/// `w × h` as a pixel count, or an error when it exceeds [`MAX_DECODE_PIXELS`].
|
||||
fn decode_pixels(w: usize, h: usize) -> Result<usize> {
|
||||
match (w as u64).checked_mul(h as u64) {
|
||||
Some(n) if n <= MAX_DECODE_PIXELS => Ok(n as usize),
|
||||
_ => Err(Error::Unsupported(format!("{w}×{h} pixels is too large to decode"))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `bytes` start like a Photoshop document.
|
||||
pub fn is_psd(bytes: &[u8]) -> bool {
|
||||
bytes.len() >= 6 && &bytes[..4] == b"8BPS" && matches!(u16::from_be_bytes([bytes[4], bytes[5]]), 1 | 2)
|
||||
@@ -108,10 +123,10 @@ impl Rect {
|
||||
Rect { top, left, bottom: top + height, right: left + width }
|
||||
}
|
||||
pub fn width(&self) -> u32 {
|
||||
(self.right - self.left).max(0) as u32
|
||||
(i64::from(self.right) - i64::from(self.left)).clamp(0, i64::from(u32::MAX)) as u32
|
||||
}
|
||||
pub fn height(&self) -> u32 {
|
||||
(self.bottom - self.top).max(0) as u32
|
||||
(i64::from(self.bottom) - i64::from(self.top)).clamp(0, i64::from(u32::MAX)) as u32
|
||||
}
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.width() == 0 || self.height() == 0
|
||||
@@ -780,11 +795,12 @@ impl Psd {
|
||||
let end = offset.checked_add(len).filter(|&e| e <= self.data.len()).ok_or(Error::Truncated)?;
|
||||
let mut r = Reader::at(&self.data[..end], offset);
|
||||
let comp = r.u16()?;
|
||||
decode_pixels(w, h)?;
|
||||
let rowb = self.row_bytes(w);
|
||||
let raw = match comp {
|
||||
0 => r.bytes(rowb * h)?.to_vec(),
|
||||
1 => {
|
||||
let mut counts = Vec::with_capacity(h);
|
||||
let mut counts = Vec::with_capacity(h.min(r.remaining() / 2));
|
||||
for _ in 0..h {
|
||||
counts.push(if self.psb { r.u32()? as usize } else { r.u16()? as usize });
|
||||
}
|
||||
@@ -796,7 +812,7 @@ impl Psd {
|
||||
}
|
||||
2 | 3 => {
|
||||
let rest = r.bytes(r.remaining())?;
|
||||
let mut v = miniz_oxide::inflate::decompress_to_vec_zlib(rest).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?;
|
||||
let mut v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?;
|
||||
v.resize(rowb * h, 0);
|
||||
if comp == 3 {
|
||||
self.unpredict(&mut v, w, h);
|
||||
@@ -891,6 +907,7 @@ impl Psd {
|
||||
/// The merged (flattened) image.
|
||||
pub fn composite(&self) -> Result<Pixels> {
|
||||
let (w, h) = (self.width as usize, self.height as usize);
|
||||
decode_pixels(w, h)?;
|
||||
let mut r = Reader::at(&self.data, self.merged_offset);
|
||||
let comp = r.u16()?;
|
||||
let nch = self.channels as usize;
|
||||
@@ -905,7 +922,7 @@ impl Psd {
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let mut counts = Vec::with_capacity(nch * h);
|
||||
let mut counts = Vec::with_capacity((nch * h).min(r.remaining() / 2));
|
||||
for _ in 0..nch * h {
|
||||
counts.push(if self.psb { r.u32()? as usize } else { r.u16()? as usize });
|
||||
}
|
||||
@@ -919,7 +936,7 @@ impl Psd {
|
||||
}
|
||||
2 | 3 => {
|
||||
let rest = r.bytes(r.remaining())?;
|
||||
let v = miniz_oxide::inflate::decompress_to_vec_zlib(rest).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?;
|
||||
let v = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(rest, want * rowb * h).map_err(|e| Error::Invalid(format!("zip: {e:?}")))?;
|
||||
for (c, p) in planes.iter_mut().enumerate().take(want) {
|
||||
let mut plane = v.get(c * rowb * h..(c + 1) * rowb * h).ok_or(Error::Truncated)?.to_vec();
|
||||
if comp == 3 {
|
||||
@@ -956,7 +973,7 @@ impl Psd {
|
||||
fill_rect
|
||||
};
|
||||
let (ow, oh) = (out_rect.width() as usize, out_rect.height() as usize);
|
||||
let mut out = vec![[0.0f32; 4]; ow * oh];
|
||||
let mut out = vec![[0.0f32; 4]; decode_pixels(ow, oh)?];
|
||||
let src = fill_rect;
|
||||
let (sw, sh) = (src.width() as usize, src.height() as usize);
|
||||
if sw > 0 && sh > 0 {
|
||||
@@ -972,22 +989,26 @@ impl Psd {
|
||||
}
|
||||
}
|
||||
let fill = l.fill.map(|c| c.map(|v| v as f32));
|
||||
for y in 0..sh {
|
||||
let dy = src.top + y as i32 - out_rect.top;
|
||||
if dy < 0 || dy >= oh as i32 {
|
||||
continue;
|
||||
}
|
||||
for x in 0..sw {
|
||||
let dx = src.left + x as i32 - out_rect.left;
|
||||
if dx < 0 || dx >= ow as i32 {
|
||||
continue;
|
||||
}
|
||||
// The source rows / columns that land inside the output (64-bit: file bounds can span
|
||||
// the whole i32 range).
|
||||
let span = |s0: i32, n: usize, o0: i32, on: usize| {
|
||||
let off = i64::from(o0) - i64::from(s0);
|
||||
let lo = off.clamp(0, n as i64);
|
||||
let hi = (off + on as i64).clamp(lo, n as i64);
|
||||
(lo as usize..hi as usize, off)
|
||||
};
|
||||
let (ys, oy) = span(src.top, sh, out_rect.top, oh);
|
||||
let (xs, ox) = span(src.left, sw, out_rect.left, ow);
|
||||
for y in ys {
|
||||
let dy = (y as i64 - oy) as usize;
|
||||
for x in xs.clone() {
|
||||
let dx = (x as i64 - ox) as usize;
|
||||
let i = y * sw + x;
|
||||
let (rgb, a) = match fill {
|
||||
Some(f) if !l.has_pixels() => (f, 1.0),
|
||||
_ => (self.to_rgb(&planes, i), alpha.as_ref().map_or(1.0, |a| a[i])),
|
||||
};
|
||||
out[dy as usize * ow + dx as usize] = [rgb[0], rgb[1], rgb[2], a];
|
||||
out[dy * ow + dx] = [rgb[0], rgb[1], rgb[2], a];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,3 +218,37 @@ fn smart_objects_and_embedded_files() {
|
||||
assert_eq!((e.width, e.height), (20, 10));
|
||||
assert!(p.layers[0].smart_object.is_none());
|
||||
}
|
||||
|
||||
/// Replace the first layer record's bounds (`top, left, bottom, right`) in a written document.
|
||||
fn patch_layer_rect(bytes: &mut [u8], from: Rect, to: Rect) {
|
||||
let enc = |r: Rect| [r.top, r.left, r.bottom, r.right].iter().flat_map(|v| v.to_be_bytes()).collect::<Vec<u8>>();
|
||||
let (old, new) = (enc(from), enc(to));
|
||||
let at = bytes.windows(16).position(|w| w == old.as_slice()).expect("layer rect in the written file");
|
||||
bytes[at..at + 16].copy_from_slice(&new);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extreme_layer_bounds_error_instead_of_overflowing() {
|
||||
// A layer spanning the whole i32 range: its width overflowed i32 subtraction.
|
||||
let r = Rect::new(2, 3, 4, 5);
|
||||
let mut d = WDoc::new(8, 8);
|
||||
d.layers = vec![WLayer::solid("L", r, [1.0, 0.0, 0.0, 1.0])];
|
||||
let mut bytes = write(&d);
|
||||
patch_layer_rect(&mut bytes, r, Rect { top: i32::MIN, left: i32::MIN, bottom: i32::MAX, right: i32::MAX });
|
||||
let p = Psd::parse(bytes).unwrap();
|
||||
assert_eq!(p.layers[0].rect.width(), u32::MAX);
|
||||
assert!(p.layer_pixels(0, false).is_err());
|
||||
assert!(p.layer_pixels(0, true).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn huge_layer_bounds_error_instead_of_allocating() {
|
||||
// A few bytes claiming a 2-billion-pixel-square layer must not try to allocate it.
|
||||
let r = Rect::new(2, 3, 4, 5);
|
||||
let mut d = WDoc::new(8, 8);
|
||||
d.layers = vec![WLayer::solid("L", r, [1.0, 0.0, 0.0, 1.0])];
|
||||
let mut bytes = write(&d);
|
||||
patch_layer_rect(&mut bytes, r, Rect { top: 0, left: 0, bottom: i32::MAX, right: i32::MAX });
|
||||
let p = Psd::parse(bytes).unwrap();
|
||||
assert!(matches!(p.layer_pixels(0, false), Err(Error::Unsupported(_))));
|
||||
}
|
||||
Reference in new issue
Block a user