Skip to content

Commit e829e72

Browse files
authored
[skrifa] VARC: limit number of edges processed (#2032)
Add a work budget for both outline memory computation and drawing to prevent exponential fan-out. internal bug ref: b/537783570
1 parent 9bbb04d commit e829e72

1 file changed

Lines changed: 102 additions & 3 deletions

File tree

skrifa/src/outline/varc/mod.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
instance::Size,
1919
outline::{cff, glyf, metrics::GlyphHMetrics, pen::PathStyle, DrawError, OutlinePen},
2020
provider::MetadataProvider,
21-
GLYF_COMPOSITE_RECURSION_LIMIT,
21+
GLYF_COMPOSITE_RECURSION_LIMIT, MAX_GRAPH_EDGES,
2222
};
2323

2424
#[cfg(feature = "libm")]
@@ -39,6 +39,7 @@ struct Scratchpad {
3939
deltas: DeltaVec,
4040
axis_indices: AxisIndexVec,
4141
axis_values: AxisValueVec,
42+
edges_left: usize,
4243
}
4344

4445
impl Scratchpad {
@@ -47,6 +48,7 @@ impl Scratchpad {
4748
deltas: DeltaVec::new(),
4849
axis_indices: AxisIndexVec::new(),
4950
axis_values: AxisValueVec::new(),
51+
edges_left: MAX_GRAPH_EDGES,
5052
}
5153
}
5254
}
@@ -217,21 +219,29 @@ impl<'a> Outlines<'a> {
217219
coverage_index: u16,
218220
) -> Result<usize, ReadError> {
219221
let mut stack = GlyphStack::new();
220-
self.max_component_memory_for_glyph(glyph_id, coverage_index, &mut stack)
222+
let mut edges_left = MAX_GRAPH_EDGES;
223+
self.max_component_memory_for_glyph(glyph_id, coverage_index, &mut stack, &mut edges_left)
221224
}
222225

223226
fn max_component_memory_for_glyph(
224227
&self,
225228
glyph_id: GlyphId,
226229
coverage_index: u16,
227230
stack: &mut GlyphStack,
231+
edges_left: &mut usize,
228232
) -> Result<usize, ReadError> {
229233
if stack.contains(&glyph_id) {
230234
return Ok(0);
231235
}
236+
// HB returns success for both recursion and edge limits, so we do the same.
237+
// See <https://github.com/harfbuzz/harfbuzz/blob/0fef675a5ea4973ce49d6dd00c02e70ec409eee3/src/OT/Var/VARC/VARC.cc#L386>
232238
if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT {
233239
return Ok(0);
234240
}
241+
if *edges_left == 0 {
242+
return Ok(0);
243+
}
244+
*edges_left -= 1;
235245
stack.push(glyph_id);
236246
let mut max_memory = 0usize;
237247
let glyph = self.varc.glyph(coverage_index as usize)?;
@@ -241,7 +251,12 @@ impl<'a> Outlines<'a> {
241251
let component_memory = if component_gid == glyph_id {
242252
self.base.base_outline_memory(component_gid)
243253
} else if let Some(coverage_index) = self.coverage_index(component_gid)? {
244-
self.max_component_memory_for_glyph(component_gid, coverage_index, stack)?
254+
self.max_component_memory_for_glyph(
255+
component_gid,
256+
coverage_index,
257+
stack,
258+
edges_left,
259+
)?
245260
} else {
246261
self.base.base_outline_memory(component_gid)
247262
};
@@ -317,6 +332,10 @@ impl<'a> Outlines<'a> {
317332
if stack.len() >= GLYF_COMPOSITE_RECURSION_LIMIT {
318333
return Err(DrawError::RecursionLimitExceeded(glyph_id));
319334
}
335+
if scratch.edges_left == 0 {
336+
return Err(DrawError::RecursionLimitExceeded(glyph_id));
337+
}
338+
scratch.edges_left -= 1;
320339
let glyph = self.varc.glyph(coverage_index as usize)?;
321340
stack.push(glyph_id);
322341
let coverage = ctx.coverage;
@@ -1406,4 +1425,84 @@ mod tests {
14061425
&mut pen,
14071426
);
14081427
}
1428+
1429+
fn first_nested_varc_edge(outlines: &Outlines<'_>) -> Option<(GlyphId, u16)> {
1430+
for gid16 in outlines.coverage.iter() {
1431+
let gid: GlyphId = gid16.into();
1432+
let coverage_index = outlines.coverage.get(gid)?;
1433+
let glyph = outlines.varc.glyph(coverage_index as usize).ok()?;
1434+
for component in glyph.components() {
1435+
let component = component.ok()?;
1436+
// Pick an unconditional child edge so the traversal always attempts
1437+
// to recurse for this component.
1438+
if component.condition_index().is_none()
1439+
&& component.gid() != gid
1440+
&& outlines.coverage.get(component.gid()).is_some()
1441+
{
1442+
return Some((gid, coverage_index));
1443+
}
1444+
}
1445+
}
1446+
None
1447+
}
1448+
1449+
#[test]
1450+
fn draw_glyph_respects_total_edge_budget() {
1451+
let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1452+
let outlines = Outlines::new(&font).unwrap();
1453+
let (root_gid, root_cov_idx) = first_nested_varc_edge(&outlines)
1454+
.expect("expected at least one VARC->VARC component edge in fixture font");
1455+
let mut coords = CoordVec::new();
1456+
expand_coords(&mut coords, outlines.axis_count, &[]);
1457+
let ctx = VarcSharedContext {
1458+
font_coords: &coords,
1459+
size: Size::unscaled(),
1460+
path_style: PathStyle::default(),
1461+
coverage: &outlines.coverage,
1462+
var_store: outlines.var_store.as_ref(),
1463+
store_regions: outlines.var_store.as_ref().zip(outlines.regions.as_ref()),
1464+
};
1465+
let outline = outlines.outline(root_gid).unwrap().unwrap();
1466+
let mut memory = vec![0u8; outline.required_buffer_size()];
1467+
let mut pen = Vec::<PathElement>::new();
1468+
let mut stack = GlyphStack::new();
1469+
let mut scalar_cache = outlines
1470+
.scalar_cache_from_store(outlines.var_store.as_ref())
1471+
.unwrap();
1472+
let mut scratch = Scratchpad::new();
1473+
// Budget of one edge allows entering the root glyph only; any recursive
1474+
// component edge must be rejected as over budget.
1475+
scratch.edges_left = 1;
1476+
let result = outlines.draw_glyph(
1477+
root_gid,
1478+
root_cov_idx,
1479+
&coords,
1480+
Affine::IDENTITY,
1481+
&ctx,
1482+
&mut memory,
1483+
&mut pen,
1484+
&mut stack,
1485+
&mut scalar_cache,
1486+
&mut scratch,
1487+
);
1488+
assert!(
1489+
matches!(result, Err(DrawError::RecursionLimitExceeded(_))),
1490+
"expected RecursionLimitExceeded when edge budget is exhausted, got {result:?}"
1491+
);
1492+
}
1493+
1494+
#[test]
1495+
fn max_component_memory_respects_total_edge_budget() {
1496+
let font = FontRef::new(font_test_data::varc::CJK_6868).unwrap();
1497+
let outlines = Outlines::new(&font).unwrap();
1498+
let (root_gid, root_cov_idx) = first_nested_varc_edge(&outlines)
1499+
.expect("expected at least one VARC->VARC component edge in fixture font");
1500+
let mut stack = GlyphStack::new();
1501+
let mut edges_left = 0;
1502+
let memory = outlines
1503+
.max_component_memory_for_glyph(root_gid, root_cov_idx, &mut stack, &mut edges_left)
1504+
.unwrap();
1505+
assert_eq!(memory, 0);
1506+
assert_eq!(edges_left, 0);
1507+
}
14091508
}

0 commit comments

Comments
 (0)