[1] LazyLoadVideoObserver use-after-free on cloning a video in a torn-down document
Severity: High | Component: WebCore DOM / HTML lazy-loading | b74177d
Rated High because the diff fixes a web-reachable use-after-free in the renderer: cloneNode() on a video element in a detached, GC-eligible document reaches a freed Document-owned LazyLoadVideoObserver, and the dereference happens on a deterministic, attacker-driven trigger; escalation to a stronger primitive requires reclaiming the freed TZone slot under heap grooming, which the object's limited attacker-controlled fields make non-trivial but do not block.
A REGRESSION(314874@main) crash report traced a use-after-free of the LazyLoadVideoObserver in LazyLoadVideoObserver::observe() after early destruction of the owning Document. Because the Document owns the LazyLoadVideoObserver, an operation that uses the observer must keep the document alive for the full duration. The fix holds a Ref to the Document across the clone, adds LIFETIME_BOUND to the lazyLoadVideoObserver() getter so the static analyzer would have flagged the unsafe code, and hardens LazyLoadVideoObserver with CanMakeCheckedPtr. The code and test derive largely from initial work by Kristian Monsen.
Source/WebCore/dom/Node.cpp
Ref<Node> Node::cloneNode(bool deep) const
{
RefPtr registry = CustomElementRegistry::registryForNodeOrTreeScope(*this, treeScope());
- return cloneNodeInternal(document(), deep ? CloningOperation::Everything : CloningOperation::SelfOnly, registry.get());
+ return cloneNodeInternal(protect(document()), deep ? CloningOperation::Everything : CloningOperation::SelfOnly, registry.get());
}
Source/WebCore/html/LazyLoadVideoObserver.cpp
void LazyLoadVideoObserver::observe(HTMLVideoElement& element)
{
- auto& observer = protect(element.document())->lazyLoadVideoObserver();
- RefPtr intersectionObserver = observer.intersectionObserver(protect(element.document()));
- if (!intersectionObserver)
- return;
- intersectionObserver->observe(element);
+ Ref document = element.document();
+ if (RefPtr intersectionObserver = protect(document->lazyLoadVideoObserver())->intersectionObserver(document))
+ intersectionObserver->observe(element);
}
Source/WebCore/dom/Document.h
- LazyLoadVideoObserver& lazyLoadVideoObserver();
+ LazyLoadVideoObserver& lazyLoadVideoObserver() LIFETIME_BOUND;
LayoutTests/fast/dom/lazy-video-clone-after-document-teardown-crash.html
+function createVideoInDetachedDocument() {
+ const document = Document.parseHTMLUnsafe("");
+ return document.createElementNS("http://www.w3.org/1999/xhtml", "video");
+}
+ const video = createVideoInDetachedDocument();
+ await new Promise(resolve => setTimeout(resolve, 0));
+ GCController.collect();
+ video.cloneNode(false);
Patch Details
Three production changes plus hardening. Node::cloneNode now wraps the document in protect(document()) before calling cloneNodeInternal, holding a Ref<Document> for the entire clone. LazyLoadVideoObserver::observe is rewritten to take a single function-scoped Ref document = element.document() and to wrap the document-owned observer in protect(...) (now a CheckedRef, since the type became CanMakeCheckedPtr) before calling intersectionObserver(document). The old code obtained auto& observer = protect(element.document())->lazyLoadVideoObserver(); under a statement-scoped temporary Ref and then used observer on the next statement. Document::lazyLoadVideoObserver() gains LIFETIME_BOUND; LazyLoadVideoObserver is made final and CanMakeCheckedPtr, and m_observer becomes a const RefPtr<IntersectionObserver> initialized via lazyInitialize.
Failure to hold an owning reference to a parent object across an operation that uses one of its owned sub-objects, letting the sub-object's reference dangle when the parent is destroyed.
Background
WebCore manages object lifetime with Ref<T>/RefPtr<T>, smart pointers that keep an object alive while held. A Ref created from a temporary expression — protect(x) used inline — lives only until the end of the full statement (the semicolon). A Document owns various helper objects as members, including m_lazyLoadObserver, a LazyLoadVideoObserver; when the Document is destroyed, those members are destroyed with it. LazyLoadVideoObserver lazily creates an IntersectionObserver (m_observer) to watch <video> elements and trigger loading when they intersect the viewport; its observe()/intersectionObserver() entry points fetch the per-document instance and operate on it.
Node::cloneNode delegates to cloneNodeInternal, which constructs a copy of the element and runs element setup that, for video elements, registers the clone with the document's LazyLoadVideoObserver. LIFETIME_BOUND is a Clang annotation that makes the static analyzer flag callers who let a returned reference outlive the object it was derived from. CanMakeCheckedPtr/CheckedPtr is a WebKit smart-pointer scheme that, in hardened builds, asserts the pointee has not been freed when dereferenced.
Analysis
This is a use-after-free from a lifetime-management error: a reference into an owned sub-object outliving its owner. Before the fix, Node::cloneNode passed document() — a bare Document& — into cloneNodeInternal without taking a Ref, so nothing on the stack kept the document alive while the cloned video element was constructed and registered for lazy loading. Cloning a video element reaches LazyLoadVideoObserver::observe(element), which obtained auto& observer = protect(element.document())->lazyLoadVideoObserver();. Here protect(element.document()) produces only a statement-scoped temporary Ref<Document> released at the semicolon, while observer is a long-lived reference into the document-owned object.
Once the protecting Ref is gone and the document is the detached, GC-eligible one from the trigger, the Document can be torn down, destroying the embedded LazyLoadVideoObserver; the subsequent observer.intersectionObserver(...) call dereferences freed memory.
Aaa Aaaaaaaaaa Aaaa Aaaaaaaaaaaa Aaa Aaaaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa a Aaaaaaaa Aaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaa a Aaaaaaaaa Aa Aaa Aaa Aa Aaaaaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaa Aa Aaaaaaa Aaaaaaa Aaaaaaaaaa a Aaa Aaaaa Aaaaaaa Aaaaa Aaaaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaa Aa Aaa Aaaaaaa Aaaa Aaaa a Aaaaaaaaa Aaaa Aaa Aaaaaaaaaaaaaa Aaaaaaaa Aaaaa Aaa Aaaaaaaaaaaaaaaa Aaaaaaaaaa Aaaaa Aaa Aaaaaaaaa
Aaaa Aa Aaaaaa Aaaaaaaaaaa Aa a Aaaaaaaaaaaaa Aaaaaaaa Aaa Aaaa a Aaaaaaaaaaaaa Aaaaaaaa Aaaaaaaaaa Aaaaaa a Aaaaaaaaaa Aaaaa Aaaaa Aaaaaaa Aaaa Aaaaaaaa Aa Aaaaaaa Aaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaa Aaaaaaaaaaaaaaaaa Aaaaaa Aaa Aaaaaaaaaaaa Aa Aaaaaaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaa Aaaaa Aaaaa Aaaaaaaaaaaa Aa Aaaaa Aaa Aaaaaaaaa Aaa Aaaaa Aa Aaaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaaaa Aa Aaaaaaa a Aaa Aaaaaaaa Aaaa Aaaaa Aa Aa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa a Aa Aaa Aaaaaaaaa Aaaaaaaaa Aa a Aaaaaaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaa Aa Aaaa Aaa Aaaaaaaaa Aaaaaaaaaaa Aaaaaaaaa
Aaaa Aaaaaaaaaaaaa Aaaaaaa Aaaaaa Aaaaaa Aaaaaa Aaa Aaaaaaaaaa Aaaaaaaa Aaaaaaaa Aaa Aaa Aaaaaaaa Aaaaa Aaaaaaa a Aaaaaaaaaa Aaaaaaaa Aaa Aaaaaaaaa Aaaaaaaa Aaa Aaaaa Aaaaaaaaaaaa Aaaaaa Aaa Aaa Aaaa Aaaaaaaaa Aaa Aaaaaaaaa Aa Aa Aaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaa Aa a Aaaaa Aaaaaaa Aa a Aaaaaaaaa Aaaaaaaa Aaaaa Aaaaa a Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaaa Aaaaa Aaaaaaa a Aaaaaaaa Aaaaaaa Aaaaaa Aa Aaaaaa Aaa Aaaa Aa Aaa Aaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaa Aaaaa Aaa Aaa Aaa Aaaaaaaaaa Aa Aa Aaaaaaa Aaaaaa Aa Aaaa Aaaa Aaaaaaaaaa Aaaaa Aaaaaaaaaaa Aaaa Aaaaaaa Aaaaaaaaa Aaaaaaaaaa Aaaa Aaaaaaaaaaaaaa Aaaaaaa Aaa Aa Aaaaaaa Aaaaaa Aaaaaaaa Aaa Aaaaaaaaaa Aaa Aaaaaaaa Aa Aaa Aaaaaa Aa Aaa Aaaaaaa
Aaaaaaaaa Aaa Aaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaa Aaa Aaaaa Aaaaaaaaaa Aaaaaaaa Aa Aaaaaaaaa Aaa Aaa Aaaaa Aaaaaaaaaaaaaaaaaa Aaaa Aaaa Aaa Aaaaaaaa Aaaa Aaa Aaaaaa Aaaaaaa Aaa Aaaaaaaaaaa Aaaa Aaaaaaaa Aaaaaa Aaaa Aaaaaaaa Aaaaaaa Aa Aaa Aaaaa Aaa Aaaa Aaaaaaaa Aaaaa Aaa Aaaaa Aaaaaaaaaa Aaa Aaaaaaaaaaaa Aaaaaaaaa Aa Aaa Aaaaaa
🔒The ownership and lifetime model behind this crash is traced end to end, with an assessment of whether the freed object can be turned into more than a crash.
Subscribe to read more
Audit directions
a Aaaaaaaaa Aaaaaaaaa a Aaaaaaaaa Aaaa a Aaaaaaaaaaaaaaaaaaa Aaaaaaa Aaaaa Aa a Aaaaaaaaaa Aaaaa Aaaaa a Aaaaaaaaaaaaaaaa Aaaaaaaaaaaa a Aaaaaa Aaaaaaa Aaa Aaaaa Aaaa Aaa Aaa Aaaaaaaaaaaaaaa Aaaaaaaa Aaaaaa Aaa Aaaaaaaa Aaaaaaaaa Aaaaaaaa Aaaa Aaa Aaaaa Aa Aaaa Aaaaa Aaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaaa a Aaaaaaaaaaaaaaaaa Aaaaaaa Aaaaaaaaa Aaaaaaaaaa Aa Aaaaa Aaaaaaa Aaaaa Aaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaa Aaa Aaaaaaa Aaaaa Aaa Aaaaaaaa Aaaaaaaaa Aaaaa Aaa Aaaaaaaaaa Aaaaaaa Aaaaaaaaaaaaaaa Aaaaa Aaaaa Aa Aaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaa Aaaaaaaa Aa Aaaaaa Aaaaaaaaaaa
a Aaaaa Aaaaaaaa Aaaaa Aaaaaa Aaaaaaa Aaaaaa Aaaaaaa Aaaaaaa Aaaa Aaaa Aaaaaaaaaaaa Aa Aaaa Aaaaaaaaa Aaaa Aaaaaaa Aaaa Aaa Aaaaaaa Aaaaaaaa Aaaaaa Aaaa Aaaaa a Aaaaaaaaaaaaaaa Aaaaaa Aaa Aaaaa Aaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaa Aaaaa Aaa Aaaaaaaaaaaaaaa Aaaaaaaaaaaa Aaaa Aaaaaaa Aaaaaaaaaaaaaa Aaaaaaaaa Aaaaaaa Aaaaaaaaaa Aaa Aaaaaaaaa
a Aaaaaaaaa Aaaa Aaaaaa Aaaaa Aaaaaaaaaaaaaaaa Aaa Aa Aaaaaa Aaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaa Aaa Aaaaaaa Aaaaaaaaa Aaaa Aa Aaaaa Aaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaaa Aa Aaaaaaa Aa Aaa Aaaaaaaa Aaaaa Aaaaaaaa Aaaaaaaaaaa Aaaa Aaaaaa Aaaaa Aa Aa Aaaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaa Aaaaaaaaaaaaaaaaaaaaaaaaa Aaa Aaaaaaaaaaaaaaaaaaaaaaa Aaaaaaaaa Aaa Aaaaaaaaa Aaaaaaaaaaa
🔒Multiple reusable audit patterns identified for finding sibling lifetime bugs across the DOM and lazy-loading subsystems, with concrete starting points.
Subscribe to read more