[5] Complex text path did not retain the system fallbacks it used
The complex text path never learned the rule the simple path follows.
Medium입니다. Web에서 도달 가능하고 attacker가 타이밍을 조정할 수 있는 경로이지만, WTF weak pointer는 destruction 시점에 0으로 초기화되기 때문에 확인되는 영향은 stale-pointer read가 아니라 painting 중 발생하는 null-base fault입니다. DoS를 넘어선 확장이 성립하려면, purge 이전에 buffer로부터 raw Font alias를 미리 끌어내는 consumer가 필요합니다.
텍스트 렌더링은 shape된 run을 캐싱해서, 같은 문자열을 반복해서 그릴 때 re-shaping을 건너뛸 수 있도록 합니다. 이때 캐싱된 결과는 reference cycle을 피하기 위해 사용한 font를 약하게만 참조합니다. WebKit은 빠른 "simple" 경로를 사용하거나, 복잡한 script와 heavy kerning이 필요한 경우 ComplexTextController를 거쳐 텍스트를 shape하는데, Apple 플랫폼에서는 이 작업이 Core Text로 넘어갑니다. FontCascade 뒤에 있는 객체인 FontCascadeFonts는 shape된 텍스트 캐시와 함께 HashSet<Ref<Font>>를 갖고 있으며, 이 HashSet의 유일한 목적은 캐싱된 run이 참조하는 system fallback face를 계속 살아있게 유지하는 것입니다. Weak font reference가 안전하게 동작하려면, 캐싱된 glyph buffer에 Font를 기록하는 쪽이 반드시 그에 대한 strong owner도 함께 등록해야 한다는 계약이 지켜져야 합니다.
관전 포인트: 복잡한 script 텍스트를 캐싱된 run으로 렌더링하는 페이지는, 여전히 살아있는 캐싱된 run이 참조 중인 fallback face를 font cache가 회수하도록 유도할 수 있습니다. 이후 원하는 시점에 다시 그리게 만들어 renderer를 fault시킬 수 있습니다.
Commit message는 이 비대칭 구조를 직접적으로 짚습니다.
A
GlyphBuffercached in aFontCascadeFontsshaped text cache references itsFontsthrough weak pointers (308842@main). The simple text path keeps the system fallbacks it uses alive inFontCascadeFonts::glyphDataForSystemFallback, but the complex text path did not, so a Core Text fallback used only there had a single reference andFontCache::purgeInactiveFontDatacould destroy it while a cached shaped run still referenced it. Painting that run then dereferenced an expired weak pointer inFontCascade::drawGlyphBuffer.
Source/WebCore/platform/graphics/coretext/ComplexTextControllerCoreText.mm
Source/WebCore/platform/graphics/FontCascadeFonts.cpp
Tools/TestWebKitAPI/Tests/WebCore/FontCascade.cpp
Patch Details
FontCascadeFonts.h/.cpp에 public 함수 void FontCascadeFonts::addSystemFallbackFont(Ref<Font>&&)가 추가되었습니다. 이 함수는 기존 HashSet<Ref<Font>> m_systemFallbackFontSet에 값을 삽입합니다. 기존 simple-path 호출 지점인 FontCascadeFonts::glyphDataForSystemFallback에서는 m_systemFallbackFontSet.add(systemFallbackFont.releaseNonNull()) 호출이 addSystemFallbackFont(systemFallbackFont.releaseNonNull())로 변경되었습니다. 동작은 동일한 순수 리팩터링입니다.
실질적인 fix는 ComplexTextController::collectComplexTextRunsForCharacters에 있습니다. 기존에는 Core Text run font를 temporary에서 나온 bare pointer 형태로 만들었습니다 — runFont = protect(FontCache::forCurrentThread())->fontForPlatformData(runFontPlatformData).ptr() — 이 부분이 이번 패치에서 바뀌었습니다. 먼저 Ref systemFallbackFont = ...fontForPlatformData(runFontPlatformData)로 바인딩하고, RefPtr fonts = m_fontCascade->fonts()를 얻은 뒤, fonts->addSystemFallbackFont(systemFallbackFont.copyRef())를 호출해 FontCascadeFonts 객체가 strong reference를 갖도록 합니다. runFont = systemFallbackFont.ptr()은 그 이후에야 설정됩니다. 추가된 API test는 "Times" cascade 아래에서 complex-script 코드 포인트로 이루어진 run을 shaping하고, fallback들을 SingleThreadWeakHashSet<const Font>에 수집한 뒤 각각에 대해 EXPECT_FALSE(font.hasOneRef())를 검증합니다. 즉 FontCache 자신의 entry 외에 다른 owner가 각 fallback을 붙잡고 있는지 확인하는 것입니다.
동일한 동작을 수행하는 두 구현 사이의 비대칭적인 lifetime 처리. 한쪽 경로는 weak cache에 publish하는 리소스를 retain하지만, 다른 경로는 그렇지 않습니다.
Background
Simple vs. complex text path.
WebKit은 텍스트를 빠른 "simple" 경로(WidthIterator, 문자 단위 glyph lookup)로 shaping하거나, full shaping이 필요한 run에 대해서는 ComplexTextController를 거칩니다. Apple 플랫폼에서는 이 경로가 Core Text typesetting에 위임됩니다.
System fallback font.
지정된 font family가 특정 code point를 렌더링할 수 없으면, 플랫폼이 대체 face를 선택합니다. Complex 경로에서는 Core Text가 이 cascade 과정을 직접 수행하고 run마다 CTFont를 반환하며, WebKit은 이를 FontCache::fontForPlatformData(FontPlatformData)로 감쌉니다.
Reference types.
Ref<T>/RefPtr<T>는 WebKit의 strong reference-counting smart pointer입니다. .ptr()은 ownership을 넘기지 않은 채 raw pointer를 반환합니다. SingleThreadWeakPtr<T>는 zeroing weak pointer로, pointee를 살려두지 않으며 pointee가 파괴되면 이후의 읽기는 null을 반환합니다.
GlyphBuffer.
shaping 결과를 담는 구조체로, glyph ID, advance, origin, string offset, per-glyph font reference를 병렬 배열 형태로 보유합니다. font reference는 GlyphBuffer.h에 Vector<SingleThreadWeakPtr<const Font>> m_fonts로 선언되어 있으며, fontAt(index)는 *m_fonts[index]를 반환합니다.
FontCascadeFonts and the shaped text cache.
FontCascadeFonts는 FontCascade 뒤에 있는 RefCounted 객체로, realized fallback range, glyph-page cache, HashSet<Ref<Font>> m_systemFallbackFontSet("사용 중인 system fallback font를 살려두기 위한 용도"), GlyphGeometryCache, 그리고 TextShapingResultAndDisplayList entry들로 구성된 ShapedTextCache를 보유합니다. FontCascadeFonts::getOrCreateCachedShapedText()는 text run을 key로 shaping 결과를 memoize합니다. ShapedTextCacheDefaults의 튜닝 상수는 Canvas의 fillText/strokeText를 대상으로 문서화되어 있으며, maxTextLength = 128, maxSize = 3000 값을 사용합니다.
FontCache and purging.
FontCache::forCurrentThread()는 FontDataCache(HashMap<FontPlatformData, Ref<Font>>)와 purgeInactiveFontDataIfNeeded를 구동하는 m_purgeTimer를 소유합니다. cache 자신만 보유하고 있는 font는 memory pressure 상황에서 회수 대상으로 의도된 것입니다.
FontCascadeCache.
font description과 selector를 key로 FontCascadeFonts entry에 매핑되며, clearShapedTextCaches(), pruneUnreferencedEntries()(fonts.get().hasOneRef()인 entry를 제거), pruneSystemFallbackFonts()(내부적으로 FontCascadeFonts::pruneSystemFallbacks()를 호출)를 제공합니다.
Analysis
이 버그는 object-lifetime 위반에 해당합니다. cache된 구조체가 weak reference로 리소스를 참조하고 있는데, 이에 대응하는 strong owner가 없는 상태입니다.
Simple path Complex path (pre-fix)
─────────── ──────────────────────
glyphDataForSystemFallback collectComplexTextRunsForCharacters
font = fontForPlatformData(..) fontForPlatformData(..).ptr()
m_systemFallbackFontSet.add ─┐ └─► Ref dropped immediately
│
strong owner ┘ only owner: FontDataCache entry
│
GlyphBuffer.m_fonts[i] (weak) ──────┘
│
purgeInactiveFontData: hasOneRef ► destroy
│
repaint ► fontAt(i) ► *m_fonts[i] ← cleared weak
GlyphBuffer.h는 font 배열을 Vector<SingleThreadWeakPtr<const Font>> m_fonts로 선언하고 있습니다. 즉 buffer는 자신의 Font들을 weak하게만 참조합니다. commit message는 이 설계를 이전 revision인 308842@main에서 비롯된 것으로 설명합니다. 이 구조가 만들어내는 invariant는, 어떤 Font를 cache된 GlyphBuffer에 넣는 쪽이라면 그 cache entry가 살아있는 동안 해당 Font를 붙잡아둘 strong owner를 반드시 마련해야 한다는 것입니다. simple text 경로는 이 규칙을 지킵니다. glyphDataForSystemFallback은 자신이 resolve하는 모든 system fallback을 m_systemFallbackFontSet에 추가합니다. 반면 complex 경로는 그렇지 않았습니다. FontCache::fontForPlatformData(...).ptr()을 호출한 뒤 반환된 Ref를 즉시 버렸기 때문에, Core Text가 선택한 fallback에 대한 유일한 strong reference는 FontCache 자신의 FontDataCache entry뿐이었습니다.
FontCache::purgeInactiveFontData는 남아 있는 reference가 cache 자신의 것뿐인 Font를 회수합니다. 추가된 test는 hasOneRef()를 통해 정확히 이 조건을 그대로 인코딩하고 있고, 새로 추가된 test 주석도 문제 상황을 직접 서술합니다("FontCache::purgeInactiveFontData가 font를 회수하면, cache된 shaped run의 weak Font reference가 dangling 상태가 된다"). complex 경로에서만 사용되는 fallback은 이 조건을 만족할 수 있었습니다. 이때 살아 있는 FontCascadeFonts가 여전히 해당 fallback을 가리키는 GlyphBuffer를 담은 shaped run을 붙잡고 있는 상태였는데도 그랬습니다. Font가 파괴되고 나면 zeroing weak slot들이 clear되고, 이후의 paint가 GlyphBuffer::fontAt()에 도달하게 됩니다 — ASSERT(m_fonts[index]); return *m_fonts[index]; 코드에서 이 assertion은 release 빌드에서 컴파일 단계에 제거되므로, painting 도중 clear된 weak pointer를 dereference하는 결과로 이어집니다.
한 가지 세부사항은 제공된 context만으로는 확정되지 않습니다. FontCascadeFonts::pruneSystemFallbacks()는 m_shapedTextCache.clear()로 끝나는데, 이름과 함께 붙어 있는 retention 관련 주석으로 미루어 볼 때 m_systemFallbackFontSet도 함께 clear할 가능성이 높습니다. 다만 전체 본문은 제공된 context가 잘려 있어 확인할 수 없습니다. FontCascadeCache::pruneSystemFallbackFonts()는 m_entries를 순회하며 이 함수를 호출하므로, 통상적인 purge 경로에서는 shaped cache도 함께 invalidate됩니다. 따라서 이 취약점이 발생하는 window는 purge가 m_entries를 통해 도달하지 못하는 FontCascadeFonts를 필요로 할 가능성이 높습니다. 예를 들어 pruneUnreferencedEntries()나 maximumEntries random eviction이 map entry를 제거한 뒤에도 살아 있는 FontCascade가 여전히 참조하고 있는 경우, 혹은 애초에 map에 들어간 적이 없는 createForPlatformFont instance인 경우입니다. 또는 purgeInactiveFontData 내부에서 cascade cache가 clear되기 전에 font가 먼저 해제되는 순서 문제일 수도 있습니다.
diff에 등장하는 코드를 기준으로, 이론적으로 가능한 trigger 순서는 다음과 같습니다.
- 대상 스크립트를 커버하지 못하는 font family로 element나 canvas context를 styling합니다(test에서는 "Times"에 U+06D8, U+092D, U+0B40, U+0F96, U+0DBD, U+0EAF, U+A86C, U+0ACF를 사용). 이렇게 하면
collectComplexTextRunsForCharacters가effectiveFont->isSystemFontFallbackPlaceholder()분기를 타게 되고, 이 분기에서 Core Text가 run font를 선택합니다. - 같은 짧은 run을 반복해서
getOrCreateCachedShapedText가 이를 memoize하도록 만듭니다.ShapedTextCacheDefaults의 주석은 CanvasfillText/strokeText를 튜닝 대상 workload로 명시하고 있고,initialInterval = -3이라는 값은 entry가 처음 등장하는 순간부터 cache된다는 뜻입니다. 이 결과로 만들어지는GlyphBuffer는 fallback face를 weak하게만 기록합니다. FontCache::purgeInactiveFontData를 구동시킵니다. memory pressure를 유발하거나, 서로 다른 font description을 대량으로 churn시켜 cache를 age out시키는 방법을 쓸 수 있습니다. 이때 소유 중인FontCascadeFonts가pruneSystemFallbackFonts()에 의해 도달되지 않는 시점이어야 합니다. 예를 들어 살아 있는FontCascade가 여전히Ref를 쥐고 있는 상태에서,pruneUnreferencedEntries()나maximumEntriesrandom eviction이m_entriesslot을 제거한 이후가 이에 해당합니다. 이 조건에서, fix 이전에는 유일한 strong reference였던FontDataCache의 참조가 사라지면서 fallback이 파괴됩니다.- cache된 run을 다시 paint하면, glyph-drawing 경로가 이미 clear된 weak pointer를 담은
GlyphBufferslot을 읽게 됩니다.
3번 단계가 이 시나리오 전체를 지탱하는 동시에 가장 확인이 덜 된 부분입니다. 제공된 FontCache.cpp 발췌본은 purgeInactiveFontData 이전에서 잘려 있어서, shaped cache entry를 살려둔 채로 만드는 정확한 purge/prune 순서는 직접 읽은 것이 아니라 추론한 내용입니다.
Exploitability 관점에서 보면, 이 경로는 privileged API 없이 일반적인 웹 콘텐츠만으로 도달할 수 있습니다. 즉각적으로 관찰되는 영향은 painting 도중 clear된 SingleThreadWeakPtr<const Font>를 dereference하는 것입니다. fontAt()은 *m_fonts[index]를 반환하는데 release 빌드에서는 assertion이 컴파일 단계에서 제거되므로, 이어지는 member access가 null base에서 fault를 일으킵니다. 이는 stale-pointer read라기보다는 공격자가 스케줄링할 수 있는 renderer crash에 가깝습니다. WTF weak pointer는 파괴 시점에 0으로 초기화되기 때문에, cache된 GlyphBuffer 자체만으로는 흔히 보는 '해제된 slot을 재활용하는' 경로로 곧바로 이어지지는 않습니다. 더 강력한 primitive를 얻으려면, purge 이전에 const Font&를 buffer 밖으로 꺼내 raw alias(또는 CTFontRef/FontPlatformData 복사본)로 만들어두었다가 이후에 그 alias를 사용하는 consumer가 필요합니다. glyph-drawing 경로나 그 하위 호출에 그런 alias가 실제로 존재한다면, 그 경로는 Core Graphics glyph drawing에 platform data를 공급하는 Font에 대한 진짜 use-after-free로 이어질 가능성이 있고, 이 경우 heap grooming도 의미를 가지게 될 수 있습니다. 다만 제공된 context에는 FontCascade::drawGlyphBuffer의 본문이 포함되어 있지 않으므로, 이 확장 시나리오는 어디까지나 예상되는 방향입니다.
이 vulnerability는 WebContent process 내부의 memory safety를 약화시킵니다. 여기서 걸려 있는 security-model assumption은 GlyphBuffer가 weak Font reference를 보유하는 데 따라오는 ownership 계약입니다. cache된 shaped run에서 도달 가능한 모든 Font는 해당 cache entry보다 오래 살아남는 strong owner를 가져야 합니다. fix 이전에는 complex text 경로에서만 사용되는 Core-Text-선택 system fallback에 그런 owner가 없었습니다. 그 결과 평범한 페이지 콘텐츠와 font-cache purging이 결합되면 painting이 clear된 weak pointer를 dereference하는 상황으로 이어질 수 있었습니다. 이는 공격자가 작성한 콘텐츠가 임의로 유발할 수 있는 renderer 측 crash로, 안정적인 denial of service이자 crash-oracle/heap-state signal로도 활용될 수 있습니다.
Insight
이 케이스에서 흥미로운 지점은 'ownership 의무를 새로 만들어낸 mitigation'이라는 점입니다. GlyphBuffer::m_fonts를 weak로 만든 결정은 reference cycle을 없애고 buffer를 저렴하게 cache할 수 있게 해주었지만, 그 대가로 "GlyphBuffer에 Font를 쓰는 사람"이라는 규칙을 "GlyphBuffer에 Font를 쓰는 사람은 반드시 strong owner도 함께 등록해야 한다"는 규칙으로 조용히 바꿔놓았습니다. 두 개의 shaping 경로 중 이 규칙을 학습한 쪽은 하나뿐이었습니다. 패치가 추가한 주석("mirrors glyphDataForSystemFallback()") 자체가, 이 계약이 type system이 아니라 산문 형태로만 존재해왔다는 사실을 인정하는 셈입니다. 구조적으로 올바른 fix는 매 producer가 규칙을 기억하리라 믿는 대신, sink 쪽 — GlyphBuffer::add나 shaped-cache 삽입 지점 — 이 retention 책임을 지도록 만드는 것입니다. 한 가지 더 읽어둘 만한 layered-defense 신호가 있습니다. 기존에 있던 test PurgeInactiveFontDataClearsShapedTextCache는 팀이 이미 'purge가 cache를 함께 clear한다'는 것으로 충분한 보호라고 믿고 있었음을 보여줍니다. 이번 버그는 그 첫 번째 보장이 닿지 않는 경로에서, 실제로 지켜져야 했던 것이 더 약한 두 번째 보장이었을 때 벌어지는 일을 보여주는 사례입니다.
Audit directions
-
Weak-reference cache with externalized retention duty. cache가 공유 객체에 대해 non-owning reference만 보관하면서, retention 책임을 삽입 지점에서 강제하는 대신 관례적으로 각 producer에게 위임하는 패턴입니다. 여기서 지켜져야 할 invariant는 container가 weak reference를 보유한다면, strong reference를 소유해야 하는 쪽은 호출자가 아니라 container의 insert API여야 한다는 것입니다. 좁게는,
WebCore/platform/graphics에서GlyphBuffer::add/makeHole에 도달하거나TextShapingResult를 채우는 모든 호출 지점을 검색하고, 각각에 대응하는addSystemFallbackFont/m_systemFallbackFontSet삽입이 있는지 확인해야 합니다. 이번 fix된 라인과 동일한fontForPlatformData(...).ptr()형태를 갖는, Core Text가 아닌 shaping backend들(ComplexTextControllerHarfBuzz/Skia, Windows/DirectWrite 대응 코드)부터 살펴보는 것이 좋습니다. 조금 더 넓게는, value 타입에WeakPtr/SingleThreadWeakPtr/WeakHashSet멤버를 내장한 다른 WebCore cache들 —MixedFontGlyphPage::m_fonts,FontCascadeFonts::m_cachedPrimaryFont, platform 리소스를 보유하는 display-list recorder 등 — 을 점검하면서, 각각에 대해 cache의 lifetime이 weak하게 참조되는 객체의 lifetime을 넘어설 수 있는지 물어봐야 합니다. 가장 넓게 보면, 이는 intern/resource cache와 weak back-reference를 함께 갖는 어떤 시스템에서든 나타날 수 있는 일반적인 "weak-reference cache with externalized retention duty" 클래스에 해당합니다 — Chromium의 Skia typeface cache 안base::WeakPtr, Rust memoization table의Weak<T>, JavaWeakHashMap의 value graph 등이 같은 계열입니다. 좁은 범위에서의 match tell은, temporaryRef/RefPtr에 대해cache->getOrCreate(...).ptr()이나.get()형태로 호출한 뒤 그 결과를 해당 statement보다 오래 살아남는 어딘가에 저장하는 코드입니다. 조금 더 넓은 범위의 match tell은, weak 멤버와clear()/prune 메서드를 동시에 갖고 있으면서 일부 code path가 prune entry point를 거치지 않고도 그 구조체에 도달할 수 있는 경우입니다. 가장 넓은 범위의 match tell은, codebase에 "refcount == 1"을 읽는 eviction predicate가 있다면, 그 객체를 참조하면서도 의도적으로 refcount에 기여하지 않는 container가 무엇인지 물어보는 것입니다. -
하나의 논리적 operation을 서로 다르게 구현한 중복 코드. Fast path와 slow path, platform A와 platform B처럼, lifetime이나 security 의무가 한쪽 branch에만 구현되어 있는 경우입니다. 지켜야 할 invariant는 operation에 부여된 모든 의무는 그 operation의 모든 구현에서, 가급적 공유된 choke point에서 이행되어야 한다는 것입니다. Narrow:
FontCascadeFonts::glyphDataForSystemFallback과ComplexTextController::collectComplexTextRunsForCharacters를 diff해서 retention 이외의 behavioural divergence가 남아있는지 확인할 필요가 있습니다. small-caps 처리는 complex path에 이미 FIXME로 표시되어 있고, synthetic-oblique와 vertical-orientation 처리도 같은 방식으로 비교해볼 가치가 있습니다. Wider: 같은 방식의 대응 비교를 WebCore text의 다른 simple/complex 분기에도 적용할 수 있습니다 —FontCascade::layoutSimpleText대layoutComplexText,widthForSimpleText대 complex width path, 그리고 emphasis-mark와 text-decoration variant들입니다. 한쪽에서 발견된 각 의무에 대해 "twin도 이걸 하는가?"를 물어야 합니다. Widest: 이것은 하나의 데이터에 대해 최적화가 두 번째 code path를 만들어내는 모든 곳에 적용되는 "parallel implementation drift" 클래스에 해당합니다 — JIT fast path 대 interpreter slow path, SIMD 대 scalar kernel, 그리고 어떤 renderer에서든 cached 대 uncached branch가 그 예입니다. Match tell은 "mirrors X()" 또는 "like the simple path" 형태의 주석입니다. 이런 주석은 사람의 기억에만 의존해 강제되는 contract를 표시하는 것이며, 각각이 다음 drift가 발생할 후보 지점이 됩니다. -
sweep이 registry를 통해 객체를 열거하지만, 객체가 그 registry보다 더 오래 살아남을 수 있는 two-phase invalidation.
FontCache::purgeInactiveFontData와 이것이 invalidate해야 하는 cascade-level cache 사이의 순서 및 reachability contract를 점검할 필요가 있습니다. Narrow: purge 시점에 살아있는 모든FontCascadeFonts가FontCascadeCache::m_entries에서 도달 가능한지 추적해야 합니다.pruneUnreferencedEntries()는hasOneRef()기준으로 entry를 제거하고,maximumEntriesguard는m_entries.remove(m_entries.random())를 호출하며,FontCascadeFonts::createForPlatformFont는 등록되지 않을 수도 있는 instance를 생성합니다. 이 각각이m_shapedTextCache가 채워진 cascade가pruneSystemFallbackFonts()를 놓칠 수 있는 경로에 해당합니다. Wider: registry를 순회하며 dependent를 invalidate하는 다른 WebKit sweep들 — font-cache generation 증가에 따른 glyph-page invalidation,FontSelectorversion invalidation, display-list resource cache — 도 동일한 "registry는 ownership graph가 아니다" 문제가 있는지 점검할 필요가 있습니다. Widest: 재사용 가능한 invariant는 invalidation sweep은 그 enumeration set이 invalidate 대상 reference를 보유한 객체 집합의 superset일 때만 sound하다는 것입니다. 이는 GC-adjacent하거나 epoch 기반의 모든 reclamation scheme에 적용되며, Chromium resource cache의 refcount-plus-registry 설계나 lock-free container의 epoch reclamation도 포함됩니다. Match tell은 eviction policy를 함께 가진 container에 대한 cleanup loop입니다. entry가 container를 떠난 뒤에도 그 entry가 가리키던 객체가 살아있을 수 있다면, 그 sweep에는 구멍이 있는 것입니다. 이 부분은 source만으로 검증하기 쉽지 않습니다. cascade entry를 evict하면서FontCascade는 살려둔 채로 purge를 수행하는 targeted test를 만들면 경험적으로 확인할 수 있을 것입니다. -
platform-owned handle을 간접적으로 참조하는 cached rendering artifact. shaped-text cache의 sink 쪽이 font뿐 아니라 참조하는 어떤 resource보다도 더 오래 살아남지 않는지 확인해야 합니다. Narrow:
TextShapingResult와RefPtr<const DisplayList::DisplayList>를 짝지은TextShapingResultAndDisplayList를 살펴봐야 합니다. display list에 기록된 item들이 참조하는 대상 — font, image, gradient, platform surface — 을 열거하고,m_systemFallbackFontSet이 이제 font를 강하게 붙잡고 있는 것과 동일한 방식으로 각각이 entry의 lifetime 동안 강하게 held되는지 확인할 필요가 있습니다. Wider: 같은 질문이 WebCore와 GPU process의 display-list replay 경로에 있는, 오래 유지되는 recorded-drawing cache 전반에 적용됩니다. 이런 곳에서는 recorded item이 resource를 reference가 아니라 identifier로 지칭하는 경우가 있습니다. Widest: 이것은 record-then-replay 아키텍처 전반에서 흔히 나타나는 "serialized/recorded reference가 자신이 가리키는 대상보다 더 오래 살아남는" 클래스에 해당합니다 — graphics API의 command buffer, deferred rendering queue, 그리고 multi-process renderer의 remote-procedure display list가 그 예입니다. Match tell은 필드가 owning reference가 아니라 handle, identifier, 또는 weak pointer로 이루어진 cached 또는 serialized 구조체이면서, 사용 전에 그 handle을 다시 검증하지 않는 replay entry point와 결합되어 있는 경우입니다.