[14] SWServer dereferences unchecked HashMap end iterator
두 client map이 race condition으로 동기화가 어긋날 때만 도달 가능한 HashMap end-iterator 역참조를 수정합니다. commit message에는 재현 방법이 알려져 있지 않다고 명시됩니다. 영향은 service-worker bookkeeping 경로에서의 crash에 국한되며, 공격자가 메모리 읽기/쓰기를 제어할 수 있다는 근거는 없습니다.
m_clientIdentifiersPerOrigin과 m_clientsById가 동기화 상태를 잃으면, topLevelServiceWorkerClientFromPageIdentifier()에서 crash가 발생합니다. 이 함수는 m_clientIdentifiersPerOrigin에서 client identifier를 순회하며 각각에 대해 m_clientsById.find()를 호출합니다. 그러나 역참조 전에 반환값이 m_clientsById.end()와 같은지 전혀 확인하지 않습니다.
Source/WebCore/workers/service/server/SWServer.cpp
Patch Details
serviceWorkerClientWithOriginByID()에서는 기존의 ASSERT(clientIterator != m_clientsById.end())가 실제 end-iterator 검사로 변경되었습니다. end 조건에 해당하면 release 빌드에서 std::nullopt를 반환합니다. topLevelServiceWorkerClientFromPageIdentifier()에서는 iterator->value.identifiers를 순회하며 m_clientsById.find(clientIdentifier)를 호출하던 루프가, 기존에는 결과를 무조건 역참조했습니다. 이제 end-iterator 검사와 함께 ASSERT_NOT_REACHED() 및 continue가 추가되어, 해당 identifier가 없는 항목은 건너뜁니다.
비동기 lifecycle 경로에서 paired-map 불변 조건이 어긋날 수 있는 환경에서, end-check 없이 HashMap iterator를 역참조하는 패턴.
Background
SWServer는 세션의 service-worker 상태를 process 경계를 넘어 관리하는 소유자입니다. m_clientIdentifiersPerOrigin은 HashMap<ClientOrigin, ...> 타입으로, 각 origin에 client로 등록된 모든 ScriptExecutionContextIdentifier를 identifiers 집합으로 관리합니다. m_clientsById는 동일한 identifier를 키로 사용하는 별도의 HashMap<ScriptExecutionContextIdentifier, RefPtr<ServiceWorkerClientData>>입니다. 두 map의 불변 조건은, per-origin identifiers 집합에 등장하는 모든 identifier가 반드시 m_clientsById에도 존재해야 한다는 전제에 기반합니다. HashMap::find()는 키가 없을 때 end()와 같은 iterator를 반환하는데, 이 end iterator를 역참조하면 undefined behaviour가 됩니다. WebCore의 ASSERT(...)는 release 빌드에서 컴파일 시 제거되므로, ASSERT만으로는 런타임 보호 효과를 기대할 수 없습니다.
Analysis
topLevelServiceWorkerClientFromPageIdentifier()는 m_clientIdentifiersPerOrigin의 per-origin 집합에서 client identifier를 가져온 뒤, m_clientsById.find(clientIdentifier)의 반환값이 m_clientsById.end()와 같은지 확인하지 않고 clientIterator->value를 역참조했습니다. 두 map은 서로 다른 code path를 통해 채워지고 해제됩니다. commit message에서는 이를, 두 map의 동기화가 어긋날 수 있는 race로 설명합니다.
serviceWorkerClientWithOriginByID()도 동일한 구조였습니다. ASSERT만 존재했기 때문에 debug 빌드에서는 문제가 없었지만, release 빌드에서는 ASSERT가 제거되어 end iterator를 무조건 역참조하게 되었습니다.
이 취약점은 SWServer를 호스팅하는 network process의 가용성을 약화시켰습니다. 두 client map의 동기화가 어긋나면 HashMap end-iterator 역참조에 도달하며, 결과는 service-worker bookkeeping 경로에서의 crash입니다. diff에서 공격자가 메모리 내용을 읽거나 쓸 수 있다는 근거는 없습니다. 동일한 identifier를 키로 사용하는 두 개의 병렬 HashMap을 별도의 code path로 유지하면서 ASSERT만으로 불변 조건을 강제하는 방식은, WebKit에서 반복적으로 나타나는 패턴입니다. commit message에서도 clientIsAppInitiatedForRegistrableDomain()이 이미 방어적인 end-check를 갖추고 있었음을 언급합니다. 버그의 본질은, 형제 메서드들이 이 패턴에서 이탈했다는 점입니다.
Audit directions
ASSERT만으로 불변 조건을 강제하는 paired HashMap 패턴 — "map A의 모든 키가 map B에도 존재한다"는 암묵적 가정.Source/WebCore/workers/service/server/SWServer.cpp및 관련SWServer*파일에서m_clientsById,m_clientIdentifiersPerOrigin,m_registrations,m_runningOrTerminatingWorkers에 대한.find()호출 중 end-check 없이 iterator를 역참조하는 지점을 점검해야 합니다.ASSERT(iterator != container.end())직후에iterator->value를 역참조하는 패턴.Source/WebCore/와Source/WebKit/NetworkProcess/ServiceWorker/에서 정규식ASSERT\(.*!= .*\.end\(\)\);를 검색한 뒤, 5줄 이내에 동일 iterator를 역참조하는 지점을 확인해야 합니다. 각 결과는 동일한 방어적 재작성이 필요한 후보입니다.m_clientIdentifiersPerOrigin과m_clientsById를 관리하는 등록/해제 code path.m_clientsById.add,m_clientsById.remove,m_clientIdentifiersPerOrigin.add,.identifiers.add,.identifiers.remove를 검색하여, 한쪽 map의 항목만 제거하고 다른 쪽은 그대로 두는 실행 순서가 있는지 파악해야 합니다. 또한 IPC 수신 과정에서 한쪽 map을 순회 중일 때 다른 쪽이 재진입적으로 변경될 수 있는지도 확인해야 합니다.clientIterator->value가RefPtr/std::unique_ptr인clientIterator->value->frameType체인에서, iterator가 유효하더라도 값이 null일 가능성.m_clientsById가ServiceWorkerClientData생성 또는 소멸 과정에서 일시적으로 null 값을 저장하는 경우가 있는지 확인해야 합니다.