[2] Complex text path에서 system fallback font가 유지되지 않는 문제
Two text paths shared a cache — only one remembered to hold on.
Severity가 High로 평가된 이유는 다음과 같습니다. Complex path에서만 쓰이는 Core Text system fallback font가 shaped-text cache에서 weak reference로만 유지되고 있었습니다. 이 때문에 FontCache::purgeInactiveFontData가 해당 font를 해제할 수 있었고, 이때 살아있는 cached run이 여전히 그 font를 참조하는 상황이 가능했습니다. 결과적으로 painting 도중 web content에서 도달 가능한 dangling Font dereference가 발생합니다. Controlled primitive로의 확장은 heap grooming을 통해 해제된 slot을 재점유하는 과정이 필요한데, 이는 diff에서 확인되지 않습니다. 다만 crash로 이어지는 dereference 자체는 직접 도달 가능합니다.
FontCascadeFonts의 shaped-text cache에 저장된 GlyphBuffer는 자신의 Fonts를 weak pointer로 참조합니다 (308842@main). Simple text path는 FontCascadeFonts::glyphDataForSystemFallback에서 사용하는 system fallback을 계속 살아있게 유지하지만, complex text path는 그렇지 않았습니다. 그래서 그곳에서만 쓰이는 Core Text fallback은 참조가 단 하나뿐인 상태가 되었고, FontCache::purgeInactiveFontData가 이를 파괴할 수 있었습니다. 이때 cached shaped run은 여전히 그 font를 참조하고 있었습니다. 이후 해당 run을 painting하면 FontCascade::drawGlyphBuffer에서 만료된 weak pointer를 역참조하게 됩니다. Fix는 simple path와 동일한 방식으로 이 fallback들을 FontCascadeFonts에 유지시킵니다.
Source/WebCore/platform/graphics/coretext/ComplexTextControllerCoreText.mm
Source/WebCore/platform/graphics/FontCascadeFonts.cpp
Tools/TestWebKitAPI/Tests/WebCore/FontCascade.cpp
Patch Details
Fix에서는 FontCascadeFonts::addSystemFallbackFont(Ref<Font>&&)가 추가되었으며, 이 함수는 기존 m_systemFallbackFontSet (HashSet<Ref<Font>>)에 strong Ref<Font>를 삽입합니다. glyphDataForSystemFallback도 이 함수를 호출하도록 리팩터링되었습니다. 핵심 변경은 ComplexTextController::collectComplexTextRunsForCharacters에 있습니다. 기존에는 complex path가 fontForPlatformData(...).ptr()를 통해 맨 포인터만 취하고 반환된 Ref를 버렸습니다. 수정 후에는 font를 Ref systemFallbackFont에 담아 유지하고, addSystemFallbackFont(systemFallbackFont.copyRef())를 통해 FontCascadeFonts에 strong reference를 등록한 뒤에야 runFont = systemFallbackFont.ptr()로 추출합니다. 회귀 테스트는 Core Text fallback을 강제로 유발하는 complex-script text를 shaping하고, 사용된 각 fallback이 단일 reference로만 유지되지 않음을 검증합니다.
Weak reference로 캐시되는 객체가 두 개의 병렬 code path 중 한 쪽에서는 strong reference로 뒷받침되지 않았고, 그 결과 cache purge가 살아있는 cache entry의 대상을 해제할 수 있었던 패턴입니다.
Background
Complex text path는 primary font가 shaping하지 못하는 script와 문자 시퀀스(아랍어, 데바나가리, 티베트어)를 처리하며, Core Text에 위임하여 system fallback font를 선택합니다. 이 shaping을 주도하는 것이 ComplexTextController입니다. FontCascadeFonts는 cascade별 font 상태를 소유하는데, 여기에는 동일 run에 대한 반복적인 layout/paint를 빠르게 처리하기 위한 GlyphBuffer shaped-text cache가 포함됩니다. 308842@main 변경 이후, cached GlyphBuffer는 자신의 Font를 weak pointer(SingleThreadWeakPtr<const Font>)로만 참조하게 되어, cache 자체는 font를 살아있게 유지하지 않습니다. FontCache::purgeInactiveFontData는 외부 strong reference가 없는 font를 주기적으로 해제합니다. FontCascadeFonts의 HashSet<Ref<Font>> m_systemFallbackFontSet은 바로 이런 purge에서 살아남도록 cascade가 사용하는 fallback font의 strong reference를 붙잡아두기 위해 존재합니다. Ref<Font>는 null이 될 수 없는 strong smart pointer이며, .ptr()은 ownership을 넘기지 않고 raw pointer만 추출합니다. 따라서 .ptr()을 추출한 뒤 버려지는 rvalue Ref는 자신의 reference를 즉시 해제하게 됩니다.
Analysis
이 문제는 dangling weak pointer를 통한 use-after-free에 해당합니다. Fix 이전에는 complex path가 FontCache::fontForPlatformData로부터 Core Text system fallback Font를 얻은 뒤, 반환된 Ref를 즉시 버리고 raw pointer만 남겼습니다. 이 fallback으로부터 만들어진 shaped run은 GlyphBuffer로 캐시되는데, 이 GlyphBuffer는 자신의 Font를 weak reference로만 참조합니다. 결과적으로 해당 fallback에 대한 유일한 strong reference는 FontCache 내부에만 남아 있었습니다. FontCache::purgeInactiveFontData가 비활성으로 판단한 font를 회수할 때, FontCascadeFonts가 붙잡고 있는 strong reference가 없었으므로 purge는 마지막 strong ref를 제거하고 Font를 파괴했습니다. 이후 cached run의 weak pointer는 dangling 상태가 됩니다.
이 경로에 web content에서 도달하려면, 페이지가 선택한 font로 표시할 수 없는 complex-script 문자를 포함한 텍스트를 렌더링하여 complex path를 통해 Core Text fallback을 강제로 유발해야 합니다. 이때 shaped run이 반복적인 측정이나 paint를 통해 cache에 남아있어야 합니다 (header 기본값으로 미루어볼 때 이 cache는 Canvas fillText에 맞춰 튜닝된 것으로 추정됩니다). 그 다음 메모리 압박이나 font-cache churn을 통해 FontCache::purgeInactiveFontData를 유발하면, reference가 없는 fallback Font가 해제됩니다. 이후 cached run을 painting하면 FontCascade::drawGlyphBuffer에 도달하는데, 여기서 이미 dangling 상태가 된 weak Font pointer를 역참조하게 됩니다. 직접적인 영향은 glyph painting 도중 해제된 Font를 use-after-free로 역참조하는 것입니다. 만약 heap grooming을 통해 해제된 slot이 공격자가 제어하는 데이터로 재점유된다면, drawGlyphBuffer가 사용하는 Font 필드에 대한 controlled UAF read/write로 이어질 가능성이 있습니다. 다만 그러한 재점유가 이루어지지 않는다면 관찰되는 영향은 crash에 그칩니다.
이 vulnerability는 rendering process 내부의 memory safety를 약화시킵니다. Weak-pointer caching scheme이 정상 동작하려면, 살아있는 cached GlyphBuffer가 참조하는 모든 Font가 painting 가능한 동안 유효하게 유지되도록 어딘가에서 strong reference를 붙잡고 있어야 합니다. Fix 이전에는 complex path에서만 쓰이는 Core Text fallback에 대해 이 invariant가 지켜지지 않았습니다.
이는 "병렬 code path 두 개 중 하나는 유지하고 하나는 잊어버리는" 전형적인 lifetime bug이며, 308842@main에서 도입된 weak-pointer cache의 직접적인 결과입니다. Cache를 strong reference에서 weak reference로 전환하면, 그 cache에 삽입하는 모든 producer가 retention 책임을 떠안게 됩니다. Simple path는 이에 맞춰 업데이트되었지만, Core Text complex path는 누락되었습니다.
Note: 언급된 weak-pointer 전환(308842@main), purge routine의 정확한 회수 기준, 그리고 painting 시점의 drawGlyphBuffer 역참조는 이 diff 밖의 내용이며, commit message와 test comment로부터 추론된 것입니다. Fix가 바로잡는 retention 비대칭 자체는 변경된 call site에서 직접 확인됩니다.
Audit directions
- Weak reference로 전환된 cache는 모든 producer가 각자 strong reference를 붙잡아야 안전하며, 이를 빠뜨리는 producer가 하나라도 있으면 purge 이후 dangling pointer bug로 이어집니다.
FontCascadeFonts의 shaped-text/glyph cache와MixedFontGlyphPage에 값을 기록하는 모든 지점을 점검하여,Font를m_systemFallbackFontSet에 등록하는지 확인해야 합니다. Graphics/coretext와 platform font 코드에서FontCache::fontForPlatformData/fontForCharacter의 반환값에.ptr()을 호출하는 지점, 즉 반환된Ref가 raw pointer로 격하되는 지점을 grep하는 것부터 시작하면 됩니다. - Simple text path와 complex text path 사이의 retention 비대칭.
ComplexTextControllerCoreText.mm과 그에 대응하는 non-Core-Text 코드의 모든 fallback 선택 지점이FontCascadeFonts::glyphDataForSystemFallback에서 수행하는 retention을 동일하게 따르는지 확인해야 합니다.runFont/effectiveFont가 새로 생성된 font로부터 할당되는 모든 지점을 추적하여, strong reference가m_systemFallbackFontSet까지 도달하는지 확인해야 합니다. - 오래 유지되는 cache가 reference-counted 자원을
SingleThreadWeakPtr로만 참조하고, 별도의 purge routine이 이를 해제하는 패턴. Glyph page, image buffer 등WeakPtr/SingleThreadWeakPtr로 reference-counted 자원을 저장하는 다른 WebCore cache들을 각각의 purge routine과 대조하여, 모든 weak holder보다 strong owner가 더 오래 살아남는지 확인해야 합니다.SingleThreadWeakPtr<const Font>와WeakHashSet을purge/prune메서드와 함께 grep하면 됩니다.