Skip to content

Commit f03027c

Browse files
fix: cap total paint-graph node visits in COLRv1 painting (#225)
The recursion stack bounds path depth and forbids repeats along one path, but not fan-out; layers_count is a u8, so a small font can force an enormous traversal. Adds a per-call visit budget.
1 parent 47c25ae commit f03027c

1 file changed

Lines changed: 29 additions & 0 deletions

File tree

src/tables/colr.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,7 @@ impl<'a> Table<'a> {
886886
let mut recursion_stack = RecursionStack {
887887
stack: [0; 64],
888888
len: 0,
889+
visits_left: MAX_PAINT_VISITS,
889890
};
890891

891892
self.paint_impl(
@@ -1004,6 +1005,10 @@ impl<'a> Table<'a> {
10041005
return None;
10051006
}
10061007

1008+
// Total-visit budget exceeded (a DAG can revisit the same offset via different sibling
1009+
// branches without ever cycling on the active path -- see `RecursionStack::visits_left`).
1010+
recursion_stack.consume_visit().ok()?;
1011+
10071012
recursion_stack.push(offset).ok()?;
10081013
let result = self.parse_paint_impl(
10091014
offset,
@@ -1836,8 +1841,21 @@ struct RecursionStack {
18361841
// The limit of 64 is chosen arbitrarily and not from the spec. But we have to stop somewhere...
18371842
stack: [usize; 64],
18381843
len: usize,
1844+
// `stack`/`contains` only detect a cycle on the CURRENT root-to-leaf path (an entry is popped
1845+
// as soon as its call returns), so a paint graph shaped as a DAG -- the same offset reachable
1846+
// through multiple sibling branches, none of which individually cycles -- is not caught by it.
1847+
// A chain of `PaintColrLayers` records where every layer slot at each level points at one
1848+
// shared next-level record forces `branching^depth` calls while never re-entering the active
1849+
// path. This budget bounds the total number of `parse_paint` calls for one top-level `paint`
1850+
// call, independent of the DAG's branching factor.
1851+
visits_left: u32,
18391852
}
18401853

1854+
// The limit of 100_000 total paint-graph node visits is chosen arbitrarily, mirroring
1855+
// `glyf::MAX_COMPONENT_VISITS` -- real color-font paint graphs are expected to have at most a few
1856+
// hundred nodes.
1857+
const MAX_PAINT_VISITS: u32 = 100_000;
1858+
18411859
impl RecursionStack {
18421860
#[inline]
18431861
pub fn is_empty(&self) -> bool {
@@ -1869,6 +1887,17 @@ impl RecursionStack {
18691887
debug_assert!(!self.is_empty());
18701888
self.len -= 1;
18711889
}
1890+
1891+
#[inline]
1892+
pub fn consume_visit(&mut self) -> Result<(), ()> {
1893+
match self.visits_left.checked_sub(1) {
1894+
Some(left) => {
1895+
self.visits_left = left;
1896+
Ok(())
1897+
}
1898+
None => Err(()),
1899+
}
1900+
}
18721901
}
18731902

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

0 commit comments

Comments
 (0)