Skip to content

Don't clip the <body> when its overflow propagates to the viewport - #880

Open
nicoburns wants to merge 1 commit into
mainfrom
devin/1789260080-body-overflow-propagation
Open

nicoburns wants to merge 1 commit into
mainfrom
devin/1789260080-body-overflow-propagation

Conversation

@nicoburns

@nicoburns nicoburns commented Sep 13, 2026

Copy link
Copy Markdown
Member

Summary

Split out of #877.

The painter exempted only the root element from overflow clipping. Per css-overflow-3 §3.3, when <html> has overflow: visible in both axes, the overflow of the first <body> child that generates a box is propagated to the viewport instead, and the body's own used overflow becomes visible — so body { overflow: hidden } must not clip the body's content (css/CSS2/visufx/overflow-propagation-001a/b/c).

  • New BaseDocument::viewport_overflow_element() returns the element whose overflow is propagated: the root, unless it's an <html> with visible overflow and its first <body> child has a box (a display: none body propagates nothing → overflow-body-propagation-016).
  • BlitzDomPainter caches it as viewport_overflow_element_id; in render_element that element skips clipping for overflow reasons only:
    let clips_overflow = !propagates_overflow_to_viewport && (overflow_x != Visible || overflow_y != Visible);
    let should_clip = is_image || is_sub_doc || is_text_input || contain_paint || clips_overflow;
    Previously the root also skipped contain: paint / image / iframe / text-input clipping; now those still apply (overflow-body-propagation-010).

Scrolling already followed these rules (scrolling.rs); this brings paint clipping in line.

WPT (local)

  • css/CSS2/visufx: +3 (overflow-propagation-001*)
  • css/css-overflow: 227 → 232 (overflow-body-propagation-007/008/009/014/015), no regressions
  • css/css-position, flexbox, grid, text: unchanged

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/355437a9e8fd4bebbf536cf039421df2
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/355437a9e8fd4bebbf536cf039421df2?variant=devin-insiders
Requested by: @nicoburns

WPT results

Subtests: 16 newly passing, 12 newly failing (net +4).

Full diff (28 changed tests)
+ FAIL => PASS  [1/1]  +1  css/CSS2/visufx/overflow-propagation-001a.html
+ FAIL => PASS  [1/1]  +1  css/CSS2/visufx/overflow-propagation-001b.html
+ FAIL => PASS  [1/1]  +1  css/CSS2/visufx/overflow-propagation-001c.html
+ FAIL => PASS  [1/1]  +1  css/css-backgrounds/background-origin/origin-border-box.html
- PASS => FAIL  [0/1]  -1  css/css-backgrounds/background-origin/origin-border-box_with_position.html
+ FAIL => PASS  [1/1]  +1  css/css-backgrounds/background-origin/origin-border-box_with_radius.html
+ FAIL => PASS  [1/1]  +1  css/css-backgrounds/background-origin/origin-border-box_with_size.html
- PASS => FAIL  [0/1]  -1  css/css-contain/contain-body-overflow-001.html
- PASS => FAIL  [0/1]  -1  css/css-contain/contain-body-overflow-003.html
- PASS => FAIL  [0/1]  -1  css/css-contain/contain-body-overflow-004.html
- PASS => FAIL  [0/1]  -1  css/css-contain/contain-html-overflow-001.html
- PASS => FAIL  [0/1]  -1  css/css-contain/contain-html-overflow-002.html
- PASS => FAIL  [0/1]  -1  css/css-contain/contain-html-overflow-003.html
- PASS => FAIL  [0/1]  -1  css/css-contain/contain-html-overflow-004.html
- PASS => FAIL  [0/1]  -1  css/css-images/image-orientation/image-orientation-img-object-fit.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/overflow-body-propagation-007.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/overflow-body-propagation-008.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/overflow-body-propagation-009.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/overflow-body-propagation-014.html
+ FAIL => PASS  [1/1]  +1  css/css-overflow/overflow-body-propagation-015.html
- PASS => FAIL  [0/1]  -1  css/css-scroll-snap/snap-after-initial-layout/scroll-snap-initial-layout-000.html
+ FAIL => PASS  [1/1]  +1  css/css-sizing/fit-content-block-size-abspos.html
+ FAIL => PASS  [1/1]  +1  css/css-transforms/transform-fixed-bg-006.html
+ FAIL => PASS  [1/1]  +1  css/css-transforms/transform-inherit-001.html
+ FAIL => PASS  [1/1]  +1  css/css-transforms/transform-transformed-td-contains-fixed-position.html
+ FAIL => PASS  [1/1]  +1  css/css-transforms/transform-transformed-th-contains-fixed-position.html
- PASS => FAIL  [0/1]  -1  css/cssom-view/cssom-getBoundingClientRect-vertical-rl.html
- PASS => FAIL  [0/1]  -1  css/filter-effects/backdrop-filter-plus-mask-large.html

Generated by the WPT workflow.

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@yinnho yinnho left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified this against Chrome while closing the same hole in another engine — the paint-half here matches, and the display:none/display:contents filter on the body candidate is the detail most implementations miss (WPT overflow-body-propagation-016: a body that generates no box propagates nothing, and a later body is not a fallback).

Two sibling behaviors from the same spec section that this PR doesn't cover yet, in case you want them in scope (or as a follow-up):

  1. The scrolling area collapses. When hidden/clip arrives at the viewport, the page's scrolling area becomes exactly the viewport: Chrome reports document.scrollingElement.scrollHeight === document.scrollingElement.clientHeight and window.scrollTo(0, 1000) leaves scrollY === 0 on a 5000px-tall page. If the scroll-extent computation doesn't consult the propagated overflow, body { overflow: hidden } stops clipping (with this PR) but the page stays scroll-programmable — half-correct in the other direction.
  2. The body's own extent survives. Although the body's used overflow flips back to visible, Chrome keeps document.body.scrollHeight at the full content extent (e.g. 5000) — useful as a test oracle to distinguish "propagated" from "clipped locally".

overflow: scroll/auto propagated to the viewport must of course keep the viewport scrollable — only hidden/clip pin the range at zero.

yinnho pushed a commit to yinnho/aginxbrowser that referenced this pull request Sep 14, 2026
… §3.3 (blitz#880)

UAF-style divergence: body{overflow:hidden} never reached the viewport —
the viewport stayed scrollable and body clipped its own descendants.
Now the first box-generating body child propagates its overflow to the
viewport when html is visible, and body's used value flips back to
visible (must not clip, scrollHeight keeps content extent).

- diting_layout: shared effective_viewport_overflow/body_overflow_propagates
  helpers; paint collect exempts html + propagating body from clipping
- diting_js: scroll_extent + band_frame collapse the scrolling area to the
  viewport for hidden/clip at the propagated viewport overflow
- bootstrap: root scroller scrollTop/scrollLeft setters clamp to the real
  scroll range (scrollTo on a collapsed viewport pins at 0, like Chrome)
- video: camera-scroll test pins the output viewport before navigate so
  the scroll range exists

Refs DioxusLabs/blitz#880 (paint half landed there; this carries the
scrolling-area collapse + body extent survival halves too).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

2 participants