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:
Brandon Thomas authored and GitHub committed 2026-10-04 15:28:25 -04:00
commit 91a3b14cb2
3 files changed
+93 -22

No files matched your search

+20 -4
View File
@@ -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
View File
@@ -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];
}
}
}
+34
View File
@@ -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(_))));
}