← All issues

[11] AudioContext destructor touches Document during Document's own destruction

Severity: Medium | Component: WebCore Web Audio | ed04ff4

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::~DocumentScriptExecutionContext::~ScriptExecutionContextBaseAudioContext::deleteMarkedNodesAudioContext::~AudioContextDocument::removeAudioProducer. The patch removes audio producers from AudioContext::stop() instead and guards ~AudioContext with !isStopped().

Source/WebCore/Modules/webaudio/AudioContext.cpp

AudioContext::~AudioContext()
{
m_mediaSession->invalidateClient();
 
- if (RefPtr document = this->document())
- document->removeAudioProducer(*this);
+ if (!isStopped()) {
+ if (RefPtr document = this->document())
+ document->removeAudioProducer(*this);
+ }
}
...
+void AudioContext::stop()
+{
+ if (RefPtr document = this->document())
+ document->removeAudioProducer(*this);
+ BaseAudioContext::stop();
+}

Source/WebCore/Modules/webaudio/AudioContext.h

// ActiveDOMObject
+ void stop() final;
void suspend(ReasonForSuspension) final;

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::commonTeardownScriptExecutionContext::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.

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.

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 teardown ordering and re-entrant destructor chain behind this UAF are traced end-to-end, with an assessment of how much attacker control is realistic during the destruction window.

Subscribe to read more

🔒

Four reusable audit patterns identified around ActiveDOMObject lifetimes and deferred-deletion queues, with concrete subsystem starting points for variant discovery.

Subscribe to read more