From 5a8821a608660b8efe30c7c7de61103973eab0e0 Mon Sep 17 00:00:00 2001 From: Stefano Di Martino Date: Sun, 14 Jun 2026 15:04:58 +0200 Subject: [PATCH] Fix overflow panic when filling paths with extreme coordinates When anti-aliased filling a path whose device-space vertical bounds lie far outside the clip, `start_y`/`stop_y` (the path bounds) were shifted up for supersampling with a plain `<<`. A large-negative bound then overflowed `i32` and wrapped into a large-positive value, escaping the subsequent clip clamp and breaking `walk_edges`' `last_y >= curr_y` invariant. Use a saturating shift so out-of-range bounds stay on the correct side and get clamped to the clip as intended. In-range bounds are unaffected. Originally reported against resvg: https://github.com/linebender/resvg/issues/933 Reproduced with: Crash dump (debug build): thread 'main' panicked at tiny-skia/src/scan/path.rs:221:13: assertion failed: edges[curr_idx].last_y >= curr_y as i32 3: tiny_skia::scan::path::walk_edges 4: tiny_skia::scan::path::fill_path_impl 5: tiny_skia::scan::path_aa::fill_path_impl 6: tiny_skia::scan::path_aa::fill_path 7: tiny_skia::painter::::fill_path Crash dump (release build): thread 'main' panicked at tiny-skia/src/pipeline/mod.rs:181:31: range start index 15006310411 out of range for slice of length 1024 3: tiny_skia::pipeline::lowp::load_dst_tail 4: tiny_skia::pipeline::lowp::start 5: ::blit_anti_h 6: tiny_skia::scan::path_aa::SuperBlitter::flush 7: tiny_skia::scan::path_aa::fill_path_impl Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 5 +++++ src/scan/path.rs | 10 ++++++++-- tests/integration/fill.rs | 24 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06ab4bc..4defc55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Fixed +- Fixed a panic (an out-of-bounds slice access in release builds) when + anti-aliased filling a path with extreme coordinates. The path's device-space + vertical bounds overflowed `i32` while being shifted up for supersampling. + See [resvg#933](https://github.com/linebender/resvg/issues/933) ## [0.12.0] - 2026-02-02 ### Fixed diff --git a/src/scan/path.rs b/src/scan/path.rs index a52c7de..6a37370 100644 --- a/src/scan/path.rs +++ b/src/scan/path.rs @@ -161,8 +161,14 @@ pub fn fill_path_impl( ..LineEdge::default() })); - start_y <<= shift_edges_up; - stop_y <<= shift_edges_up; + // Use a saturating shift here. The path bounds can extend far beyond the + // clip (e.g. a path with extreme coordinates), in which case shifting them + // up for supersampling would overflow `i32` and wrap a large-negative + // `start_y` into a large-positive value. That bogus value would escape the + // clip clamp below and break `walk_edges`' invariant. Saturating keeps the + // out-of-range bounds on the correct side so the clamp can do its job. + start_y = start_y.saturating_mul(1 << shift_edges_up); + stop_y = stop_y.saturating_mul(1 << shift_edges_up); let top = shifted_clip.shifted().y() as i32; if !path_contained_in_clip && start_y < top { diff --git a/tests/integration/fill.rs b/tests/integration/fill.rs index 4737f48..2d20ebc 100644 --- a/tests/integration/fill.rs +++ b/tests/integration/fill.rs @@ -644,3 +644,27 @@ fn fill_rect() { let expected = Pixmap::load_png("tests/images/canvas/fill-rect.png").unwrap(); assert_eq!(pixmap, expected); } + +// Filling an anti-aliased path with extremely large coordinates used to overflow +// the fixed-point scan converter: the path's device-space top, when shifted up +// for supersampling, wrapped around `i32` into a large positive value, escaped +// the clip clamp and broke an invariant in `walk_edges` (a panic in debug, an +// out-of-bounds slice access in release). +// See https://github.com/linebender/resvg/issues/933 +#[test] +fn huge_coordinates() { + let mut paint = Paint::default(); + paint.set_color_rgba8(50, 127, 150, 200); + paint.anti_alias = true; + + let mut pb = PathBuilder::new(); + pb.move_to(3.0, 6.0); + pb.line_to(11.0, 6.0); + pb.line_to(11.0, -700_000_000.0); + pb.close(); + let path = pb.finish().unwrap(); + + let mut pixmap = Pixmap::new(32, 32).unwrap(); + // Must not panic. + pixmap.fill_path(&path, &paint, FillRule::Winding, Transform::identity(), None); +}