From 4aad36cf199aa7d5dd568a695dd810e6dc665a43 Mon Sep 17 00:00:00 2001 From: aiongg Date: Thu, 22 Oct 2026 14:05:41 +0000 Subject: [PATCH] fix(create): refuse an indexed colour space whose base is indexed before following it Image export parsed an Indexed colour space's base before checking whether that base was itself Indexed. A base that refers back to the same array recursed until the stack overflowed. The base is now checked first; since Indexed is the only colour space that recurses, this also ends any cycle. --- crates/create/src/extract.rs | 11 +++++++++-- crates/create/src/tests.rs | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/crates/create/src/extract.rs b/crates/create/src/extract.rs index 768da4d1f1..bd5ab5271c 100644 --- a/crates/create/src/extract.rs +++ b/crates/create/src/extract.rs @@ -383,10 +383,17 @@ fn space(doc: &Document, cs: Option<&Object>) -> Result { Some(b"CalGray") => Ok(Space::Gray), Some(b"CalRGB") => Ok(Space::Rgb), Some(b"Indexed" | b"I") => { - let base = space(doc, a.get(1))?; - if matches!(base, Space::Indexed(..)) { + // Checked before recursing: Indexed is the only space that recurses, so refusing + // an Indexed base here also ends a cyclic base reference. + let nested = match a.get(1).map(|b| doc.resolve(b)).as_deref() { + Some(Object::Array(b)) => matches!(b.first().and_then(Object::as_name), Some(b"Indexed" | b"I")), + Some(Object::Name(n)) => matches!(n.as_slice(), b"Indexed" | b"I"), + _ => false, + }; + if nested { return Err("nested indexed colour spaces aren't valid".into()); } + let base = space(doc, a.get(1))?; let hival = match a.get(2).map(|value| doc.resolve(value)).as_deref() { Some(Object::Int(value)) if (0..=255).contains(value) => *value as u8, _ => return Err("an indexed colour space with an invalid hival".into()), diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 6356105699..a214977675 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -1181,3 +1181,19 @@ fn a_jpeg_with_an_exif_orientation_becomes_an_upright_page() { let img = image_of(&doc, 1); assert_eq!((img.int(b"Width"), img.int(b"Height")), (Some(60), Some(20))); } + +#[test] +fn a_cyclic_indexed_colour_space_is_refused() { + let mut doc = from_images(&[("flat.png".into(), png_bytes(false))]).unwrap(); + // 5 0 obj [/Indexed 5 0 R 255 <00>]: the base is the indexed space itself. + let cs = doc.add(Object::Null); + doc.set(cs, Object::Array(vec![Object::name("Indexed"), Object::Ref(cs), Object::Int(255), Object::String(PdfString::literal(vec![0]))])); + with_image(&mut doc, |_| { + let mut dict = image_dict(1, 1, "DeviceGray", 8); + dict.set(b"ColorSpace".to_vec(), Object::Ref(cs)); + Stream::from_raw(dict, vec![0]) + }); + let out = extract_images(&doc, &[0], 0); + assert!(out.images.is_empty()); + assert_eq!(out.skipped.len(), 1, "{:?}", out.skipped); +}