[11] AudioContext destructor touches Document during Document's own destruction
~Document 내부의 BaseAudioContext::deleteMarkedNodes를 통해 도달하는 ~AudioContext가 절반쯤 파괴된 Document에 쓰기를 수행하는 UAF-during-destruction을 수정하는 패치이므로 Medium으로 평가됩니다. 해제된 슬롯 내용에 대한 attacker의 영향력은 간접적입니다. Document storage가 해제된 뒤 그 자리를 차지하는 allocation에 따라 제한됩니다.
Document destructor가 호출되는 시점에, 현재 파괴 중인 Document를 참조하는 code path로 진입할 가능성이 있습니다. 해당 경로는 다음과 같습니다: Document::~Document → ScriptExecutionContext::~ScriptExecutionContext → BaseAudioContext::deleteMarkedNodes → AudioContext::~AudioContext → Document::removeAudioProducer. 패치는 audio producer 제거를 AudioContext::stop()에서 처리하도록 변경하고, ~AudioContext를 !isStopped() 조건으로 보호합니다.
Source/WebCore/Modules/webaudio/AudioContext.cpp
Source/WebCore/Modules/webaudio/AudioContext.h
Patch Details
AudioContext teardown을 재구성하는 변경은 세 가지입니다. 먼저 ~AudioContext가 document->removeAudioProducer(*this) 호출을 if (!isStopped()) 조건으로 감쌉니다. 다음으로 새로운 override인 AudioContext::stop()은 BaseAudioContext::stop()에 위임하기 전에 document->removeAudioProducer(*this)를 먼저 호출합니다. 마지막으로 BaseAudioContext::stop()의 접근 제한이 private에서 public으로 상향되어, 파생 클래스에서 직접 이를 호출할 수 있게 됩니다. stop()의 실행 시점은 Document::commonTeardown → ScriptExecutionContext::stopActiveDOMObjects 흐름 내, Document destructor 시작 이전입니다. 수동 ASAN reproducer도 추가되었습니다.
소유 객체 자신의 teardown에 의한 부수 효과로 실행되는 child destructor에서, 절반쯤 파괴된 소유 객체로의 re-entrant 호출.
Background
AudioContext는 BaseAudioContext를 상속하며, BaseAudioContext는 ThreadSafeRefCounted이면서 ActiveDOMObject(per-Document lifecycle interface)를 상속합니다. ActiveDOMObject는 stop() hook을 제공하는데, Document::commonTeardown이 ~Document 실행 이전에 ScriptExecutionContext::stopActiveDOMObjects를 통해 이를 호출합니다. BaseAudioContext::deleteMarkedNodes는 삭제 예정으로 표시된 audio node의 reference를 해제하는 deferred-deletion sweep입니다. 해당 reference가 마지막 참조였을 경우, 소유 AudioContext의 소멸이 deleteMarkedNodes 내부에서 연쇄적으로 발생합니다. Document::addAudioProducer/removeAudioProducer는 Document 위에서 MediaProducer* 집합을 관리하며, 현재 audio를 생성 중인 context를 추적합니다. BaseAudioContext::isStopped()가 true를 반환하는 시점은 stop()이 m_isStopScheduled를 설정한 이후입니다.
Analysis
commit message에 기록된 crash chain의 흐름은 다음과 같습니다. Document::~Document가 실행되면 ~ScriptExecutionContext가 연쇄적으로 실행됩니다. 이 base destructor로 인해 BaseAudioContext::deleteMarkedNodes가 삭제 지연된 AudioContext의 마지막 reference를 해제합니다. 그 결과 ~AudioContext가 호출되고, 내부에서 document->removeAudioProducer(*this)가 실행됩니다. 이 시점에 Document subobject는 파괴 진행 중인 상태입니다. Document에 속하지만 ScriptExecutionContext에는 아직 속하지 않은 필드들이 이미 선언 역순으로 파괴된 뒤입니다. removeAudioProducer는 이 절반쯤 파괴된 Document 객체의 상태를 변경합니다.
패치는 removeAudioProducer 호출을 stop()으로 끌어올림으로써 이 순환을 끊습니다. Document::commonTeardown이 stopActiveDOMObjects를 통해 stop()을 호출하는 시점은 destructor cascade가 시작되기 이전입니다. ~AudioContext의 !isStopped() guard는 stop() 실행 없이 destructor에 도달하는 경로를 대비한 방어적 안전장치입니다.
이 취약점은 renderer의 memory safety를 약화시킵니다. 패치 이전 코드는 "ActiveDOMObject는 Document 파괴가 시작된 이후 해당 Document를 건드려서는 안 된다"는 invariant를 위반합니다. 이로 인해 웹에서 도달 가능한 JS로, teardown 순서가 destructor 안에서 절반쯤 파괴된 Document에 쓰기를 유발하는 object graph를 구성할 수 있습니다. exploit에 성공하면 Document member subobject가 차지했던 renderer heap 메모리에 UAF write primitive를 얻게 됩니다. 이를 통해 해제된 HashSet(또는 동등한 컨테이너)의 storage를 손상시킬 가능성이 있습니다. 이 패턴은 WebCore에서 반복적으로 나타납니다. deferred-deletion queue에 참여하는 모든 ActiveDOMObject는 stop()이 아닌 소유 Document의 destructor cascade 내부에서 destructor가 호출될 가능성이 있습니다. 패턴 차원의 수정 방향은 일관됩니다. 소유 객체를 건드리는 cleanup은 전부 stop()에서 처리하고, destructor는 자기 소유 자원 해제에만 집중하며, isStopped()를 방어적 guard로 활용해야 합니다.
Audit directions
- 소유
Document또는ScriptExecutionContext를 건드리는ActiveDOMObjectdestructor. 이 destructor는 base 클래스 teardown(~ScriptExecutionContext의 deferred-deletion sweep 실행)을 통해~Document내부에서 도달할 수 있습니다. 이 시점에는Document멤버가 이미 파괴된 상태입니다.WebCore에서document()->...,scriptExecutionContext()->...,executionContext()->...를 호출하는ActiveDOMObject파생 클래스의 destructor를 검색하시기 바랍니다. 우선BaseAudioContext,MediaSource,RTCPeerConnection,IDBDatabase,WebSocket의 서브클래스부터 살펴보시기 바랍니다. - parent destructor 내부에서 최종 reference를 해제하는 deferred-deletion queue.
BaseAudioContext,MediaSource,MessagePort, 그리고 소멸을 일괄 처리하는 모든 서브시스템에서markForDeletion/deleteMarkedNodes/deleteUnfinished*헬퍼를 점검해야 합니다. queue 내용의 destructor 작업이 소유 객체를 건드리지 않는지, 그리고 queue가 base destructor가 아닌stop()/commonTeardown에서 비워지는지 확인하시기 바랍니다. stop()시점의 대칭적 등록 해제 없이 등록만 이루어진MediaProducer.addAudioProducer(/addMediaProducer(를 검색하고, 각 호출 지점에~Document이전에 실행되는 매칭 등록 해제가 있는지 확인하시기 바랍니다.- 동일 계층 내에서 일부 destructor에는 있지만 다른 곳에는 없는 방어적
isStopped()/isContextStopped()guard. 이 수정이 반영된 이후, 형제 서브클래스(OfflineAudioContext및 기타BaseAudioContext파생 클래스)에서 동일한~Foo→document->...형태가 존재하는지, 그리고 동등한 guard가 있는지 점검하시기 바랍니다.