[2] Complex text path retains system fallback fonts
Two text paths shared a cache — only one remembered to hold on.
Rated High because a Core Text system fallback font used only by the complex path was held only weakly by the shaped-text cache, so FontCache::purgeInactiveFontData could free it while a live cached run still referenced it, yielding a web-content-reachable dangling-Font dereference during painting; escalation to a controlled primitive requires reclaiming the freed slot under heap grooming, which the diff does not establish, but the crashing dereference is directly reachable.
A GlyphBuffer cached in a FontCascadeFonts shaped-text cache references its Fonts through weak pointers (308842@main). The simple text path keeps the system fallbacks it uses alive in FontCascadeFonts::glyphDataForSystemFallback, but the complex text path did not, so a Core Text fallback used only there had a single reference and FontCache::purgeInactiveFontData could destroy it while a cached shaped run still referenced it. Painting that run then dereferenced an expired weak pointer in FontCascade::drawGlyphBuffer. The fix retains those fallbacks on the FontCascadeFonts the same way the simple path does.
Source/WebCore/platform/graphics/coretext/ComplexTextControllerCoreText.mm
Source/WebCore/platform/graphics/FontCascadeFonts.cpp
Tools/TestWebKitAPI/Tests/WebCore/FontCascade.cpp
Patch Details
The fix adds FontCascadeFonts::addSystemFallbackFont(Ref<Font>&&), which inserts a strong Ref<Font> into the existing m_systemFallbackFontSet (HashSet<Ref<Font>>); glyphDataForSystemFallback is refactored to call it. The load-bearing change is in ComplexTextController::collectComplexTextRunsForCharacters: where the complex path previously took a bare pointer via fontForPlatformData(...).ptr() and dropped the returned Ref, it now holds the font in Ref systemFallbackFont, registers a strong reference on the FontCascadeFonts via addSystemFallbackFont(systemFallbackFont.copyRef()), and only then extracts runFont = systemFallbackFont.ptr(). The regression test shapes complex-script text that forces Core Text fallbacks and asserts each used fallback is not held by a single reference.
A weakly-referenced cached object was not backed by a strong reference on one of two parallel code paths, so a cache purge could free it while a live cache still pointed at it.
Background
The complex text path handles scripts and character sequences the primary font cannot shape (Arabic, Devanagari, Tibetan), delegating to Core Text, which selects system fallback fonts; ComplexTextController drives this shaping. FontCascadeFonts owns per-cascade font state, including a shaped-text cache of GlyphBuffers so that repeated layout or paint of the same run is fast. Per change 308842@main, cached GlyphBuffers hold their Fonts through weak pointers (SingleThreadWeakPtr<const Font>), so the cache does not itself keep fonts alive. FontCache::purgeInactiveFontData periodically frees fonts that have no external strong references. The HashSet<Ref<Font>> m_systemFallbackFontSet on FontCascadeFonts exists to hold strong references to fallback fonts used by a cascade so they survive purges. Ref<Font> is a non-null strong smart pointer; .ptr() extracts the raw pointer without transferring ownership, so an rvalue Ref whose .ptr() is taken and then discarded releases its reference.
Analysis
This is a use-after-free via a dangling weak pointer. Before the fix, the complex path obtained a Core Text system fallback Font from FontCache::fontForPlatformData and immediately dropped the returned Ref, keeping only a raw pointer. The shaped run produced from this fallback is cached as a GlyphBuffer, which references its Fonts only weakly. Consequently the only strong reference to that fallback lived inside FontCache itself. When FontCache::purgeInactiveFontData reclaimed fonts it considered inactive, with no strong reference held by FontCascadeFonts, the purge dropped the last strong ref and destroyed the Font. The cached run's weak pointer then dangled.
To reach it from web content, render text containing complex-script characters the page's chosen font cannot display, forcing Core Text fallbacks via the complex path; ensure the shaped run is cached through repeated measurement or paint (the cache is tuned for Canvas fillText, inferred from the header defaults). Then induce FontCache::purgeInactiveFontData through memory pressure or font-cache churn so the unreferenced fallback Font is freed. Subsequent painting of the cached run reaches FontCascade::drawGlyphBuffer, which dereferences the now-dangling weak Font pointer. The immediate effect is a use-after-free dereference of a freed Font during glyph painting. If the freed slot were reclaimed with attacker-controlled data under heap grooming, this could evolve into a controlled UAF read/write over Font fields consumed by drawGlyphBuffer; without that reclamation the observed effect is a crash.
This vulnerability weakens memory safety within the rendering process. The weak-pointer caching scheme relies on some owner holding a strong reference so that any Font referenced by a live cached GlyphBuffer stays valid as long as the entry can be painted; before the fix that invariant was violated for Core Text fallbacks used only by the complex path.
This is a classic "two parallel code paths, one retains, one forgets" lifetime bug, and it is a direct consequence of the weak-pointer cache introduced in 308842@main: converting a cache from strong to weak references shifts the retention burden onto every producer that inserts into it. The simple path was updated; the Core Text complex path was missed.
Note: The referenced weak-pointer conversion (308842@main), the purge routine's exact reclamation criterion, and the painting-time dereference in drawGlyphBuffer are external to this diff and inferred from the commit message and test comments. The retention asymmetry the fix corrects is directly visible in the changed call site.
Audit directions
- A cache converted to weak references relies on every producer separately holding a strong reference; any producer that forgets creates a dangling-pointer-after-purge bug. Audit all writers into the
FontCascadeFontsshaped-text/glyph caches andMixedFontGlyphPagefor whether they register theFontinm_systemFallbackFontSet. Start by grepping for.ptr()on the result ofFontCache::fontForPlatformData/fontForCharacterin graphics/coretext and platform font code, where a returnedRefis dropped to a raw pointer. - Retention asymmetry between simple and complex text paths. Verify that every fallback-selection site in
ComplexTextControllerCoreText.mmand its non-Core-Text counterparts mirrors the retention done inFontCascadeFonts::glyphDataForSystemFallback. Trace each placerunFont/effectiveFontis assigned from a freshly created font to confirm a strong ref reachesm_systemFallbackFontSet. - Objects referenced only through
SingleThreadWeakPtrfrom a long-lived cache and freed by an independent purge routine. Audit other WebCore caches that storeWeakPtr/SingleThreadWeakPtrto reference-counted resources (glyph pages, image buffers) against their purge routines to confirm a strong owner outlives every weak holder. Grep forSingleThreadWeakPtr<const Font>andWeakHashSetalongsidepurge/prunemethods.