[5] RemoteMediaPlayerProxy rejects duplicate CreateAudioSourceProvider
The GPU audio handler assumed a renderer would only ever ask once.
Severity가 High로 책정된 이유는 다음과 같습니다. 손상된 WebContent process가 중복된 CreateAudioSourceProvider 메시지를 보내면, MediaToolbox thread가 콜백을 호출하는 도중에 tapStorage->lock 없이 살아있는 콜백 closure를 덮어쓸 수 있습니다. 이 과정에서 더 높은 권한을 가진 GPU process 안에서 사용 중이던 RemoteAudioSourceProviderProxy가 해제됩니다. 더 강력한 primitive로의 확장은 audio thread가 동작하는 시간대 안에서 해제된 slot을 재확보해야 가능한데, 이는 diff만으로는 확인되지 않습니다. 따라서 안정적으로 도달 가능한 결과는 sandbox 경계를 넘는 GPU process crash입니다.
정상적으로 동작하는 WebContent process라면 RemoteMediaPlayerProxy 하나당 CreateAudioSourceProvider를 최대 한 번만 보냅니다. 두 번째 메시지가 도달하면 AudioSourceProviderAVFObjC의 setConfigureAudioStorageCallback / setAudioCallback이 실행되면서 tapStorage->lock 없이 콜백을 덮어씁니다. 이때 MediaToolbox thread가 해당 lock을 잡은 채로 그 콜백을 호출하고 있을 수 있으며, 결과적으로 사용 중이던 RemoteAudioSourceProviderProxy가 해제됩니다. 이 fix는 MESSAGE_CHECK로 중복 메시지를 거부합니다.
Source/WebKit/GPUProcess/media/RemoteMediaPlayerProxy.cpp
LayoutTests/ipc/create-audio-source-provider-twice.html
Patch Details
RemoteMediaPlayerProxy::createAudioSourceProvider()의 맨 앞에 MESSAGE_CHECK(!m_remoteAudioSourceProvider, "RemoteAudioSourceProvider already created.")가 추가되었습니다. 해당 player에 대해 provider가 이미 생성된 상태라면, 이 IPC 메시지를 거부하고 m_webProcessConnection을 대상으로 한 MESSAGE_CHECK_WITH_MESSAGE_BASE를 통해 문제를 일으킨 connection을 종료합니다. #define/#undef MESSAGE_CHECK 쌍은 이 매크로를 해당 파일의 web-process connection에 연결하는 역할을 합니다. 나머지 변경은 IPC Testing API를 통해 CreateAudioSourceProvider를 두 번 호출하는 regression test입니다.
IPC handler에 idempotency guard가 없으면, 중복 메시지가 동기화 없이 살아있는 cross-thread 참조 콜백 객체를 덮어쓸 수 있고, 사용 중이던 객체가 그 상태로 해제됩니다.
Background
GPU process는 media element 하나당 RemoteMediaPlayerProxy 객체를 하나씩 호스팅하며, WebContent로부터 CreateAudioSourceProvider와 SetShouldEnableAudioSourceProvider 같은 IPC 메시지를 수신합니다. AudioSourceProviderAVFObjC는 AVFoundation media stream을 Web Audio로 연결하는 Cocoa 구현체로, 오디오 샘플을 가져오기 위해 MediaToolbox의 real-time audio thread에서 나중에 호출되는 콜백(setConfigureAudioStorageCallback, setAudioCallback, 이른바 "tap" 콜백)을 설치합니다. tapStorage->lock은 main thread와 audio thread 사이에서 이 tap state에 대한 접근을 직렬화하는 역할을 합니다. MESSAGE_CHECK/MESSAGE_CHECK_WITH_MESSAGE_BASE는 WebKit의 IPC 검증 매크로로, assertion이 실패하면 해당 메시지를 악의적인 것으로 간주하고 문제를 일으킨 connection을 종료합니다. m_remoteAudioSourceProvider는 생성된 provider를 담는 멤버로, 여기서는 "이미 생성되었는지"를 판별하는 sentinel로 사용됩니다. IPCTestingAPIEnabled는 CoreIPC를 테스트용 JavaScript에 노출시켜, 메시지를 한 번만 보내는 WebContent bindings를 우회하고 raw IPC를 직접 보낼 수 있게 합니다.
Analysis
이 취약점은 중복 메시지와 idempotency guard 부재가 cross-thread lifetime 및 locking 문제와 결합되어 발생하는 use-after-free입니다. Fix 이전의 createAudioSourceProvider()는 정상적으로 동작하는 WebContent process가 proxy 하나당 CreateAudioSourceProvider를 최대 한 번만 보낼 것이라고 가정했을 뿐, 이를 강제하지는 않았습니다. 두 번째 메시지가 들어오면 provider 설정 경로가 다시 실행되어 AudioSourceProviderAVFObjC::setConfigureAudioStorageCallback / setAudioCallback에 도달하고, 이전에 설치된 콜백 closure를 덮어씁니다. Commit message에 따르면 이 덮어쓰기는 tapStorage->lock을 잡지 않은 상태에서 이루어지며, 이와 동시에 MediaToolbox audio thread가 그 lock을 잡은 채로 같은 콜백을 호출하고 있을 수 있습니다. 이 덮어쓰기로 인해 이전에 캡처되어 있던 RemoteAudioSourceProviderProxy의 마지막 reference가 사라지는데, audio thread는 여전히 이 객체를 참조하며 dereference를 시도하는 상태입니다.
Test가 모델링하는 exploit 방향은 다음과 같습니다. 먼저 활성 상태인 player에 대해 CreateAudioSourceProvider를 보내고, MediaToolbox tap thread가 tapStorage->lock을 잡은 채로 설치된 콜백을 호출하기 시작하도록 만든 뒤, 두 번째 CreateAudioSourceProvider를 보냅니다. 두 번째 메시지는 lock 없이 콜백을 덮어쓰며, audio thread가 여전히 dereference하고 있는 상태에서 캡처된 proxy의 마지막 reference를 없애버립니다. 만약 공격자가 controlled heap condition 하에서 이 racing thread의 시간대 안에 해제된 RemoteAudioSourceProviderProxy allocation을 재확보할 수 있다면, 진행 중인 콜백의 dereference가 공격자가 조작한 메모리를 대상으로 동작하게 될 가능성이 있습니다. 이 경우 GPU process 안에서 type confusion이나 control-flow primitive로 이어질 가능성도 있습니다. 다만 최소한으로 확실하게 도달 가능한 결과는 WebContent에서 유발할 수 있는 GPU process crash입니다.
이 취약점은 WebContent sandbox와 GPU process 사이의 cross-process trust boundary를 약화시킵니다. 원래 보안 모델은 GPU process의 IPC handler가 손상되었을 가능성이 있는 WebContent process로부터 오는 잘못된 형식이나 순서가 어긋난 메시지에도 견고하게 동작한다고 가정합니다. 그런데 이 handler는 그 대신 발신자가 메시지를 최대 한 번만 보낼 것이라고 신뢰하고 있었습니다. 취약한 코드가 더 높은 권한을 가진 GPU process에서 실행되고, 이를 유발하는 데 필요한 조건이 WebContent로서 IPC를 보낼 수 있는 능력뿐이라는 점에서, 이 버그는 WebContent에서 GPU로 sandbox 경계를 넘는 성격을 갖습니다.
이 fix는 재설치 과정을 thread-safe하게 만드는 대신, 중복 메시지 자체를 거부하는 저비용의 견고한 방식을 선택했습니다. 다른 thread가 캡처한 콜백을 설치하거나 교체하는 모든 IPC entry point는 동일한 패턴의 후보가 될 수 있습니다.
Note: 콜백을 덮어쓰는 메커니즘, 그 시점에 tapStorage->lock이 없다는 사실, 그리고 audio thread에서 dereference 도중 발생하는 해제는 commit message에 근거한 내용이며 diff만으로는 확인되지 않습니다. Diff에는 추가된 guard와 test만 나타나 있습니다. 반면 이 guard가 메워주는 idempotency gap 자체는 diff에서 직접 확인할 수 있습니다.
Audit directions
- GPU/Networking process의 IPC handler 중 singleton resource를 생성하거나 설치하면서도 "이미 생성됨" guard가 없는 경우, 중복 메시지가 setup을 다시 실행시켜 살아있는 state를 덮어쓸 수 있습니다.
Source/WebKit/GPUProcess에서RemoteMediaPlayerProxy와 그 형제격인Remote*Proxy클래스들을 점검하여, null 여부를 먼저 확인하지 않고 멤버를 변경하는 create/setup 메서드가 있는지 확인해야 합니다. 해당 멤버에 대한MESSAGE_CHECK가 선행되지 않은 채m_remote*를 대입하는 handler를 검색해 보십시오. - Main thread에서 설치되지만 real-time media/tap thread에서 호출되는 콜백 closure가, 해당 thread의 lock을 잡지 않은 채로 교체되거나 파괴되는 경우.
AudioSourceProviderAVFObjC::setConfigureAudioStorageCallback/setAudioCallback을 비롯한 MediaToolbox tap-callback setter들을 점검하여, 모든 writer가tapStorage->lock을 잡는지 확인해야 합니다.WebCore/platform/audio에서 저장된 lambda가 ref-counted proxy를 캡처하는 setter 메서드를 검색하는 것부터 시작하십시오. - "정상적인 발신자는 한 번만 호출할 것"이라는 가정이
MESSAGE_CHECK로 강제되지 않는 경우.Source/WebKit/GPUProcess와Source/WebKit/NetworkProcess에서 single-shot으로 문서화된 handler를 검색하여, 각각이 replay에 대비한 idempotency guard나 state-machine guard를 갖추고 있는지 확인해야 합니다. - scoped/singleton resource를 획득하는 다른
Remote*Proxy메서드들(rendering resource, sandbox extension, layer hosting context 등)이 IPC를 통해 이중으로 호출될 수 있는지, 그리고 그 결과 하위 resource에 대한 double-consume이나 double-free가 발생할 수 있는지도 점검해야 합니다.