← Chrome — 2026-W38

6ab025cb13a7d2e2c3ca1890f989b5707d39c3bd  Ensure PopupWidgetHost popups cannot appear above the line of death

severity medium class Spoofing CVSS 5.0 confidence 0.6 signal score 4.0 content/browser/renderer_host AI review CONFIRMED
Primitive: A compromised renderer could position select/date/color popups above the top-level main frame's view, over browser UI including the omnibox -> address-bar spoofing.
Triage note: Browser-side clamp on PopupWidgetHost bounds; UI-spoofing security fix (not memory corruption). Web-reachable UI risk. Fixed:424995036.

The bug at a glance

This is a browser-UI spoofing primitive, not a memory-safety bug: the worst case is an attacker page positioning a renderer-drawn popup (a dropdown, or a date/time/color picker) on top of trusted browser chrome, including the omnibox. Because the popup content is fully attacker-controlled but is painted at OS-window z-order above the browser’s own UI, it can imitate the address bar or a security indicator, which is the classic address-bar-spoofing pattern that has repeatedly earned medium (rarely low) ratings in Chrome. The commit metadata (band medium, CVSS 5.0, exploitable=false) is consistent: there is no direct code-execution or data-disclosure path, spoofing requires user interaction (the user must be induced to trust the fake chrome), and the underlying bugs (crbug.com/40092820 etc.) were reachable even from a non-compromised renderer, which is what makes this worth fixing rather than dismissing. It is not high because the impact is confined to visual deception and mitigated by the fact that popups are transient and dismissed on focus changes; it is not low because reaching over the omnibox is exactly the line Chrome treats as security-relevant.

The interesting part is not that a compromised renderer can lie about pixels inside its own frame – that is expected – but that PopupWidgetHost bounds are one of the few renderer-supplied geometries that escape the web-contents area and are honored by the browser at the native-widget level. A popup or picker is deliberately allowed to overflow the frame and even the browser window so that a long menu near the bottom of the screen can render upward. That escape hatch was never clamped at the top, so a renderer could set the popup’s y-coordinate to a negative or small value and paint arbitrary content directly over the omnibox and toolbar – the ’line of death’ being the top edge of the content area, above which only trusted UI should ever appear. The angle a reviewer should notice: the trust boundary here is geometric, and the browser was trusting a renderer-supplied rectangle without bounding one of its edges.

Root cause

When a renderer wants to show a popup widget it sends bounds over the blink.mojom.PopupWidgetHost interface. Two browser-side paths turn those bounds into a real widget position: (1) initial display via WebContentsImpl::ShowCreatedWidget, which transforms the renderer-supplied initial_rect into screen space, and (2) later repositioning via RenderWidgetHostImpl::SetPopupBounds, which calls view_->SetBounds(bounds) directly. Neither path clamped the top (y) coordinate against the top-level main frame’s view. So a renderer supplying a rectangle whose top is above the content area’s top edge would have that rectangle passed straight through to the platform view, placing attacker-painted pixels over browser chrome. The fix introduces a single choke point, RenderWidgetHostDelegate::ConstrainPopupBounds, implemented by WebContentsImpl to clamp bounds.y() up to the outermost main frame view’s top (the line of death) and wires both display paths through it, gated behind the kLimitPopupWidgetHostPosition kill-switch feature (default-on).

Key code

The clamp: the popup’s top edge is raised to the outermost main frame view’s top (the line of death), gated by a default-on kill switch.

gfx::Rect WebContentsImpl::ConstrainPopupBounds(const gfx::Rect& bounds) {
  if (!base::FeatureList::IsEnabled(features::kLimitPopupWidgetHostPosition)) {
    return bounds;
  }
  // Constrain popup bounds so that the top of the popup is at or below the line
  // of death (the top of the top-level main frame).
  RenderWidgetHostView* view = GetTopLevelRenderWidgetHostView();
  if (!view) {
    return bounds;
  }
  gfx::Rect constrained_bounds = bounds;
  int line_of_death = view->GetViewBounds().y();
  if (constrained_bounds.y() < line_of_death) {
    constrained_bounds.set_y(line_of_death);
  }
  return constrained_bounds;
}

Patch walkthrough

  • content/browser/renderer_host/render_widget_host_delegate.h / .cc — Adds a new virtual RenderWidgetHostDelegate::ConstrainPopupBounds(const gfx::Rect&) with a pass-through default (returns bounds unchanged). This introduces the extension point so RenderWidgetHostImpl, which only knows its delegate, can ask the owning WebContents to apply the clamp without RenderWidgetHostImpl itself needing to know about frame trees or the line of death.
  • content/browser/renderer_host/render_widget_host_impl.cc — In SetPopupBounds (the repositioning path), replaces the direct view_->SetBounds(bounds) with view_->SetBounds(delegate_->ConstrainPopupBounds(bounds)) when a delegate exists. This is the path a compromised renderer would hammer to move an already-shown popup over the omnibox; it was previously entirely unclamped.
  • content/browser/web_contents/web_contents_impl.cc / .h — Implements the override. It reads the outermost main frame view’s top via GetTopLevelRenderWidgetHostView()->GetViewBounds().y() and raises any smaller popup y up to it. It also inserts transformed_rect = ConstrainPopupBounds(transformed_rect) into ShowCreatedWidget so the very first display of a popup is clamped too, closing the initial-show path in addition to the reposition path.
  • content/common/features.cc / .h — Declares/defines BASE_FEATURE(kLimitPopupWidgetHostPosition, FEATURE_ENABLED_BY_DEFAULT) as an explicit kill switch. Because the CL notes this can affect legitimate very-large-menu sites (a behavior change that is unspecified across browsers), shipping it behind a flag lets it be disabled server-side without a respin if a compatibility regression surfaces.
  • content/browser/renderer_host/render_widget_host_unittest.cc — Adds a mock delegate hook (set_constrain_popup_bounds_callback) and SetPopupBoundsConstrainedByDelegate, which drives the real PopupWidgetHost mojo path and asserts the delegate’s constraint is applied to view bounds. This proves the reposition path actually routes through the new choke point.
  • content/browser/web_contents/web_contents_impl_unittest.cc — Adds ConstrainPopupBounds covering: a popup above the line of death is clamped, one exactly at it is unchanged, one below is unchanged, the clamp follows a moved line of death, and with the feature disabled no clamping occurs. This pins down the boundary semantics (at-or-below is allowed) and the kill-switch behavior.

Background

Line of death — Informal Chrome-security term for the top edge of the web-content area: everything above it (omnibox, toolbar, security indicators) is trusted browser chrome that renderer content must never be able to paint over. Any primitive that lets renderer pixels cross that line upward is treated as a spoofing vulnerability.

PopupWidgetHost — The browser-side host for renderer-requested popup widgets used by native form controls – dropdowns and date/time and color pickers. These are deliberately allowed to overflow their frame and even the browser window (so tall menus can extend), which is precisely why their geometry needs an explicit upper clamp.

Compromised vs. non-compromised renderer — The CL notes the pickers could be mispositioned even by an ordinary (non-compromised) renderer via normal APIs (crbug.com/40092820), so this is not solely a compromised-renderer hardening item – a plain malicious web page could trigger the spoof, which raises real-world reachability.

gfx::Rect coordinate convention — y() is the top edge; smaller y is higher on screen. Clamping constrained_bounds.y() up to the view’s y() guarantees the popup’s top never rises above the content area’s top, while leaving x, width, and height untouched so horizontal overflow and downward extension still work.

Vulnerability window

  1. Prior state — Multiple filed bugs (40086125, 40092820, 40065947, 424995513, 417555081) documented select/picker popups rendering over browser UI including the omnibox, some reachable without a compromised renderer.
  2. Fix — 2026-09-14, Mason Freed lands the browser-side clamp in both ShowCreatedWidget and SetPopupBounds behind a default-on kill switch, Cr-Commit-Position 1697455, tracked as Fixed: 424995036.
  3. Compatibility posture — Documented as matching existing Firefox behavior and as unspecified/non-standardized; expected to only affect very large menus in edge cases, hence the kill switch for safe rollback.

Proof of concept

No exploit or PoC is present in the diff, and I will not fabricate one. Conceptually the pre-patch trigger is straightforward and non-memory-corrupting: a page with a native form control (e.g. a or an ) opens its popup and the renderer supplies, via the PopupWidgetHost bounds path, a rectangle whose top is above the content area, causing attacker-controlled popup pixels to paint over the omnibox for an address-bar spoof. The unit tests in the diff (SetPopupBoundsConstrainedByDelegate and ConstrainPopupBounds) demonstrate the boundary behavior directly at the C++ level but are regression tests, not an exploit. Actual weaponization would require crafting popup content that convincingly imitates trusted chrome and timing it against user interaction, which the diff neither contains nor requires.

Exploitation

  1. Setup — Attacker page includes a native control whose popup it can open (select/date/time/color) and supplies popup bounds with a top above the line of death, either through normal mispositioning (pre-existing bug) or from a compromised renderer directly driving PopupWidgetHost.
  2. Overlay — The unclamped bounds place an attacker-painted widget over the omnibox/toolbar region, where the popup renders arbitrary attacker HTML-derived pixels above trusted chrome.
  3. Deception — The popup imitates the address bar or a security prompt to mislead the user about origin or to solicit input; success depends entirely on social engineering, since no data or code boundary is crossed by the pixels themselves.
  4. Limits — Popups are transient and dismissed on focus/scroll changes, and post-patch the top is clamped to the content-area top, so the overlay can no longer reach above the line of death at all.

Detection & hunting

For defenders and detection engineers:

  • PopupWidgetHost bounds with negative or small y —
  • kLimitPopupWidgetHostPosition clamp activations —

Audit directions

  • Other renderer-supplied geometries that escape the content area —
  • GetTopLevelRenderWidgetHostView null and multi-view cases —
  • Reposition-after-show races —

Before / after

Loading diff…