Branch: refs/heads/main
Home: https://github.com/WebKit/WebKit
Commit: 0fa28697001e02d0dbf4b9109bf7385f5f74f575
https://github.com/WebKit/WebKit/commit/0fa28697001e02d0dbf4b9109bf7385f5f74f575
Author: Kiet Ho <[email protected]>
Date: 2026-06-02 (Tue, 02 Jun 2026)
Changed paths:
M LayoutTests/intersection-observer/no-document-leak.html
M Source/WebCore/page/IntersectionObserver.cpp
Log Message:
-----------
[intersection-observer] Reference cycle between IntersectionObserver and
m_targetsWaitingForFirstObservation
rdar://178385445
https://bugs.webkit.org/show_bug.cgi?id=315964
Reviewed by Ryosuke Niwa.
IntersectionObserver holds ref-counted elements in
m_targetsWaitingForFirstObservation.
Its isReachableFromOpaqueRoots method looks like this:
bool IntersectionObserver::isReachableFromOpaqueRoots(JSC::AbstractSlotVisitor&
visitor) const
{
for (auto& target : m_observationTargets) {
if (containsWebCoreOpaqueRoot(visitor, target))
return true;
}
for (auto& target : m_pendingTargets) {
if (containsWebCoreOpaqueRoot(visitor, target.get()))
return true;
}
return !m_targetsWaitingForFirstObservation.isEmpty(); <== (1)
}
(1) means IntersectionObserver (more specifically, its JS wrapper) doesn't get
garbage collected if m_targetsWaitingForFirstObservation is not empty.
It's possible to set up a reference cycle such that IntersectionObserver keeps
elements in m_targetsWaitingForFirstObservation alive, and those elements also
keep IntersectionObserver alive.
intersection-observer/no-document-leak.html creates a lot of iframes, each
creating
one IntersectionObserver, then discards the iframes. This is done in one go
without
waiting for the main document to update its rendering between the steps, hence
the
observers don't get updated. When they aren't, IntersectionObserver::notify
doesn't
get called and m_targetsWaitingForFirstObservation doesn't get emptied.
After the iframe is removed:
(1) GC can't deallocate elements in m_targetsWaitingForFirstObservation, as
they're
being kept alive by the observer
(2) GC can't deallocate the observer, as
IntersectionObserver::isReachableFromOpaqueRoots
returns true because m_targetsWaitingForFirstObservation is not empty.
Hence the GC won't garbage collect either the elements or observer.
Fix this by checking if any elements in m_targetsWaitingForFirstObservation are
reachable
using containsWebCoreOpaqueRoot, like how it's done to m_pendingTargets. When
the iframe
is removed, containsWebCoreOpaqueRoot on the elements will be false, as their
opaque root
(the iframe root node) is already GC'ed when the iframe is removed. Then the
observer
can be GC'ed, and its destructor will destroy
m_targetsWaitingForFirstObservation and
elements in it (because at this point m_targetsWaitingForFirstObservation holds
the
only reference to the elements)
313834@main tried to fix this by adding:
// [...]
// Drop the first-observation keep-alive so a permanently detached
target/document
// can be collected (intersection-observer/no-document-leak.html).
if (!root() && !target.document().isFullyActive()) {
m_targetsWaitingForFirstObservation.removeFirstMatching([&](auto&
pendingTarget) {
return pendingTarget.ptr() == ⌖
});
[...]
But this doesn't address the root cause of the issue. It just so happens that
the
actual change (to make observer not update when the target's document is not
fully
active) uncovers this bug.
Test: intersection-observer/no-document-leak.html
* LayoutTests/intersection-observer/no-document-leak.html:
- Create more iframes to reliably reproduce the leak.
* Source/WebCore/page/IntersectionObserver.cpp:
(WebCore::IntersectionObserver::updateObservations):
(WebCore::IntersectionObserver::isReachableFromOpaqueRoots const):
Canonical link: https://commits.webkit.org/314380@main
To unsubscribe from these emails, change your notification settings at
https://github.com/WebKit/WebKit/settings/notifications