Skip to content

Commit 29ae374

Browse files
authored
[skrifa] COLRv1: avoid unbalanced stack state on error (#2031)
When handling nodes that push/pop states (transforms, clips, layers), capture nested paint results and return after popping the relevant state. This ensures that clients always get a pop callback for every push. internal bug ref: b/538461733
1 parent e29b8b5 commit 29ae374

1 file changed

Lines changed: 135 additions & 23 deletions

File tree

skrifa/src/color/traversal.rs

Lines changed: 135 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,8 @@ pub(crate) fn traverse_with_callbacks<'a, P: ColorPainter>(
198198
if let Some(rect) = clipbox {
199199
state.painter.push_clip_box(rect);
200200
}
201-
202-
let result = traverse_with_callbacks(
203-
&state.resolve_paint(&base_glyph)?,
201+
let result = traverse_unresolved_paint(
202+
&base_glyph,
204203
state,
205204
&mut cycle_guard,
206205
recurse_depth + 1,
@@ -236,12 +235,7 @@ pub(crate) fn traverse_with_callbacks<'a, P: ColorPainter>(
236235
.ok_or(ReadError::MalformedData("expected a transform paint"))?
237236
.0,
238237
);
239-
let result = traverse_with_callbacks(
240-
&state.resolve_paint(next_paint)?,
241-
state,
242-
decycler,
243-
recurse_depth + 1,
244-
);
238+
let result = traverse_unresolved_paint(next_paint, state, decycler, recurse_depth + 1);
245239
state.painter.pop_transform();
246240
result
247241
}
@@ -251,27 +245,31 @@ pub(crate) fn traverse_with_callbacks<'a, P: ColorPainter>(
251245
backdrop_paint,
252246
} => {
253247
state.painter.push_layer(CompositeMode::SrcOver);
254-
let mut result = traverse_with_callbacks(
255-
&state.resolve_paint(backdrop_paint)?,
256-
state,
257-
decycler,
258-
recurse_depth + 1,
259-
);
260-
result?;
248+
let mut result =
249+
traverse_unresolved_paint(backdrop_paint, state, decycler, recurse_depth + 1);
250+
if result.is_err() {
251+
state.painter.pop_layer_with_mode(CompositeMode::SrcOver);
252+
return result;
253+
}
261254
state.painter.push_layer(*mode);
262-
result = traverse_with_callbacks(
263-
&state.resolve_paint(source_paint)?,
264-
state,
265-
decycler,
266-
recurse_depth + 1,
267-
);
255+
result = traverse_unresolved_paint(source_paint, state, decycler, recurse_depth + 1);
268256
state.painter.pop_layer_with_mode(*mode);
269257
state.painter.pop_layer_with_mode(CompositeMode::SrcOver);
270258
result
271259
}
272260
}
273261
}
274262

263+
fn traverse_unresolved_paint<'a, P: ColorPainter>(
264+
paint: &Paint<'a>,
265+
state: &mut TraversalState<'a, P>,
266+
decycler: &mut PaintDecycler,
267+
recurse_depth: usize,
268+
) -> Result<(), PaintError> {
269+
let resolved_paint = state.resolve_paint(paint)?;
270+
traverse_with_callbacks(&resolved_paint, state, decycler, recurse_depth)
271+
}
272+
275273
pub(crate) fn traverse_v0_range(
276274
range: &Range<usize>,
277275
instance: &ColrInstance,
@@ -304,7 +302,10 @@ mod tests {
304302
MetadataProvider,
305303
};
306304
use raw::types::GlyphId;
307-
use read_fonts::{types::BoundingBox, FontRef, TableProvider};
305+
use read_fonts::{
306+
types::{BoundingBox, GlyphId16},
307+
FontRef, TableProvider,
308+
};
308309

309310
#[test]
310311
fn clipbox_test() {
@@ -394,6 +395,117 @@ mod tests {
394395
}
395396
}
396397

398+
#[derive(Default)]
399+
struct StackTrackingPainter {
400+
transform_pushes: usize,
401+
transform_pops: usize,
402+
clip_pushes: usize,
403+
clip_pops: usize,
404+
layer_pushes: usize,
405+
layer_pops: usize,
406+
}
407+
408+
impl ColorPainter for StackTrackingPainter {
409+
fn push_transform(&mut self, _transform: Transform) {
410+
self.transform_pushes += 1;
411+
}
412+
413+
fn pop_transform(&mut self) {
414+
self.transform_pops += 1;
415+
}
416+
417+
fn push_clip_glyph(&mut self, _glyph_id: GlyphId) {
418+
self.clip_pushes += 1;
419+
}
420+
421+
fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {
422+
self.clip_pushes += 1;
423+
}
424+
425+
fn pop_clip(&mut self) {
426+
self.clip_pops += 1;
427+
}
428+
429+
fn fill(&mut self, _brush: Brush<'_>) {
430+
// nop
431+
}
432+
433+
fn push_layer(&mut self, _composite_mode: CompositeMode) {
434+
self.layer_pushes += 1;
435+
}
436+
437+
fn pop_layer(&mut self) {
438+
self.layer_pops += 1;
439+
}
440+
}
441+
442+
#[test]
443+
fn transform_error_unwinds_transform_stack() {
444+
let colr_font = font_test_data::COLRV0V1_VARIABLE;
445+
let font = FontRef::new(colr_font).unwrap();
446+
let glyph_id = font.charmap().map(CLIPBOX[0]).unwrap();
447+
let mut painter = StackTrackingPainter::default();
448+
let instance = ColrInstance::new(font.colr().unwrap(), &[]);
449+
let inner_paint = instance.v1_base_glyph(glyph_id).unwrap().unwrap().0;
450+
let mut state = TraversalState::new(instance, &mut painter);
451+
let mut decycler = PaintDecycler::new();
452+
state.nodes_left = 0;
453+
let paint = ResolvedPaint::Transform {
454+
xx: 1.0,
455+
yx: 0.0,
456+
xy: 0.0,
457+
yy: 1.0,
458+
dx: 0.0,
459+
dy: 0.0,
460+
paint: inner_paint,
461+
};
462+
let result = traverse_with_callbacks(&paint, &mut state, &mut decycler, 0);
463+
assert!(matches!(result, Err(PaintError::DepthLimitExceeded)));
464+
assert_eq!(painter.transform_pushes, painter.transform_pops);
465+
assert_ne!(painter.transform_pushes, 0);
466+
}
467+
468+
#[test]
469+
fn composite_error_unwinds_layer_stack() {
470+
let colr_font = font_test_data::COLRV0V1_VARIABLE;
471+
let font = FontRef::new(colr_font).unwrap();
472+
let glyph_id = font.charmap().map(CLIPBOX[0]).unwrap();
473+
let mut painter = StackTrackingPainter::default();
474+
let instance = ColrInstance::new(font.colr().unwrap(), &[]);
475+
let inner_paint = instance.v1_base_glyph(glyph_id).unwrap().unwrap().0;
476+
let mut state = TraversalState::new(instance, &mut painter);
477+
let mut decycler = PaintDecycler::new();
478+
state.nodes_left = 0;
479+
let paint = ResolvedPaint::Composite {
480+
source_paint: inner_paint.clone(),
481+
mode: CompositeMode::SrcOver,
482+
backdrop_paint: inner_paint,
483+
};
484+
let result = traverse_with_callbacks(&paint, &mut state, &mut decycler, 0);
485+
assert!(matches!(result, Err(PaintError::DepthLimitExceeded)));
486+
assert_eq!(painter.layer_pushes, painter.layer_pops);
487+
assert_ne!(painter.layer_pushes, 0);
488+
}
489+
490+
#[test]
491+
fn clipbox_error_unwinds_clip_stack() {
492+
let colr_font = font_test_data::COLRV0V1_VARIABLE;
493+
let font = FontRef::new(colr_font).unwrap();
494+
let glyph_id = font.charmap().map(CLIPBOX[0]).unwrap();
495+
let mut painter = StackTrackingPainter::default();
496+
let instance = ColrInstance::new(font.colr().unwrap(), &[]);
497+
let mut state = TraversalState::new(instance, &mut painter);
498+
let mut decycler = PaintDecycler::new();
499+
state.nodes_left = 0;
500+
let paint = ResolvedPaint::ColrGlyph {
501+
glyph_id: GlyphId16::new(glyph_id.to_u32() as u16),
502+
};
503+
let result = traverse_with_callbacks(&paint, &mut state, &mut decycler, 0);
504+
assert!(matches!(result, Err(PaintError::DepthLimitExceeded)));
505+
assert_eq!(painter.clip_pushes, painter.clip_pops);
506+
assert_ne!(painter.clip_pushes, 0);
507+
}
508+
397509
#[test]
398510
fn no_panic_on_empty_colorline() {
399511
// Minimized test case from <https://issues.oss-fuzz.com/issues/375768991>.

0 commit comments

Comments
 (0)