← WebKit Silent-Fix Report — 2026-W22

e57f9072af  FontPlatformSerializedAttributes::toCFDictionary should skip null key/value pairs in paletteColors and variations

severity medium class Other confidence 0.70 WebCore Font / GPU IPC exploitable-grade
Kristian Monsen Fri May 29 14:53:04 2026 -0700 full: e57f9072af0d65bc0f80777f1ba672b06eb16e43 bug report ↗ view on GitHub ↗
Primitive: null key/value in paletteColors crashes GPU process on CacheFont
Triage note: IPC-reachable null deref in the GPU process via crafted CacheFont message; skipping null pairs is a real crash-hardening fix.
Contents

The bug at a glance

OBSERVED: the PAIR_VECTOR_TO_DICTIONARY macro in FontPlatformSerializedAttributes::toCFDictionary now skips any key/value pair where item.first or item.second is null before calling CFDictionaryAddValue. INFERRED (and stated in the commit): paletteColors and variations are IPC-decoded vectors of optional-encoded key/value pairs, so a crafted CacheFont message can carry null RetainPtrs; passing NULL to CFDictionaryAddValue raises NSInvalidArgumentException that propagates uncaught and crashes the GPU process. Medium: an IPC-reachable, unauthenticated (any web content via WebProcess->GPUProcess) remote crash of the GPU process; a reliability/DoS hardening fix with no demonstrated memory corruption.

This is a trust-boundary deserialization bug. FontPlatformSerializedAttributes crosses from the (attacker-influenced) WebContent process into the GPU process via the CacheFont IPC. The key/value entries of paletteColors/variations are encoded as IPC optionals, so a false prefix legitimately decodes to a null RetainPtr. The macro that builds the CFDictionary assumed both halves were always present and fed them straight to CoreFoundation, which rejects NULL by throwing an Objective-C exception that no one catches – taking down the shared GPU process.

Root cause

FontPlatformSerializedAttributes::toCFDictionary() reconstructs a CoreText font descriptor’s attribute dictionary from data deserialized over IPC. Two of its fields, paletteColors and variations, are vectors of key/value pairs: the key is a RetainPtr<CFNumberRef> and the value a RetainPtr<CGColorRef> (palette colors) or RetainPtr<CFNumberRef> (variations). Each half is encoded in IPC as an optional – the wire format carries a bool ‘has value’ prefix, and a false prefix decodes to a null RetainPtr on the receiving side.

The conversion is driven by the PAIR_VECTOR_TO_DICTIONARY macro. Pre-patch it iterated the vector and unconditionally called CFDictionaryAddValue(newResult.get(), item.first.get(), item.second.get()). CFDictionaryAddValue with the kCFTypeDictionary callbacks does not tolerate a NULL key or value: CoreFoundation raises an NSInvalidArgumentException. This conversion runs in the GPU process while handling the CacheFont message from a WebContent process, and the exception is not caught anywhere on that path, so it propagates and crashes the GPU process.

Because the pair halves are attacker-controllable optionals, a WebContent process (driven by ordinary web content, or the IPC testing API) can send a CacheFont whose serializableAttributes.paletteColors or variations contains entries like [{}, {}] (both null) or [number, {}] (null value) or [{}, number] (null key). Any such entry reaches CFDictionaryAddValue with a NULL argument and triggers the crash.

The fix wraps the add in a guard: for each item, only call CFDictionaryAddValue when item.first && item.second, silently skipping any pair with a null key or value. This keeps CoreFoundation from ever seeing a NULL and preserves the well-formed entries. OBSERVED: the added IPC test sends CacheFont with several null palette-color and variation entries, then does a FinalizeRenderingUpdate round-trip; if the GPU had crashed the round-trip would time out, so its success verifies the process survived.

Key code

Skip null key/value pairs in PAIR_VECTOR_TO_DICTIONARY

#define PAIR_VECTOR_TO_DICTIONARY(key, vector) \
    if (vector) { \
        RetainPtr<CFMutableDictionaryRef> newResult = adoptCF(CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); \
-        for (auto& item : *vector) \
-            CFDictionaryAddValue(newResult.get(), item.first.get(), item.second.get()); \
+        for (auto& item : *vector) { \
+            if (item.first && item.second) \
+                CFDictionaryAddValue(newResult.get(), item.first.get(), item.second.get()); \
+        } \
        CFDictionaryAddValue(result.get(), key, newResult.get()); \
    }

Patch walkthrough

  • Source/WebCore/platform/graphics/coretext/FontPlatformDataCoreText.cpp — The PAIR_VECTOR_TO_DICTIONARY macro (used for paletteColors and variations in FontPlatformSerializedAttributes::toCFDictionary) changed its per-item body from an unconditional CFDictionaryAddValue(newResult.get(), item.first.get(), item.second.get()) to a guarded if (item.first && item.second) CFDictionaryAddValue(...). This prevents passing a NULL key or value – produced by an IPC optional decoding to a null RetainPtr – to CoreFoundation, which would otherwise throw an uncaught NSInvalidArgumentException and crash the GPU process.
  • LayoutTests/ipc/cache-font-null-palette-colors-crash.html — IPC test (IPCTestingAPIEnabled) that sends a CacheFont message whose serializableAttributes.paletteColors and variations contain null-keyed/null-valued pairs, then verifies GPU-process liveness with a FinalizeRenderingUpdate round-trip that would time out if the process had crashed.

Background

FontPlatformSerializedAttributes — IPC-serializable representation of a CoreText font descriptor’s attributes. toCFDictionary() rebuilds the CFDictionaryRef in the receiving (GPU) process from bytes originating in a WebContent process.

CacheFont IPC — A GPUConnectionToWebProcess / RemoteRenderingBackend message that ships font platform data to the GPU process for caching. Its platformData carries the serializableAttributes, including paletteColors and variations.

IPC optional encoding — Optionals are encoded as a bool presence prefix followed by the value only if present. A false prefix legitimately decodes to a null RetainPtr, so any optional-encoded field can arrive null from a crafted sender.

CFDictionaryAddValue with NULL — Under kCFTypeDictionary callbacks CoreFoundation rejects a NULL key or value by raising NSInvalidArgumentException. Uncaught in the GPU process, it crashes the whole process.

Vulnerability window

  1. Serialization design — paletteColors/variations pair halves were encoded as IPC optionals, making a null RetainPtr a representable decoded value.
  2. Unchecked conversion — PAIR_VECTOR_TO_DICTIONARY fed item.first/second directly to CFDictionaryAddValue with no null check.
  3. Crash — A crafted CacheFont with null palette/variation entries passed NULL to CoreFoundation, raising an uncaught NSInvalidArgumentException that crashed the GPU process.
  4. Discovery — bug 315820 / rdar://177612731 identified the IPC-reachable GPU-process crash.
  5. Fix + test — Add if (item.first && item.second) guard; new IPC test confirms GPU liveness via a FinalizeRenderingUpdate round-trip.

Proof of concept

VERBATIM from LayoutTests/ipc/cache-font-null-palette-colors-crash.html (IPCTestingAPIEnabled). paletteColors and variations carry pairs where one or both halves are {} (an absent optional -> null RetainPtr): [{},{}] (both null), [number,{}] (null value), [{},number] (null key). Pre-patch each null half reached CFDictionaryAddValue and threw an uncaught NSInvalidArgumentException, crashing the GPU process; the test then confirms the crash by a FinalizeRenderingUpdate round-trip that times out without the fix.

const number = { optionalValue: { get: { alias: { variantType: "char", variant: 1 } } } };
try {
    remoteBackend.CacheFont({
        data: { renderingResourceIdentifier: {}, origin: 0, isInterstitial: 0, visibility: 0, isTextOrientationFallback: 0 },
        platformData: {
            m_size: 12, m_orientation: 0, m_widthVariant: 0, m_textRenderingMode: 0,
            m_syntheticBold: false, m_syntheticOblique: false,
            serializableAttributes: { optionalValue: {
                fontName: "Helvetica", descriptorLanguage: "", descriptorTextStyle: "",
                matrix: {}, ignoreLegibilityWeight: {}, baselineAdjust: {}, fallbackOption: {},
                fixedAdvance: {}, orientation: {}, palette: {}, size: {}, sizeCategory: {},
                track: {}, unscaledTracking: {},
                paletteColors: { optionalValue: [
                    [{}, {}],
                    [number, {}],
                ] },
                variations: { optionalValue: [
                    [{}, {}],
                    [{}, number],
                    [number, {}],
                ] },
                opticalSize: {}, traits: {}, featureSettings: {}, additionalNumber: {}
            } },
            m_options: 0, m_url: {}, m_psName: {}
        },
        renderingResourceIdentifier: {}
    });
} catch { }

Exploitation

  1. Reach the IPC — From a compromised or scripted WebContent process (or via the IPC testing API), craft a CacheFont message to the GPU process’s RemoteRenderingBackend.
  2. Embed null pairs — Set serializableAttributes.paletteColors or variations entries with an absent optional for the key or value, decoding to a null RetainPtr in the GPU process.
  3. Crash GPU process — CFDictionaryAddValue receives NULL and raises NSInvalidArgumentException; uncaught, it crashes the shared GPU process, disrupting rendering for all clients – a cross-process DoS from web-reachable input.

Detection & hunting

For defenders and SOC / detection engineers:

  • NSInvalidArgumentException in GPU process
  • CacheFont with null palette/variation pairs

Audit directions

  • Optional-decoded IPC fields into CF/CG APIs
  • Font attribute reconstruction
  • Uncaught ObjC exceptions across IPC

Before / after

Loading diff…