← All issues

[3] Cross-thread Font refcount race on display-list transfer

A recorded font stays safe on one thread — until a canvas hands it to two.

Severity: High | Component: WebKit GPU process — RemoteRenderingBackend | ad056c8

High. A single-owner rule for a non-atomic refcount was broken by making the object reachable from two independently-threaded rendering backends after a buffer transfer. Winning the resulting refcount race yields a UAF in the GPU process; escalation past a controlled crash depends on heap-grooming the freed Font slot, which the change does not itself hand the attacker.

Cross-thread reference counting in WebKit's GPU-process rendering pipeline is normally kept safe by a single-owner rule: an object using non-atomic refcounts is only ever touched by the thread that built it. Each RemoteRenderingBackend — the GPU-process object that services rendering IPC for one WebContent client — owns a work queue and runs its drawing commands on that queue's own dedicated thread. A DisplayList-backed ImageBuffer — a recorded sequence of drawing Items replayed later — can be transferred between backends, but a WebCore::Font referenced by its DrawGlyphs items is built as single-thread RefCounted and is safe only on the thread that constructed it.

The angle: a page driving canvas rendering can move a display-list buffer between two GPU-process backend threads and race their ref/deref on a shared Font, freeing it while a live reference still exists — a use-after-free inside the GPU process, which is more privileged than WebContent with respect to graphics resources.

WebCore::Font uses non-thread-safe RefCounted. A DrawGlyphs display-list item holds a Ref<const Font>, and a DisplayList-backed ImageBuffer can be transferred to another RemoteRenderingBackend, which runs on its own work-queue thread. After the transfer both threads run ref and deref on the same Font, racing the non-atomic refcount and leading to a use-after-free.

Making Font ThreadSafeRefCounted would require dropping its single-thread weak pointers (e.g. GlyphPage weakly points back at its Font to break the ownership cycle) in favor of strong references, changing Font lifetime so fonts could no longer be purged. Instead, when a display list is transferred, each referenced Font is replaced with the data needed to rebuild it (its FontInternalAttributes and FontPlatformData) on the source thread, and rebuilt on the destination thread, recursing into nested DrawDisplayList items. Because Font stamps its single-thread weak pointer with the constructing thread, rebuilding on the destination keeps each Font constructed, used, and destroyed on a single thread. Separately, ImageBufferDisplayListBackend::copyNativeImage now replays through the backend's own ControlFactory instead of the main-thread ControlFactory::singleton(), which is not safe off the main thread.

Source/WebCore/platform/graphics/displaylists/DisplayListItems.h

+struct FontRebuildData {
+ FontInternalAttributes attributes;
+ FontPlatformData platformData;
+};
...
- Ref<const Font> font() const { return m_font; }
+ Ref<const Font> font() const { return std::get<Ref<const Font>>(m_font); }
+ void replaceFontWithRebuildData()
+ {
+ Ref<const Font> font = std::get<Ref<const Font>>(m_font);
+ m_font = FontRebuildData { font->attributes(), font->platformData() };
+ }
+ void rebuildFont()
+ {
+ auto& rebuildData = std::get<FontRebuildData>(m_font);
+ m_font = Ref<const Font> { Font::create(FontInternalAttributes { rebuildData.attributes }, FontPlatformData { rebuildData.platformData }) };
+ }
...
- Ref<const Font> m_font;
+ Variant<Ref<const Font>, FontRebuildData> m_font;

Source/WebKit/GPUProcess/graphics/RemoteRenderingBackend.cpp

void RemoteRenderingBackend::moveToSerializedBuffer(...)
MESSAGE_CHECK(imageBuffer->hasOneRef(), "ImageBuffer in use");
+ imageBuffer->replaceFontsWithRebuildData();
...
void RemoteRenderingBackend::moveToImageBuffer(...)
imageBuffer->transferToNewContext(creationContext);
+ imageBuffer->rebuildFonts();

Source/WebCore/platform/graphics/FontCustomPlatformData.h

-struct FontCustomPlatformData : public RefCounted<FontCustomPlatformData> {
+struct FontCustomPlatformData : public ThreadSafeRefCounted<FontCustomPlatformData> {

LayoutTests/ipc/move-to-image-buffer-cross-thread-font-crash.html

+ backendA.backend.MoveToSerializedBuffer({ identifier: bufferToTransfer.imageBufferIdentifier, serializedIdentifier });
+ backendB.backend.MoveToImageBuffer({ identifier: serializedIdentifier, ... });
+ for (let iteration = 0; iteration < 0x1000; ++iteration) {
+ transferredBuffer.CopyNativeImage({ image: randomIPCID() });
+ racingBuffer.graphicsContext.DrawGlyphs(drawGlyphs);
+ }

The change rewrites how a Font is stored inside display-list items, plumbs the rewrite hooks through the backend transfer methods, and promotes two supporting types to thread-safe refcounting. DrawGlyphs::m_font moves from Ref<const Font> to Variant<Ref<const Font>, FontRebuildData>, where FontRebuildData bundles the FontInternalAttributes and FontPlatformData needed to reconstruct the Font; two new operations, replaceFontWithRebuildData() (drops the shared Ref and stores the rebuild data) and rebuildFont() (calls Font::create(...)), are added.

Those operations are threaded up the stack: new static helpers replaceFontsWithRebuildDataInItems/rebuildFontsInItems in DisplayListRecorderImpl.cpp recurse over nested DrawDisplayList items, exposed through RecorderImpl, ImageBufferDisplayListBackend, ImageBufferBackend, and ImageBuffer. In RemoteRenderingBackend::moveToSerializedBuffer the source thread calls imageBuffer->replaceFontsWithRebuildData() after asserting hasOneRef(); in moveToImageBuffer the destination thread calls imageBuffer->rebuildFonts().

Finally, FontCustomPlatformData and (on Skia) SkiaHarfBuzzFont become ThreadSafeRefCounted so the rebuild data can safely move across threads, and ImageBufferDisplayListBackend::copyNativeImage replays through the backend's own m_controlFactory.

Sharing a non-thread-safe reference-counted object across a thread boundary, so concurrent refcount mutations race and drop the object while still live.

Non-atomic vs. thread-safe refcounting. RefCounted<T> increments/decrements an ordinary integer with no synchronization and is safe only when all ref/deref happen on one thread; ThreadSafeRefCounted<T> uses atomic operations. A Ref<T>/RefPtr<T> is a smart pointer that ref-counts T on construction/destruction.

RemoteRenderingBackend and its thread. RemoteRenderingBackend is the GPU-process object that services rendering IPC from a WebContent process; each instance owns a work queue and runs its drawing commands on that queue's dedicated thread. This per-instance threading is why an object safe on one backend is not automatically safe on another.

DisplayList and DrawGlyphs. A display list is a recorded sequence of drawing Items replayed later; DrawGlyphs is the item that draws text and holds a reference to the Font used.

ImageBuffer transfer. MoveToSerializedBuffer detaches an ImageBuffer from one backend into a serialized handle; MoveToImageBuffer reconstitutes it on another backend — potentially a different thread.

GPU-process font cache. CacheFont/CacheFontCustomPlatformData register a Font keyed by identifier, so multiple display-list items across buffers reference the same Font instance — the sharing that makes the cross-thread move dangerous.

The bug is a use-after-free driven by a data race on a non-atomic reference count.

  Backend A thread (retained buffer)   Backend B thread (moved buffer)
  ──────────────────────────────────   ──────────────────────────────────
  DrawGlyphs -> Font::ref()  ──┐
                               │        CopyNativeImage replays list
                               │        DrawGlyphs -> Font::deref()
                               │        (non-atomic, interleaves with A)
  Font::deref() ───────────────┘
                                        refcount hits 0 mid-flight
                                        ~Font() frees the Font
  next DrawGlyphs on A ──────► use freed Font   <- UAF

A single Font instance is shared across display-list items via the GPU-process font cache, so the same Font can be referenced by a display list inside an ImageBuffer that gets moved to a second backend while another ImageBuffer on the original backend still references it. After the transfer, the destination thread (replaying the moved buffer via copyNativeImagedrawGlyphs → Font ref/deref) and the source thread (still drawing glyphs with the same Font) both mutate the Font's non-atomic refcount concurrently. The race can lose an increment or decrement, so the count reaches zero while a live reference still exists, freeing the Font — or the destructor runs while the other thread is mid-operation.

The regression test reproduces this deliberately: it caches a Font, records DrawGlyphs referencing it into two display-list buffers on backend A, moves one to backend B, and then in a tight 0x1000-iteration loop calls CopyNativeImage on B (replaying the moved list) while calling DrawGlyphs on the retained buffer on A — driving both threads onto the shared non-atomic refcount. The minimal, IPC-level shape of this test reads like a reproduction constructed from reasoning about the threading model of the display-list transfer path, though a TSan hit on the Font refcount is equally plausible.

A won race gives a use-after-free on a Font object in the GPU process, reachable from WebContent over the buffer-transfer IPC. To weaponize past a controlled crash, an attacker would reclaim the freed Font slot under heap grooming — allocating like-sized objects immediately after the free so the dangling reference lands on attacker-influenced memory when the reused allocation is dereferenced. That escalation is conditional on heap-spray success against the Font's tzone allocation and on the degree of control over the reused allocation. The primitive lands in the GPU process, which is itself sandboxed, so a full chain to code execution would need additional escapes for broader compromise.

This vulnerability weakens memory safety inside the GPU process. The model assumption that a RefCounted object is only touched from one thread is violated, so the refcount invariant can break under a race, freeing a still-referenced Font.

The chosen fix is itself the insight here: rather than promoting Font to ThreadSafeRefCounted — which would break its single-thread weak-pointer cycle-breaking with GlyphPage and change font-purging semantics — the patch keeps each Font single-thread and serializes/rebuilds it across the boundary. Any object embedded in a transferable display list that is not thread-safe-refcounted is a candidate for the same bug.