← All issues

[2] GradientRendererCG: per-thread sampled gradient cache

Severity: Medium | Component: WebCore CoreGraphics gradient renderer | d6dbb47

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

#include "SampledGradientBuilder.h"
#include <pal/spi/cg/CoreGraphicsSPI.h>
#include <wtf/HashMap.h>
+#include <wtf/ThreadSpecific.h>
#include <wtf/TinyLRUCache.h>
...
GradientRendererCG::Gradient GradientRendererCG::makeGradientBySampling(ColorInterpolationMethod colorInterpolationMethod, const GradientColorStops& stops) const
{
auto colorStops = stops.sorted().stops();
- static NeverDestroyed<TinyLRUCache<WTF::SampledGradientCacheKey, RetainPtr<CGGradientRef>, 8>> cache;
- RetainPtr gradient = cache.get().get({ colorInterpolationMethod, colorStops, m_colorSpace });
+ static NeverDestroyed<ThreadSpecific<TinyLRUCache<WTF::SampledGradientCacheKey, RetainPtr<CGGradientRef>, 8>>> cache;
+ RetainPtr gradient = cache.get()->get({ colorInterpolationMethod, colorStops, m_colorSpace });
return Gradient { WTF::move(gradient) };
}

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.

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.

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.

🔒

What looks like a performance fix in a gradient renderer also tightens a concurrency invariant — the memory-safety implications of that change are analyzed in depth.

Subscribe to read more

🔒

Multiple reusable audit patterns for shared static caches and refcounted handle lifetimes across graphics subsystems, with concrete starting points for variant discovery.

Subscribe to read more