CVE-2025-31238
Overview
Background
- ScrollerMac
- The WebCore C++ object representing a single macOS scrollbar (one orientation), owning the platform NSScrollerImp and its Objective-C delegate.
- CheckedPtr / CanMakeCheckedPtr
- A WebKit smart-pointer scheme where the pointee counts outstanding CheckedPtrs and aborts (rather than silently continuing) if it is destroyed while references remain or if a dangling pointer is used, turning UAFs into detectable crashes.
- NSScrollerImpDelegate / NSAnimation retention
- AppKit objects: an NSScrollerImp retains its delegate and running NSAnimations retain themselves, so these can outlive the WebCore object that created them if not explicitly invalidated.
- adoptNS
- A WebKit RAII helper that takes ownership of a +1-retained Objective-C object into a RetainPtr, tying its release to C++ scope.
- invalidate (delegate teardown)
- A method that severs a delegate’s back-references and stops its animations so lingering AppKit callbacks no longer touch the (soon-to-be-freed) C++ owner.
Root Cause Analysis
On macOS, scrollbar appearance animations are driven by AppKit through two Objective-C helper classes, WebScrollbarPartAnimationMac (an NSAnimation subclass) and WebScrollerImpDelegateMac (an NSScrollerImpDelegate), both of which held a raw C++ back-pointer ‘WebCore::ScrollerMac* _scroller’. These Objective-C objects have lifetimes controlled by AppKit and by pending animations: an NSScrollerImp retains its delegate, and running animations retain their animation objects, so they can outlive the C++ ScrollerMac that owns them. ScrollerMac::attach() re-creates the delegate: it did ’m_scrollerImpDelegate = adoptNS([[WebScrollerImpDelegateMac alloc] initWithScroller:this])’ and installed it on a fresh NSScrollerImp. If attach() ran while a previous delegate (and/or its part animations) was still alive and still referencing the old ScrollerMac, or if the ScrollerMac was subsequently destroyed while an AppKit-retained delegate/animation lingered, later NSScrollerImpDelegate/NSAnimation callbacks would dereference the stale raw _scroller pointer — a use-after-free. The bundled LayoutTest reaches this by churning scrollbar/scroller state during layout and focus/pointer-lock/form activity (‘scrollIntoViewIfNeeded’, ‘requestPointerLock’, form ‘submit’, and an appendMedium() style mutation), which can tear down and re-attach scrollers while animations/delegates are outstanding.
The fix has two parts. First, ScrollerMac is made ‘final’ and derives from CanMakeCheckedPtr<ScrollerMac> with WTF_MAKE_FAST_ALLOCATED and WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(ScrollerMac), and the two Objective-C classes’ raw ‘ScrollerMac* _scroller’ members become ‘CheckedPtrWebCore::ScrollerMac _scroller’ (with call sites updated to pass _scroller.get()). CheckedPtr participates in ScrollerMac’s checked-pointer accounting so that a dangling access is detected/aborted rather than silently reading freed memory, and the overridden delete enforces that no CheckedPtrs outlive the object. Second, ScrollerMac::attach() now calls ‘[m_scrollerImpDelegate invalidate]’ before replacing the delegate, so the outgoing delegate is explicitly torn down (its reference to the scroller cleared and its animations stopped/detached) instead of being left alive holding a pointer to a scroller that is being re-attached. Together these restore the invariant that no AppKit-retained delegate or animation ever dereferences a ScrollerMac after that scroller has stopped owning it. The exact body of WebScrollerImpDelegateMac’s invalidate method and the animation callbacks are not shown in the diff, so the precise clearing steps invalidate performs are inferred from the surrounding pattern.
Attack Path
- Create scrollable content with custom scrollbars Load a page that produces overlay/animated scrollbars on macOS (the test uses root margin and rtl direction to force scrollbar geometry), instantiating ScrollerMac objects with WebScrollerImpDelegateMac delegates and part animations.
- Provoke scroller re-attach Trigger operations that rebuild scroller state while animations/delegates are in flight — the test uses scrollIntoViewIfNeeded(), requestPointerLock() with an error handler, a form submit(), and a stylesheet media appendMedium() mutation — so ScrollerMac::attach() runs (pre-patch, without invalidating the old delegate).
- Outlive the C++ owner Because AppKit’s NSScrollerImp retains the delegate and running NSAnimations retain themselves, a delegate/animation persists after its ScrollerMac is freed, leaving a stale raw _scroller pointer.
- Fire a stale callback A subsequent AppKit animation tick or NSScrollerImpDelegate callback dereferences the freed ScrollerMac (e.g. calling _scroller->scrollerImp()), reading and acting on freed heap memory — the use-after-free.
- (Background, separate) escalate the UAF Standard exploitation would groom the heap to reclaim the freed ScrollerMac allocation with attacker-controlled data before the stale callback fires, turning the dangling dereference into a controlled read/write toward code execution; this is generic technique, not something the commit itself demonstrates.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ScrollerMac (class declaration)Source/WebCore/page/scrolling/mac/ScrollerMac.h |
modified | Made 'final', now inherits CanMakeCheckedPtr<ScrollerMac>, and adds WTF_MAKE_FAST_ALLOCATED and WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR(ScrollerMac) so the object supports checked-pointer lifetime accounting and detects dangling references. |
WebScrollbarPartAnimationMac (ivar _scroller)Source/WebCore/page/scrolling/mac/ScrollerMac.mm |
modified | Changed the raw 'WebCore::ScrollerMac* _scroller' instance variable to 'CheckedPtr<WebCore::ScrollerMac> _scroller' so an animation callback can no longer silently use a freed scroller. |
WebScrollerImpDelegateMac (ivar _scroller)Source/WebCore/page/scrolling/mac/ScrollerMac.mm |
modified | Changed the raw 'WebCore::ScrollerMac* _scroller' instance variable to 'CheckedPtr<WebCore::ScrollerMac> _scroller'; call sites that construct WebScrollbarPartAnimationMac now pass _scroller.get(). |
ScrollerMac::attachSource/WebCore/page/scrolling/mac/ScrollerMac.mm |
modified | Added '[m_scrollerImpDelegate invalidate]' before allocating and installing a new WebScrollerImpDelegateMac, so the previous delegate (and its animations) is torn down and no longer references the scroller being re-attached. |
Files Changed
LayoutTests/fast/scrolling/mac/scrollbars/scrollbar-crash-expected.txtLayoutTests/fast/scrolling/mac/scrollbars/scrollbar-crash.htmlSource/WebCore/page/scrolling/mac/ScrollerMac.hSource/WebCore/page/scrolling/mac/ScrollerMac.mm
Audit Directions
- Other raw back-pointers in ScrollerMac.mmVerify every _scroller / m_pair / owner reference in ScrollerMac.mm and ScrollerPairMac is now a CheckedPtr/WeakPtr and that all replace-and-reinstall sites (like attach()) invalidate the prior delegate before reassigning m_scrollerImpDelegate / m_scrollerImp.
- Objective-C delegates holding raw C++ pointersGrep the mac scrolling/rendering code for Objective-C @interface ivars typed as bare ‘WebCore::Something*’ (especially NSAnimation, NSScrollerImpDelegate, CALayer delegate, and NSObject subclasses stored via adoptNS) and check each has a matching invalidate-on-teardown and a checked/weak pointer.
- adoptNS reassignment without invalidateSearch for ’m_…Delegate = adoptNS(’ or similar delegate/animation reassignments that are not preceded by an [oldObject invalidate] or setDelegate:nil, since a replaced-but-still-retained AppKit object with a back-pointer is the exact pattern fixed here.
- CanMakeCheckedPtr adoption gapsLook for WebCore classes referenced from platform Objective-C callback objects that do NOT yet derive from CanMakeCheckedPtr/CanMakeWeakPtr (no WTF_OVERRIDE_DELETE_FOR_CHECKED_PTR), as these remain candidates for the same cross-language lifetime UAF.