← All issues

[4] TimingFunction reference-count race across the scrolling thread

Severity: Medium | Component: WebCore platform animation | c541bf0

Rated Medium because the diff converts a refcount that is provably touched from two threads to atomic, closing a data race that can drive the count to zero while a logical reference is live; reaching a usable UAF requires reliably winning the race on a renderer-reachable heap object, and the object's narrow attacker-controlled state keeps the immediate outcome a crash rather than a confirmed corruption primitive.

Because accelerated effects may be accessed from both the main thread and the scrolling thread on macOS, TimingFunction should use ThreadSafeRefCounted like the other ref-counted types used by AcceleratedEffect and AcceleratedEffectValues. The crash surfaced under ASan on an existing threaded-animations layout test; the commit notes the fix was suggested by an LLM during bug analysis and validated by the author.

Source/WebCore/platform/animation/TimingFunction.h

-#include <wtf/RefCounted.h>
+#include <wtf/ThreadSafeRefCounted.h>
...
-class TimingFunction : public RefCounted<TimingFunction> {
+class TimingFunction : public ThreadSafeRefCounted<TimingFunction> {

LayoutTests/webanimations/threaded-animations/timing-function-threading-check.html

+ const easing = "cubic-bezier(0.1, 0.7, 1.0, 0.1)";
+ const animations = [ target.animate(...{ duration, easing }), ... ];
+ await Promise.all(animations.map(animation => animationAcceleration(animation)));
+ // Both paths call AnimationEffectTiming::resolve() which protects the
+ // effect's TimingFunction; this must not trip the RefCounted threading check.
+ for (let i = 0; i < 50; ++i)
+ await UIHelper.remoteAnimationStackForElement(target);

The patch changes the base class of TimingFunction from RefCounted<TimingFunction> to ThreadSafeRefCounted<TimingFunction>, swapping the include accordingly. No logic in transformProgress, clone, or the subclasses (LinearTimingFunction, CubicBezierTimingFunction, StepsTimingFunction, SpringTimingFunction) is altered. The added test creates four accelerated animations with a cubic-bezier easing and repeatedly resolves the animation stack on the main thread while the scrolling thread concurrently applies effects, asserting the RefCounted threading check is not tripped.

Non-atomic reference counting on an object shared across threads, allowing the refcount to be corrupted by a data race.

RefCounted<T> is WTF's single-threaded reference-counted base: ref()/deref() mutate the count non-atomically and, in assertion-enabled builds, carry a thread-ownership check that traps if the count is touched from a thread other than the owner. ThreadSafeRefCounted<T> is the atomic counterpart, using atomic read-modify-write on the count so concurrent ref/deref from multiple threads is safe. Threaded (accelerated) animations on macOS run a copy of the animation effect data on the scrolling thread so scroll-driven and time-driven animations can be resolved without the main thread; AcceleratedEffect/AcceleratedEffectValues hold the effect's TimingFunction. TimingFunction::transformProgress() maps a linear progress value through the easing curve (e.g. cubic-bezier) and is called both from the main-thread keyframe interpolation path and the scrolling-thread scroll-animation path, each taking a transient ref on the shared TimingFunction for the duration of the call.

This is a data race on a non-thread-safe reference count, leading to use-after-free / double-free. Before the fix, TimingFunction derived from RefCounted, whose ref()/deref() perform plain non-atomic increments/decrements and embed a thread-ownership assertion. On macOS, accelerated animations make the same TimingFunction instance reachable from two threads simultaneously: the main thread resolves timing via AnimationEffectTiming::resolve() / KeyframeInterpolation while the scrolling thread applies effects via ScrollAnimationSmooth, with both contexts taking a transient RefPtr/protect() ref before calling transformProgress. When both threads take and drop these transient refs concurrently, the non-atomic read-modify-write races.

🔒

The cross-thread lifetime and reference-counting implications of this bug are analyzed in depth, including how realistic an escalation beyond the observed crash is.

Subscribe to read more

🔒

Multiple reusable audit patterns identified for finding thread-safety gaps across related WebKit animation subsystems, with concrete starting points.

Subscribe to read more