Medium CVSS 9.8 webkit UAF 🔧 Commit mapped

Overview

Medium
Severity
9.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to memory corruption
ComponentWebCore DOM
Bug ClassUAF
Tracker277967
Fix commitccef7b85cc9b (WebKit/WebKit)
CWECWE-787 (Out-of-bounds write)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedTashita Software Security
Disclosed2024-12-11

Background

HashCountedSet
A WTF set that stores each element once along with an integer count of how many times it was added, here used to track how many handlers a Node has registered.
WeakHashCountedSet / WeakPtr
A counted set whose keys are weak references that automatically become null when the referenced object is destroyed, preventing stale-pointer dereference.
EventTargetSet
The Document-level structure (m_touchEventTargets, m_wheelEventTargets) recording every Node that currently has a touch or wheel event handler.
isEmptyIgnoringNullReferences / computeSize
Weak-container accessors that account for entries whose weak reference has already been nulled, so callers see only live members rather than a raw slot count.
Use-after-free
A memory-safety bug where a pointer is dereferenced after the object it referenced has been freed, allowing reads/writes of reallocated attacker-controlled memory.

Root Cause Analysis

The patch replaces the type of EventTargetSet in Document.h from HashCountedSet<Node*> (a counted set of raw, non-owning Node pointers) with WeakHashCountedSet<Node, WeakPtrImplWithEventTargetData> (a counted set of weak references). Document keeps two of these sets, m_touchEventTargets and m_wheelEventTargets, which record every Node in the document that has registered a touch or wheel event handler. The invariant these sets must uphold is that every entry corresponds to a live Node: the set holds only non-owning references, so an entry must be removed before or exactly when the referenced Node is destroyed. With raw Node* keys that invariant was enforced only by explicit bookkeeping calls (didRemoveEventTargetNode / removeHandlerFromSet), and any path where a handler-bearing Node was torn down without correctly pruning these sets left a dangling raw pointer. Subsequent operations that dereference the keys — most clearly Document::absoluteRegionForEventTargets, which before the patch did RefPtr node = keyValuePair.key and called absoluteEventRegionForNode(*node), and the iteration loops in wheelEventHandlerCount/touchEventHandlerCount — would then read or ref-count freed memory, a use-after-free.

The fix makes the container itself hold weak references: a destroyed Node’s slot is automatically nulled, so stale entries can never be dereferenced as live objects. Consequently the API surface changes throughout Document.cpp: add/remove/removeAll/contains now take Node& instead of Node*, size() (which counted possibly-dead slots) is replaced by computeSize() and isEmpty() by isEmptyIgnoringNullReferences() so callers ask about live entries only, and absoluteRegionForEventTargets iterates by value taking a Ref node = keyValuePair.key directly (the weak set never yields a dead key). A new removeAll(const ValueType&)/removeAll(iterator) pair was added to WeakHashCountedSet because that method did not previously exist on the weak container.

The fix restores the live-reference invariant structurally rather than relying on every teardown path to remember to prune, which is why it is fundamentally a memory-safety (UAF) fix despite the LogicError label.

Key insight
Storing DOM Nodes as raw non-owning pointers in the touch/wheel EventTargetSet made memory safety depend on every teardown path perfectly pruning the set; the fix removes that fragile obligation by making the container hold auto-nulling weak references, turning a latent use-after-free into a structurally impossible one.

Attack Path

  1. Register handlers From attacker-controlled web content, add touch and/or wheel event listeners to one or more DOM nodes so those nodes are inserted into Document::m_touchEventTargets / m_wheelEventTargets.
  2. Trigger a teardown path that skips pruning Manipulate the DOM (node removal, frame/document detachment, or a reentrant teardown during willBeRemovedFromFrame) so a handler-bearing Node is destroyed without its raw-pointer entry being correctly removed from the counted set, leaving a dangling Node* key.
  3. Reoccupy the freed slot Use standard heap grooming (allocate objects of the same size class as the freed Node) so the dangling pointer now points at attacker-influenced memory.
  4. Force a dereference of the stale key Cause the document to walk the target set — e.g. a hit-test / scrolling-region computation calling absoluteRegionForEventTargets, or wheelEventHandlerCount()/touchEventHandlerCount(), or hasTouchEventHandlers() — which reads and, in the pre-patch absoluteRegionForEventTargets, ref-counts the freed Node as if live.
  5. Escalate Convert the resulting use-after-free read/write of a Node object into a stronger primitive (type-confused vtable/refcount manipulation) toward code execution in the WebContent process; this escalation is standard exploitation background, not shown by the diff.

Impact Assessment

The primitive is a use-after-free on a Node object reachable from the WebContent (renderer) process purely through DOM manipulation and event-handler registration, so it is remotely reachable from a malicious web page. Pre-patch, absoluteRegionForEventTargets even performed a ref-count operation (RefPtr node = key) on the stale entry, giving both a dereference and a refcount write on freed memory, which is a comparatively strong UAF that heap grooming can steer toward type confusion and, with additional work, arbitrary code execution. Apple describes the observable effect as memory corruption. The bug is confined to the sandboxed WebContent process; achieving anything beyond a renderer compromise requires a separate sandbox-escape chain.

Changed Functions

FunctionChangeNotes
EventTargetSet (type alias)
Source/WebCore/dom/Document.h
modified Changed from HashCountedSet<Node*> to WeakHashCountedSet<Node, WeakPtrImplWithEventTargetData>, the core fix converting non-owning raw pointers to auto-nulling weak references.
Document::hasTouchEventHandlers / touchEventTargetsContain / hasWheelEventHandlers
Source/WebCore/dom/Document.h
modified Use computeSize() and contains(node) on the weak set instead of size()/contains(&node), so liveness queries count only live entries.
Document::willBeRemovedFromFrame
Source/WebCore/dom/Document.cpp
modified size() checks replaced with computeSize() for the weak container when deciding whether to notify the parent document.
Document::wheelEventHandlersChanged
Source/WebCore/dom/Document.cpp
modified Uses isEmptyIgnoringNullReferences() instead of isEmpty() to ignore already-dead weak entries.
Document::didAddWheelEventHandler / didAddTouchEventHandler
Source/WebCore/dom/Document.cpp
modified add(node) by reference instead of add(&node) by pointer, matching the weak-set API.
removeHandlerFromSet
Source/WebCore/dom/Document.cpp
modified remove(node)/removeAll(node) by reference instead of by pointer.
Document::wheelEventHandlerCount / touchEventHandlerCount
Source/WebCore/dom/Document.cpp
modified Iterate by value (auto handler) over the weak set; the weak container yields only live key/value pairs.
Document::didRemoveEventTargetNode
Source/WebCore/dom/Document.cpp
modified removeAll(handler) by reference and isEmptyIgnoringNullReferences() checks before propagating to the parent document.
Document::absoluteRegionForEventTargets
Source/WebCore/dom/Document.cpp
modified Iterates by value and takes Ref node = keyValuePair.key directly instead of RefPtr node = keyValuePair.key with a null check; the weak set guarantees a live key so the freed-pointer dereference path is eliminated.
WeakHashCountedSet::removeAll(const ValueType&) / removeAll(iterator)
Source/WTF/wtf/WeakHashCountedSet.h
added New method (with declaration and inline definition) removing a value regardless of count, needed so the weak container can serve the same removeAll usage the raw HashCountedSet provided.

Audit Directions

  • Other raw-pointer counted/hash sets of DOM nodes in Document
    Grep Document.h/.cpp and related DOM classes for HashCountedSet<Node*>, HashSet<Node*>, Vector<Node*> and similar non-owning containers keyed on Node*/Element*/EventTarget*; each is a candidate for the same dangling-key UAF and should either be weak or provably pruned on teardown.
  • size()/isEmpty() used as liveness on weak or soon-to-be-weak containers
    Search for callers using ->size() or ->isEmpty() on event-target and observer sets where computeSize()/isEmptyIgnoringNullReferences() would be semantically required; a raw size() over a container with dead slots signals stale-entry logic bugs.
  • Iterators taking a reference to a key then dereferencing
    Look for loops of the form RefPtr node = pair.key; … node or auto& over node containers without a liveness guarantee (grep absoluteEventRegionForNode-style callers and other absoluteForEventTargets helpers), which is the exact pattern that dereferenced the freed key here.
  • removeAll / prune-on-destroy obligations across WebCore observer registries
    Audit registries where node destruction must call a manual removeAll/unregister (touch, wheel, IntersectionObserver, mutation, resize observers); enumerate teardown paths (detach, willBeRemovedFromFrame, reentrant document destruction) and confirm every one prunes, or migrate the container to a weak type.

Original Bug Report

The reporter's bug is still restricted on the tracker.