Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions src/tables/colr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,7 @@ impl<'a> Table<'a> {
let mut recursion_stack = RecursionStack {
stack: [0; 64],
len: 0,
visits_left: MAX_PAINT_VISITS,
};

self.paint_impl(
Expand Down Expand Up @@ -1004,6 +1005,10 @@ impl<'a> Table<'a> {
return None;
}

// Total-visit budget exceeded (a DAG can revisit the same offset via different sibling
// branches without ever cycling on the active path -- see `RecursionStack::visits_left`).
recursion_stack.consume_visit().ok()?;

recursion_stack.push(offset).ok()?;
let result = self.parse_paint_impl(
offset,
Expand Down Expand Up @@ -1836,8 +1841,21 @@ struct RecursionStack {
// The limit of 64 is chosen arbitrarily and not from the spec. But we have to stop somewhere...
stack: [usize; 64],
len: usize,
// `stack`/`contains` only detect a cycle on the CURRENT root-to-leaf path (an entry is popped
// as soon as its call returns), so a paint graph shaped as a DAG -- the same offset reachable
// through multiple sibling branches, none of which individually cycles -- is not caught by it.
// A chain of `PaintColrLayers` records where every layer slot at each level points at one
// shared next-level record forces `branching^depth` calls while never re-entering the active
// path. This budget bounds the total number of `parse_paint` calls for one top-level `paint`
// call, independent of the DAG's branching factor.
visits_left: u32,
}

// The limit of 100_000 total paint-graph node visits is chosen arbitrarily, mirroring
// `glyf::MAX_COMPONENT_VISITS` -- real color-font paint graphs are expected to have at most a few
// hundred nodes.
const MAX_PAINT_VISITS: u32 = 100_000;

impl RecursionStack {
#[inline]
pub fn is_empty(&self) -> bool {
Expand Down Expand Up @@ -1869,6 +1887,17 @@ impl RecursionStack {
debug_assert!(!self.is_empty());
self.len -= 1;
}

#[inline]
pub fn consume_visit(&mut self) -> Result<(), ()> {
match self.visits_left.checked_sub(1) {
Some(left) => {
self.visits_left = left;
Ok(())
}
None => Err(()),
}
}
}

#[cfg(feature = "variable-fonts")]
Expand Down