[2] GradientRendererCG: per-thread sampled gradient cache
Rated Medium because the diff removes unsynchronized concurrent mutation of a static LRU cache holding RetainPtr<CGGradientRef> entries, eliminating a race between eviction-driven release and concurrent reads. The race plausibly yields a UAF or refcount imbalance on a CoreGraphics handle reachable from any web content that drives concurrent gradient sampling, though the diff does not establish a concrete control primitive.
GradientRendererCG::makeGradientBySampling() used a static 8-entry TinyLRUCache shared across all threads. When multiple threads sample gradients concurrently, they evict each other's entries from the small cache, collapsing the hit rate and forcing repeated CGGradientRef rebuilds. The fix wraps the cache in WTF::ThreadSpecific so each thread gets its own 8-entry LRU; the per-thread working set fits cleanly in 8 entries, and the shared mutable structure is gone by construction.
Source/WebCore/platform/graphics/cg/GradientRendererCG.cpp
Patch Details
The change is a single-line restructuring of the static cache declaration plus an accessor update. static NeverDestroyed<TinyLRUCache<...>> becomes static NeverDestroyed<ThreadSpecific<TinyLRUCache<...>>>; cache.get().get(...) becomes cache.get()->get(...) because ThreadSpecific<T>::operator->() returns a T* for the current thread's instance, lazily constructed on first access. <wtf/ThreadSpecific.h> is added. No locking is introduced and no other code path is touched.
Unsynchronized concurrent mutation of a shared static LRU cache holding refcounted handles.
Background
GradientRendererCG builds CGGradientRefs for CSS, SVG, and Canvas gradients. The sampling path is used whenever the interpolation color space is non-sRGB or any color component is none, and is invoked from whichever thread is currently executing graphics work — the main thread, scrolling / display-list threads in the GPU process, or off-main-thread image decoders.
TinyLRUCache<K, V, N> is a fixed-capacity (N=8 here) least-recently-used cache stored in-place. get(key) returns the stored value on a hit; on a miss it calls the policy's createValueForKey to materialize a new value, inserts it, and evicts the least-recently-used entry. Both paths mutate the cache: hits update LRU ordering, misses replace a slot. NeverDestroyed<T> is WebKit's pattern for a function-local static constructed on first use and intentionally never destructed. WTF::ThreadSpecific<T> is WebKit's portable wrapper over pthread / Win32 thread-local storage. RetainPtr<CGGradientRef> calls CGGradientRetain / CGGradientRelease on assignment and destruction; assigning over a RetainPtr releases the prior value, which may be the last reference.
Analysis
The bug is a data race on a shared mutable structure. Before the fix, every thread that called makeGradientBySampling reached the same process-wide TinyLRUCache instance. The commit message frames the symptom as thrashing — cross-thread eviction collapses the hit rate — but the underlying property the fix changes is sharing of a mutable structure across threads without synchronization. The cache exposes no internal locking and the call site holds none.
The concurrent mutation surface has two arms. On the LRU ordering side, threads racing through get() on a hit promote different entries to most-recently-used simultaneously, and the slot array's index-ordering metadata can be written by thread A while thread B is reading it. On the value side, an eviction on thread B drops the last RetainPtr<CGGradientRef> reference for a slot that thread A is still consuming. The window for the second arm is narrow but precise: if thread A has loaded a raw CGGradientRef* from the cache slot and is mid-call into a CoreGraphics routine, an eviction-driven CGGradientRelease on thread B can deallocate the underlying CG object while thread A's pointer is still live. The fact that the cache is small (8 entries) and the working set per thread is also small actually increases the eviction rate, making the race more reachable rather than less.
A WebContent process able to drive concurrent gradient sampling — multiple Canvas2D contexts, SVG documents with animated gradients across multiple compositor threads, or coordinated off-main-thread image decode — can therefore exercise the race from untrusted content. The downstream effect on a successful race is a UAF or refcount imbalance on a CoreGraphics handle inside the renderer or GPU process. The diff does not establish how the freed object's contents are reused — that depends on CoreGraphics' internal allocator placement and on the relative timing of release versus reuse — so the concrete read/write primitive is not directly observable from this patch.
This vulnerability weakens the memory-safety boundary inside the renderer and GPU process by violating the invariant that data structures shared across threads either carry internal synchronization or are confined to a single thread. The mitigation strategy is worth noting: the maintainers chose ThreadSpecific rather than introducing a lock, which suggests either the cache is hot enough that locking would regress performance, or that no safe locking discipline could be retrofitted to this site without rewriting TinyLRUCache itself.
Audit directions
-
Function-local
static NeverDestroyed<...>caches reachable from multiple threads without synchronization. Grep WebCore forstatic NeverDestroyed<followed byTinyLRUCache,HashMap,HashSet, orLRUCacheinside non-static member functions, then check whether the enclosing function is invoked off the main thread (graphics, image decoding, font shaping, layout workers). Start withSource/WebCore/platform/graphics/andSource/WebCore/platform/image-decoders/. -
Shared caches whose values are CoreGraphics / CoreFoundation
RetainPtrs, where eviction drops the last reference. Audit anyTinyLRUCache<K, RetainPtr<...>>andHashMap<K, RetainPtr<...>>in graphics code for thread-safety: a race between eviction-release and a concurrent reader of the same slot is a UAF candidate even when the cache itself looks small and safe. GrepRetainPtr<CG,RetainPtr<CF,RetainPtr<CTinside cache types underSource/WebCore/platform/graphics/cg/andSource/WebCore/platform/graphics/cocoa/. -
Per-thread caches as a stand-in for missing synchronization. Review other uses of
WTF::ThreadSpecificin WebKit to see whether each one replaced a previously shared mutable structure; those sites can document, by their git history, prior races that may still have variants elsewhere. Rungit log -S 'ThreadSpecific' -- Source/WebCore/platform/graphics/and inspect the prior implementations. -
TinyLRUCacheitself for documentation of thread-safety expectations. If the template carries no contract, every existing instantiation across WebKit is an audit candidate; enumerate them withgrep -rn 'TinyLRUCache<' Source/and verify each call site is either single-threaded or externally locked.
Note: Some specifics — the absence of internal synchronization in TinyLRUCache, the exact set of off-main-thread call sites for makeGradientBySampling, the behavior of ThreadSpecific::operator->() — are inferred from surrounding code patterns rather than directly visible in the diff. The core race condition and its remediation are directly supported by the patch.