[11] AudioContext destructor touches Document during Document's own destruction
Rated Medium because the diff fixes a UAF-during-destruction where ~AudioContext (reached via BaseAudioContext::deleteMarkedNodes inside ~Document) writes into a partially-destroyed Document; attacker influence over the freed slot's contents is indirect, bounded by what allocations land in the released Document storage.
When the Document destructor is called, it is possible for it to take a code path where it references the Document which is actively being destroyed. The path is: Document::~Document → ScriptExecutionContext::~ScriptExecutionContext → BaseAudioContext::deleteMarkedNodes → AudioContext::~AudioContext → Document::removeAudioProducer. The patch removes audio producers from AudioContext::stop() instead and guards ~AudioContext with !isStopped().
Source/WebCore/Modules/webaudio/AudioContext.cpp
Source/WebCore/Modules/webaudio/AudioContext.h
Patch Details
Three changes restructure AudioContext teardown: ~AudioContext wraps the document->removeAudioProducer(*this) call in if (!isStopped()); a new override AudioContext::stop() calls document->removeAudioProducer(*this) before delegating to BaseAudioContext::stop(); and BaseAudioContext::stop() is promoted from private to public so the derived class can call it. stop() runs during Document::commonTeardown → ScriptExecutionContext::stopActiveDOMObjects, before the Document destructor begins. A manual ASAN reproducer is added.
Re-entrant call into a partially-destroyed owner object from a child's destructor that runs as a side effect of the owner's own teardown.
Background
AudioContext inherits from BaseAudioContext, which inherits from ActiveDOMObject (a per-Document lifecycle interface) and is ThreadSafeRefCounted. An ActiveDOMObject has a stop() hook that Document::commonTeardown calls via ScriptExecutionContext::stopActiveDOMObjects early in document teardown — before ~Document runs. BaseAudioContext::deleteMarkedNodes is a deferred-deletion sweep that drops references on audio nodes marked for deletion; when those references were the last ones, destruction of the owning AudioContext is chained from inside deleteMarkedNodes. Document::addAudioProducer/removeAudioProducer maintain a set of MediaProducer* on the Document tracking which contexts are currently producing audio. BaseAudioContext::isStopped() returns true once stop() has set m_isStopScheduled.
Analysis
The crash chain (from the commit message): Document::~Document runs, which transitively runs ~ScriptExecutionContext; that base destructor causes BaseAudioContext::deleteMarkedNodes to drop the last reference on an AudioContext whose deletion had been deferred; the resulting ~AudioContext then calls document->removeAudioProducer(*this). By that point, the Document subobject is mid-destruction — fields owned by Document (but not yet by ScriptExecutionContext) have already been destroyed in reverse-declaration order. removeAudioProducer mutates state on this half-destroyed Document object.
The fix breaks the cycle by hoisting removeAudioProducer into stop(), which Document::commonTeardown invokes via stopActiveDOMObjects before the destructor cascade starts; the !isStopped() guard in ~AudioContext is a defensive backstop in case any path reaches the destructor without stop() having run.
This vulnerability weakened memory safety in the renderer. The pre-fix code violates the invariant that an ActiveDOMObject must not touch its owning Document after the Document has begun destruction; this lets web-reachable JS construct an object graph whose teardown order causes a destructor to write into a half-destroyed Document. Successful exploitation would yield a UAF write into renderer heap memory previously owned by Document member subobjects — plausibly corrupting a freed HashSet (or equivalent container) storage. The class is recurring in WebCore: any ActiveDOMObject that participates in a deferred-deletion queue can have its destructor invoked from inside its owning Document's destructor cascade rather than from stop(). The pattern fix is consistent: do all owner-touching cleanup in stop(), keep destructors restricted to releasing self-owned resources, and use isStopped() as a defensive guard.
Audit directions
ActiveDOMObjectdestructors that touch their owningDocumentorScriptExecutionContext. The destructor can be reached from inside~Documentvia base-class teardown (~ScriptExecutionContextrunning deferred-deletion sweeps), at which pointDocumentmembers are already gone. GrepWebCorefor~.*ActiveDOMObject-derived destructors that calldocument()->...,scriptExecutionContext()->..., orexecutionContext()->...; start with subclasses ofBaseAudioContext,MediaSource,RTCPeerConnection,IDBDatabase, andWebSocket.- Deferred-deletion queues that drop final refs from inside a parent's destructor. Audit every
markForDeletion/deleteMarkedNodes/deleteUnfinished*helper inBaseAudioContext,MediaSource,MessagePort, and any subsystem that batches destruction. Verify queue contents have no destructor work that touches the owner — and that the queue is drained fromstop()/commonTeardownrather than from a base destructor. MediaProducerregistration without symmetricstop()-time deregistration. Grep foraddAudioProducer(/addMediaProducer(and check that each call site has a matching deregistration that runs before~Document.- Defensive
isStopped()/isContextStopped()guards present in some destructors but missing in others within the same hierarchy. Once this fix lands, audit sibling subclasses (OfflineAudioContext, otherBaseAudioContextderivatives) for the same~Foo→document->...shape and check whether they have the equivalent guard.