← Chrome — 2026-W02

79ebcbb92910aa37b379e0fbf7a62e9ce2769ddf  ScrollAnchor: Fix crash in SuppressScrollAnchorScope with null scroller

severity medium class UAF CVSS 4.3 confidence 0.6 signal score 3.8 third_party/blink/renderer AI review CONFIRMED
Primitive: When SuppressScrollAnchorScope is constructed with a null ScrollableArea, anchor_ was left uninitialized and later dereferenced in the destructor.
Triage note: Initializes anchor_ = nullptr. Uninitialized-pointer use is potentially worse than a crash, but the null-scroller construction path is internal and exploitability cannot be established from the diff.

The bug at a glance

This is precisely an uninitialized-pointer bug: a raw member ScrollAnchor* anchor_ had no initializer, so when SuppressScrollAnchorScope was constructed with a null ScrollableArea (the constructor skips assigning anchor_ in that branch), the field held indeterminate garbage. The destructor then dereferences anchor_ to restore suppression state, dereferencing a wild pointer. A wild-pointer dereference of stack garbage is closer to an uninitialized-memory-read leading to an unpredictable read/write through an attacker-uninfluenced value; it is a reliable crash and, depending on stack contents, could in principle be steered, but the diff shows no control over the garbage value. The test only asserts no crash. Medium/4.3 is defensible for a renderer-process wild dereference that is reachable when a scroller is null; it is not a controllable UAF, so it stays watch-tier rather than high.

A textbook C++ footgun: a raw pointer member with no default initializer plus a constructor path that conditionally assigns it. The ’null scroller’ branch leaves anchor_ uninitialized and the RAII destructor unconditionally uses it. Bug-class is technically uninitialized-pointer-use rather than a true use-after-free, but the effect (dereferencing an invalid ScrollAnchor pointer) is UAF-adjacent. A regression test ships, confirming the crash was real and reproducible.

Root cause

SuppressScrollAnchorScope is an RAII helper that suppresses scroll anchoring for a ScrollableArea for the duration of a scope. Its constructor takes a ScrollableArea*; when that is non-null it obtains the area’s ScrollAnchor and stores it in anchor_ (and toggles suppression), but when the ScrollableArea is null it takes an early/guarded path that never assigns anchor_. Because anchor_ was declared as a bare ‘ScrollAnchor* anchor_;’ with no in-class initializer, the null-scroller path leaves it holding whatever garbage occupied that stack/heap slot. The destructor ~SuppressScrollAnchorScope() then dereferences anchor_ to undo the suppression, reading/writing through an indeterminate pointer and crashing (or worse). The fix is a one-token default member initializer ‘ScrollAnchor* anchor_ = nullptr;’ so the null-scroller path yields a well-defined nullptr that the destructor’s null-guard handles safely.

Key code

Default-initialize the raw member so the null-scroller path leaves a defined nullptr

  ~SuppressScrollAnchorScope();
 
  private:
-  ScrollAnchor* anchor_;
+  ScrollAnchor* anchor_ = nullptr;
 };

Patch walkthrough

  • third_party/blink/renderer/core/layout/scroll_anchor.h — Adds ‘= nullptr’ to the anchor_ member declaration. This guarantees anchor_ is defined even when the constructor’s null-ScrollableArea branch does not assign it, so the destructor’s null check (rather than a wild dereference) governs cleanup.
  • third_party/blink/renderer/core/layout/scroll_anchor_test.cc — Adds TEST_F(ScrollAnchorTest, SuppressScrollAnchorScopeWithNullScroller) which simply constructs SuppressScrollAnchorScope scope(nullptr) and passes if no crash occurs, directly exercising the previously-uninitialized path and locking in the fix.

Background

Scroll anchoring — A layout feature that keeps the visible content stable when content above the viewport changes size. ScrollAnchor tracks the anchor element per ScrollableArea.

SuppressScrollAnchorScope — RAII guard that temporarily disables scroll anchoring (e.g. during operations that would otherwise fight the anchor), restoring prior state in its destructor via the stored anchor_.

Null ScrollableArea path — Some call sites construct the scope with a possibly-null area (e.g. a detached or not-yet-laid-out scroller). The constructor guards on null but historically left anchor_ untouched.

Uninitialized raw pointer member — A class member ‘T* p;’ without an initializer has indeterminate value; reading it is undefined behavior. Default member initializers eliminate this whole bug family.

Vulnerability window

  1. Introduced — Since anchor_ was declared without an initializer alongside a constructor that only conditionally assigns it. The null-scroller call path made it observable.
  2. Observed — Crash reported as bug 471610993 when SuppressScrollAnchorScope was constructed with a null scroller.
  3. Fixed — Landed 2026-01-04, refs/heads/main@{#1564184}, with a dedicated regression test.

Proof of concept

The commit ships a regression test that reconstructs the trigger directly: constructing SuppressScrollAnchorScope with a nullptr ScrollableArea. Pre-patch this leaves anchor_ uninitialized and the destructor dereferences garbage; the test passes post-patch because anchor_ is a defined nullptr. This is a crash/no-crash test, not an exploit, and no attacker-controlled corruption is demonstrated. There is no JS-level PoC in the diff; reaching this from web content requires a script/layout sequence that drives a SuppressScrollAnchorScope with a null scroller, which the diff does not enumerate.

// Verifies that SuppressScrollAnchorScope doesn't crash when constructed with
// a nullptr ScrollableArea.
// https://crbug.com/471610993
TEST_F(ScrollAnchorTest, SuppressScrollAnchorScopeWithNullScroller) {
  SuppressScrollAnchorScope scope(nullptr);
  // Pass if no crashes.
}

Exploitation

  1. Reachability — Requires a code path that constructs SuppressScrollAnchorScope with a null ScrollableArea. Whether web content can force this depends on which layout operations pass a null scroller; the diff does not identify a direct script trigger.
  2. Primitive — Dereference of an indeterminate stack pointer in the destructor. Value is not attacker-chosen in any way the diff shows, so the practical result is a reliable crash (DoS).
  3. Ceiling — Steering the garbage pointer to a controlled value would need a separate stack-grooming primitive; no such control is demonstrated. Treat as uninitialized-read crash, not a proven exploitable UAF.

Detection & hunting

For defenders and detection engineers:

  • Raw pointer members without initializers — Flag class members declared ‘T* x;’ in headers, especially in RAII helpers where the destructor uses the member; require default member initializers or full constructor initialization on every path.
  • Conditional constructor assignment — Look for constructors that assign a member only in some branches (e.g. non-null argument) while the destructor uses it unconditionally.
  • Null-argument RAII construction — Audit RAII scopes constructed with pointers that can be null; verify both construction and destruction handle the null case.

Audit directions

  • Other RAII scopes in layout — Enumerate similar scoped guards in core/layout (suppression/lock scopes) for raw uninitialized members and conditional constructor assignment.
  • ScrollAnchor call sites — Identify which callers can pass a null ScrollableArea to SuppressScrollAnchorScope to gauge real web-reachability and thus whether severity should rise.
  • Clang tooling — Ensure -Wuninitialized/member-init and clang-tidy cppcoreguidelines checks would catch uninitialized raw pointer members going forward in this directory.

Before / after

Loading diff…