Skip to content

Fix overflow panic when filling paths with extreme coordinates - #180

Open
StefanoD wants to merge 1 commit into
linebender:mainfrom
StefanoD:fix-933-fill-coord-overflow
Open

StefanoD wants to merge 1 commit into
linebender:mainfrom
StefanoD:fix-933-fill-coord-overflow

Conversation

@StefanoD

Copy link
Copy Markdown

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: linebender/resvg#933

Reproduced with:

<svg height="32" viewBox="0 0 8.4.9" width="32" x="w">
  <g m=""><g><path d="m3 6h8v-158758583"/></g></g>
</svg>

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::<impl tiny_skia::pixmap::PixmapMut>::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: <tiny_skia::pipeline::blitter::RasterPipelineBlitter as tiny_skia::blitter::Blitter>::blit_anti_h
   6: tiny_skia::scan::path_aa::SuperBlitter::flush
   7: tiny_skia::scan::path_aa::fill_path_impl

Generated by Claude


Note: I accidentally closed the original PR (#178) by deleting my fork, which auto-closed it. This reopens the same change — the branch and commits are unchanged.

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: linebender/resvg#933

Reproduced with:

    <svg height="32" viewBox="0 0 8.4.9" width="32" x="w">
      <g m=""><g><path d="m3 6h8v-158758583"/></g></g>
    </svg>

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::<impl tiny_skia::pixmap::PixmapMut>::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: <tiny_skia::pipeline::blitter::RasterPipelineBlitter as tiny_skia::blitter::Blitter>::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) <noreply@anthropic.com>
malbarbo added a commit to malbarbo/sinteract that referenced this pull request Sep 29, 2026
tiny-skia 0.12 overflows an i32 when it supersamples a path whose
bounds reach 2^29 output pixels above the canvas, and 2^31 left of it.
The render then panics, and in a release build it can abort, so one
frame from the wire with a far coordinate, a wide stroke or a large text
scale kills the frontend. The fix upstream, linebender/tiny-skia#180,
is not released.

A fill, a stroke, a clip and a text now draw nothing when their bounds
in output pixels reach past 2^28. A stroke first checks the path grown
by miter_limit widths, and outlines itself only when that bound fails,
so a common stroke costs no extra work. A clip that reaches too far
hides what it holds, as one with no area does. Element and the schema
say a renderer may draw nothing for such an element.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant