From dd01ae03f769dfcaa20076f2af8c828cc1279a62 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 21:51:22 -0800 Subject: [PATCH] PSD: the slices' version 6 descriptors share one limit of values Each record's look-ahead descriptor read started with its own allowance of 200,000 values, so a file with 10,000 records could make one Open read about two billion. The descriptors of a slices resource now spend one allowance between them, and a look-ahead that runs it out refuses the slices with a warning instead of being read past. --- crates/engine/src/cmd/fileio/psdslices.rs | 32 ++++++++++++------- .../engine/src/cmd/fileio/tests_psdslices.rs | 18 +++++++++++ 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/engine/src/cmd/fileio/psdslices.rs b/crates/engine/src/cmd/fileio/psdslices.rs index f5291c16e5..ea87f70797 100644 --- a/crates/engine/src/cmd/fileio/psdslices.rs +++ b/crates/engine/src/cmd/fileio/psdslices.rs @@ -29,9 +29,12 @@ const MAX_SLICES: usize = 10_000; /// The deepest descriptor objects and lists nest. const MAX_DEPTH: usize = 16; -/// The most values one descriptor holds, nested ones included. +/// The most values the descriptors of one slices resource hold together, nested ones included. const MAX_VALUES: usize = 200_000; +/// The error of descriptors that hold more than [`MAX_VALUES`]. +const TOO_MANY_VALUES: &str = "the slices' descriptor holds too many values"; + /// One slice as the file stores it. #[derive(Clone, Debug, PartialEq)] struct PsdSlice { @@ -60,13 +63,14 @@ fn read(bytes: &[u8]) -> Result, String> { /// The slices of resource 1050's `data`. fn decode(data: &[u8]) -> Result, String> { let mut r = Reader::new(data); + // The values every descriptor of the resource may hold together. + let mut budget = MAX_VALUES; match r.u32()? { - 6 => version_6(&mut r), + 6 => version_6(&mut r, &mut budget), 7 | 8 => { if r.u32()? != 16 { return Err("the slices' descriptor has a version other than 16".into()); } - let mut budget = MAX_VALUES; let d = descriptor(&mut r, 0, &mut budget)?; from_descriptor(&d) } @@ -75,7 +79,8 @@ fn decode(data: &[u8]) -> Result, String> { } /// Version 6: the bounds of all the slices, the group's name, the count, then each slice's record. -fn version_6(r: &mut Reader<'_>) -> Result, String> { +/// The descriptors that follow the records spend one `budget` of values between them. +fn version_6(r: &mut Reader<'_>, budget: &mut usize) -> Result, String> { r.take(16)?; unicode(r)?; let count = r.u32()? as usize; @@ -111,13 +116,18 @@ fn version_6(r: &mut Reader<'_>) -> Result, String> { format!("#{red:02x}{green:02x}{blue:02x}") }; // Photoshop 7 and later may follow a record with a descriptor (version 16); a record that - // doesn't start one is the next slice. + // doesn't start one is the next slice. A descriptor read that uses up the budget is an + // error. if r.rest().starts_with(&16u32.to_be_bytes()) { let mut ahead = Reader::new(r.rest()); - let mut budget = MAX_VALUES; - if ahead.u32().is_ok() && descriptor(&mut ahead, 0, &mut budget).is_ok() { - let used = r.rest().len() - ahead.rest().len(); - r.take(used)?; + ahead.u32()?; + match descriptor(&mut ahead, 0, budget) { + Ok(_) => { + let used = r.rest().len() - ahead.rest().len(); + r.take(used)?; + } + Err(e) if e == TOO_MANY_VALUES => return Err(e), + Err(_) => {} } } // Origin 0 is a slice generated automatically. @@ -278,7 +288,7 @@ fn descriptor(r: &mut Reader<'_>, depth: usize, budget: &mut usize) -> Result, kind: &[u8; 4], depth: usize, budget: &mut usize) -> Result { - *budget = budget.checked_sub(1).ok_or("the slices' descriptor holds too many values")?; + *budget = budget.checked_sub(1).ok_or(TOO_MANY_VALUES)?; Ok(match kind { b"long" => Value::Long(r.u32()? as i32), b"doub" => Value::Double(f64::from_be_bytes(r.array()?)), @@ -322,7 +332,7 @@ fn value(r: &mut Reader<'_>, kind: &[u8; 4], depth: usize, budget: &mut usize) - b"obj " => { let count = r.u32()? as usize; for _ in 0..count { - *budget = budget.checked_sub(1).ok_or("the slices' descriptor holds too many values")?; + *budget = budget.checked_sub(1).ok_or(TOO_MANY_VALUES)?; match &r.array::<4>()? { b"prop" => { class(r)?; diff --git a/crates/engine/src/cmd/fileio/tests_psdslices.rs b/crates/engine/src/cmd/fileio/tests_psdslices.rs index 0456ebe166..42dc33c600 100644 --- a/crates/engine/src/cmd/fileio/tests_psdslices.rs +++ b/crates/engine/src/cmd/fileio/tests_psdslices.rs @@ -262,6 +262,24 @@ fn photoshop_7_descriptors_after_version_6_records_are_read_past() { assert_eq!(names, ["a", "b"]); } +#[test] +fn version_6_descriptors_share_one_budget_of_values() { + // Each record's descriptor holds 2,501 values, well under the 200,000 one descriptor may hold; + // 100 of them hold 250,100 together. + let list = D::List((0..2_500).map(D::Long).collect()); + let trailing = [16u32.to_be_bytes().to_vec(), descriptor(&[("many", list)])].concat(); + let records: Vec = (0..100).map(|_| user("a", [0, 0, 5, 5])).collect(); + // Alone, one such descriptor is read past. + let (s, r) = open(&with_resource(&psd(false), 1050, &v6(&records[..1], &trailing)), json!({})); + assert!(warnings(&r).is_empty(), "{r}"); + assert_eq!(slices(&s).len(), 1); + // Together they are refused, and the file opens without slices. + let (s, r) = open(&with_resource(&psd(false), 1050, &v6(&records, &trailing)), json!({})); + let w = warnings(&r); + assert!(w.iter().any(|w| w.starts_with("the slices can't be read (") && w.contains("too many values")), "{w:?}"); + assert!(slices(&s).is_empty()); +} + #[test] fn version_7_and_8_descriptors_open_as_user_slices() { let slice = |origin: &'static str, kind: &'static str, l: i32, t: i32, r: i32, b: i32| {