From 769a019e7da5976124fc8b57de1ff5011680e66d Mon Sep 17 00:00:00 2001 From: midasmatterhorn <248507632+midasmatterhorn@users.noreply.github.com> Date: Wed, 14 Oct 2026 22:55:46 +0300 Subject: [PATCH] pdf: bound CFF endchar seac recursion so a cyclic font can't overflow the stack --- crates/pdf/src/cff.rs | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/crates/pdf/src/cff.rs b/crates/pdf/src/cff.rs index 1c8c603e..79e68387 100644 --- a/crates/pdf/src/cff.rs +++ b/crates/pdf/src/cff.rs @@ -533,6 +533,10 @@ impl Cff { /// A glyph's outline in glyph space (`font_matrix` maps it to text space) and its advance. pub fn outline(&self, gid: u16) -> Option<(BezPath, f64)> { + self.outline_depth(gid, 0) + } + + fn outline_depth(&self, gid: u16, depth: usize) -> Option<(BezPath, f64)> { let r = self.charstrings.get(gid as usize)?.clone(); let fd = self.fdselect.get(gid as usize).copied().unwrap_or(0) as usize; let fd = fd.min(self.lsubrs.len() - 1); @@ -547,13 +551,17 @@ impl Cff { _ => self.widths[fd].0, }; let mut path = st.path; - // endchar seac: base and accent glyphs by standard code. - if let Some((adx, ady, b, a)) = st.seac { + // endchar seac: base and accent glyphs by standard code. The nesting is bounded like the + // Type 1 interpreter's: a font whose accented glyphs seac into each other (or into + // themselves) must not recurse until the stack overflows. + if let Some((adx, ady, b, a)) = st.seac + && depth < 2 + { let find = |code: u8| crate::encoding::Base::Standard.name(code).and_then(|n| self.gid_by_name(&n)); - if let Some((bp, _)) = find(b).and_then(|g| self.outline(g)) { + if let Some((bp, _)) = find(b).and_then(|g| self.outline_depth(g, depth + 1)) { path.extend(bp); } - if let Some((ap, _)) = find(a).and_then(|g| self.outline(g)) { + if let Some((ap, _)) = find(a).and_then(|g| self.outline_depth(g, depth + 1)) { path.extend(kurbo::Affine::translate((adx, ady)) * ap); } } @@ -1048,4 +1056,30 @@ mod tests { assert!((p.area().abs() - 250_000.0).abs() < 1e-6, "{}", p.area()); assert_eq!(p.bounding_box(), kurbo::Rect::new(50.0, 0.0, 550.0, 500.0)); } + + /// An `endchar` seac whose base and accent name its own glyph used to recurse until the + /// stack overflowed and aborted the app; the depth limit cuts it off like the Type 1 + /// interpreter's. + #[test] + fn seac_cycles_are_cut_off_not_a_stack_overflow() { + let enc = |v: i32| -> Vec { + if (-107..=107).contains(&v) { + vec![(v + 139) as u8] + } else { + let mut o = vec![28]; + o.extend_from_slice(&(v as i16).to_be_bytes()); + o + } + }; + // `w adx ady bchar achar endchar`: both components are 'A' (standard code 65), this glyph. + let mut cs = vec![]; + for v in [500, 0, 0, 65, 65] { + cs.extend(enc(v)); + } + cs.push(14); + let font = write_test_cff(&[(".notdef", vec![14]), ("A", cs)]); + let cff = Cff::parse(&font).unwrap(); + let (p, _) = cff.outline(cff.gid_by_name("A").unwrap()).unwrap(); + assert!(p.elements().is_empty()); + } }