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() == &target;
    });
    [...]

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

Reply via email to