← All issues

[5] RemoteMediaPlayerProxy rejects duplicate CreateAudioSourceProvider

The GPU audio handler assumed a renderer would only ever ask once.

Severity: High | Component: WebKit GPU Process media IPC | 0181991

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를 최대 한 번만 보냅니다. 두 번째 메시지가 도달하면 AudioSourceProviderAVFObjCsetConfigureAudioStorageCallback / setAudioCallback이 실행되면서 tapStorage->lock 없이 콜백을 덮어씁니다. 이때 MediaToolbox thread가 해당 lock을 잡은 채로 그 콜백을 호출하고 있을 수 있으며, 결과적으로 사용 중이던 RemoteAudioSourceProviderProxy가 해제됩니다. 이 fix는 MESSAGE_CHECK로 중복 메시지를 거부합니다.

Source/WebKit/GPUProcess/media/RemoteMediaPlayerProxy.cpp

+#define MESSAGE_CHECK(assertion, message) MESSAGE_CHECK_WITH_MESSAGE_BASE(assertion, m_webProcessConnection.get(), message)
...
void RemoteMediaPlayerProxy::createAudioSourceProvider()
{
#if ENABLE(WEB_AUDIO) && PLATFORM(COCOA)
+ MESSAGE_CHECK(!m_remoteAudioSourceProvider, "RemoteAudioSourceProvider already created.");
+
RefPtr player = m_player;
if (!player)
return;
...
+#undef MESSAGE_CHECK

LayoutTests/ipc/create-audio-source-provider-twice.html

+ for (let i = 0; i < 5; i++) {
+ CoreIPC.GPU.RemoteMediaPlayerProxy.SetShouldEnableAudioSourceProvider(playerId, { shouldEnable: true });
+ CoreIPC.GPU.RemoteMediaPlayerProxy.CreateAudioSourceProvider(playerId, {});
+ await new Promise(resolve => setTimeout(resolve, 20));
+ }

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 참조 콜백 객체를 덮어쓸 수 있고, 사용 중이던 객체가 그 상태로 해제됩니다.

GPU process는 media element 하나당 RemoteMediaPlayerProxy 객체를 하나씩 호스팅하며, WebContent로부터 CreateAudioSourceProviderSetShouldEnableAudioSourceProvider 같은 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를 직접 보낼 수 있게 합니다.

이 취약점은 중복 메시지와 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에서 직접 확인할 수 있습니다.