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); +}