← All reports

HashMap iterator use-after-free in WebSWClientConnection::notifyRecordResponseBodyChunk

WebKit Service WorkersUAF

One half of the Service Worker protocol was fixed for re-entrancy; the other wasn't.

Component: WebKit Service Workers | 20af97e

Source/WebKit/WebProcess/Storage/WebSWClientConnection.cpp

void WebSWClientConnection::notifyRecordResponseBodyChunk(RetrieveRecordResponseBodyCallbackIdentifier identifier, IPC::SharedBufferReference&& data)
{
- auto iterator = m_retrieveRecordResponseBodyCallbacks.find(identifier);
- if (iterator == m_retrieveRecordResponseBodyCallbacks.end())
+ auto callback = m_retrieveRecordResponseBodyCallbacks.take(identifier);
+ if (!callback)
return;
auto buffer = data.unsafeBuffer();
bool isDone = !buffer;
- iterator->value(WTF::move(buffer));
- if (isDone)
- m_retrieveRecordResponseBodyCallbacks.remove(iterator);
+ callback(WTF::move(buffer));
+ if (!isDone)
+ m_retrieveRecordResponseBodyCallbacks.add(identifier, WTF::move(callback));
}

WebSWClientConnection은 WebProcess에서 Service Worker IPC를 중개하는 역할을 담당합니다. 여기에는 background fetch response body chunk를 BackgroundFetchResponseBodyLoader에 전달하는 작업도 포함되는데, 이 loader는 FetchResponse body를 뒷받침하는 ReadableStream에 데이터를 공급합니다. 진행 중인 body retrieval에 대한 callback은 callback identifier를 key로 하는 HashMap에서 관리됩니다. 이때 HashMap의 iterator와 reference는 rehash를 유발하는 어떤 insertion에 의해서도 무효화될 수 있습니다.

기존 코드는 entry를 찾아 iterator->value(...)를 호출한 뒤, 같은 iterator를 다시 사용해 해당 entry를 remove()했습니다. 반면 새 코드는 먼저 map에서 callback을 take()로 꺼낸 뒤 이를 호출하고, streaming이 계속되는 경우에만 다시 삽입합니다.

Before:                                          After:
find(id) -> iterator                            take(id) -> callback (removed from map)
  |                                                |
  iterator->value(data)  <-- may reenter           callback(data)  <-- may reenter
  |     and insert into same map, rehash!          |     safe: iterator no longer held
  |                                                |
  map.remove(iterator)  <-- iterator may be stale  if (!isDone) map.add(id, callback)

여기서 callback은 임의의 WebKit 내부 코드이며, 같은 connection 객체로 동기적으로 다시 진입할 수 있습니다. 호출된 callback이 retrieveRecordResponseBody로 동기적으로 재진입하면, HashMap이 rehash되면서 이후 remove()에 사용되던 iterator가 무효화될 수 있습니다. 이는 일반적인 Service Worker background fetch streaming 흐름에서 도달 가능한 use-after-free에 해당합니다. 형제 함수인 notifyRecordResponseBodyEnd는 이미 take-then-invoke 구조를 통해 정확히 같은 문제를 피하도록 수정되어 있었습니다. 따라서 이번 fix는 같은 프로토콜을 이루는 두 함수 사이의 일관성을 되찾은 셈입니다.

이번 건은 WebKit의 IPC callback map 처리 전반에 걸친 re-entrancy 버그 클래스가 공개적으로 patch된 사례에 해당합니다. 문제의 패턴은, callback이 스스로 변경할 수 있는 map에 대한 HashMap iterator나 reference를 여전히 쥔 상태로 저장된 Function<>을 호출하는 구조입니다. 좁게 보면, WebKit 전체에서 find()iterator->value(...)remove(iterator) 순서로 이루어진 다른 sequence들을 grep해볼 필요가 있습니다. take-then-invoke 구조와 대비되는 이런 패턴은 IPC 대상 클래스의 callback map 주변에서 찾아야 하며, WebSWClientConnection, WebSWServerConnection, 그리고 이와 유사한 background fetch, service worker, network-process callback registry들이 자연스러운 출발점이 됩니다. 같은 객체로 동기적으로 재진입해서 같은 map에 삽입할 수 있는 callback이라면 모두 동일한 UAF의 후보가 됩니다. 코드 리뷰에서 눈여겨볼 지점은, find()로 선언된 iterator의 lifetime이 저장된 callable을 통한 호출 구간까지 걸쳐 있는 형태입니다. 넓게 보면, callback 호출 도중 유지되는 모든 container iterator나 reference가 같은 패턴에 해당합니다. append가 가능한 handler를 거치는 Vector element reference나, notification dispatch 도중의 HashSet iterator도 마찬가지입니다. 가장 넓게 보면, container handle은 그 container가 변경 가능한 동안에만 유효하며, 임의의 코드를 호출하는 행위 자체가 변경의 기회가 된다는 원칙으로 귀결됩니다. 이때 계속 던져야 할 질문은 "이 callback이 내가 순회 중인 collection으로 다시 도달할 수 있는가?"입니다.