From d1413d520928aa5709c5e9b1498315c3e1fa77b2 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 14:05:55 -0400 Subject: [PATCH] Never crash: bound PSD descriptor nesting The descriptor depth limit restarted at 0 inside every nested descriptor (Objc / GlbO / ObAr), so a crafted file of 100 000 nested descriptors (2.4 MB) recursed until the stack overflowed and aborted the app. The depth now carries through nested descriptors. Co-Authored-By: Claude Opus 5.5 --- crates/psd/src/descriptor.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/psd/src/descriptor.rs b/crates/psd/src/descriptor.rs index b2cebbf4..b28c3479 100644 --- a/crates/psd/src/descriptor.rs +++ b/crates/psd/src/descriptor.rs @@ -130,6 +130,12 @@ pub(crate) fn read_unicode(r: &mut Reader) -> Result { /// Read a descriptor (after its version field). pub fn read_descriptor(r: &mut Reader) -> Result { + 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 { 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 { 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 { 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 { } 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 = [&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")