79ebcbb92910aa37b379e0fbf7a62e9ce2769ddf ScrollAnchor: Fix crash in SuppressScrollAnchorScope with null scroller
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
- Introduced — Since anchor_ was declared without an initializer alongside a constructor that only conditionally assigns it. The null-scroller call path made it observable.
- Observed — Crash reported as bug 471610993 when SuppressScrollAnchorScope was constructed with a null scroller.
- 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
- 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.
- 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).
- 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.