Skip to content

paint: fix an out-of-bounds write crash in transform:rotate() - #263

Merged
tannevaled merged 1 commit into
mainfrom
rotate-overflow-crash-r165
Oct 7, 2026
Merged

tannevaled merged 1 commit into
mainfrom
rotate-overflow-crash-r165

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

A 500-page corpus sweep (268 sites, round 165) crashed the renderer on page 21, a real site, with no bench harness involved — reproduced directly against the live URL.

Root cause

paintRotated sized its offscreen buffer using only the rotated box's own border box, trusting it to bound everything the paint recursion could write. A descendant (shrink-wrapped float, negative margin, marker, outset box-shadow) can paint below the box's own bottom edge, and blitImage/blendPixel have no bound beyond the ancestor clip — which is not scoped to the buffer's own smaller height. The write went past the allocation and crashed the render.

Fix

The buffer height now comes from subtreeExtent — the existing filter/opacity/mask-image group path's own ink-bounds helper — which already covers the box's full border box unconditionally, so the result is provably never shorter than before. The final rotated image's own disclosed scope (content outside the border box is not captured into what gets rotated) is unchanged; only the offscreen buffer's own allocation grows.

Verification

  • TestRotateDoesNotCrashWhenAChildOverflowsTheBoxBottom: stash-verified against the real unfixed code, reproducing the identical paintBox -> paintBoxContent -> blendPixel panic.
  • A first version of this test used a fully-transparent test image and passed even against the unfixed code (blendPixel's own zero-coverage short-circuit skipped the vulnerable write) — caught by stash-verifying and seeing it pass wrongly, then fixed to use an opaque image.
  • paint coverage briefly dropped to 99.9% on a clamp branch that turned out to be unreachable by construction; deleted rather than tested around, restoring the 100% floor.
  • Full suite, go vet, and all five package coverage floors green.

🤖 Generated with Claude Code

paintRotated sized its offscreen buffer using only the rotated box's own
border box, trusting it to bound everything paintBoxContent's recursion
could write there. A descendant (a shrink-wrapped float, a negative margin,
a marker, an outset box-shadow) can paint below the box's own bottom edge,
and blitImage/blendPixel have no bound beyond the ancestor clip, which is
not scoped to the buffer's own smaller height. The write went past the
buffer's allocation and crashed the render.

Found by a 500-page bench sweep (round 165), reproduced directly against
the live page with no bench harness involved:
https://www.smashingmagazine.com/2018/10/mobile-app-retention-rate/

Fixed by sizing the buffer from subtreeExtent (the filter/opacity/
mask-image group path's own ink-bounds helper), which already covers the
box's own border box unconditionally, so the result is never shorter than
before. Stash-verified: reverting paint.go alone reproduces the real panic
with the identical paintBox -> paintBoxContent -> blendPixel stack.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 26bcf69 into main Oct 7, 2026
7 checks passed
@tannevaled
tannevaled deleted the rotate-overflow-crash-r165 branch October 7, 2026 20:56
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