[4] TimingFunction reference-count race across the scrolling thread
An easing curve that two threads reference-counted by hand — until a lost increment drops the count to zero while a live reference is still using it.
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
LayoutTests/webanimations/threaded-animations/timing-function-threading-check.html
Patch Details
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.
Background
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.
Analysis
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.
Two concurrent deref() operations can read the same count value and each decrement to the same result, losing one decrement so the object leaks; in the inverse interleaving, a lost increment lets the count hit zero while another thread still holds a logical reference, freeing the TimingFunction while it is being used (transformProgress is const but is invoked on a possibly-freed object). The debug RefCounted threading-check assertion fires precisely because deref/ref happen off the owning thread — which is what the ASan-detected test crash surfaced.
This vulnerability weakens memory safety within the WebContent process by violating the implicit invariant that a TimingFunction's lifetime is governed by a consistent reference count. Before the fix, an object shared between the main thread and the scrolling thread could have its refcount corrupted, so the assumption "a live, referenced object is not freed" could be broken under concurrent access. If an attacker reliably wins the race so the count prematurely reaches zero, they could obtain a use-after-free on a heap object reachable from web content — a building block toward renderer memory corruption.
This is a recurring WebKit pattern: a type originally designed as single-threaded (RefCounted) gets adopted by a later-added cross-thread subsystem (threaded animations) without auditing whether all of its dependencies are thread-safe. AcceleratedEffect/AcceleratedEffectValues already used ThreadSafeRefCounted, but the leaf dependency TimingFunction was missed — the fix is essentially "make the whole reachable object graph thread-safe."
Note: The transient-ref behavior of KeyframeInterpolation.cpp and ScrollAnimationSmooth.cpp, the existing thread-safe status of the parent effect classes, and the macOS-specific scrolling-thread interleaving are inferred from the caller context and test commentary rather than the diff itself. The non-atomic-refcount root cause and the fix are directly supported by the patch.
Audit directions
- An object graph shared across threads must have EVERY ref-counted node use
ThreadSafeRefCounted, not just the root. Audit all members reachable fromAcceleratedEffectandAcceleratedEffectValuesfor any type still deriving from plainRefCounted. Start by grepping theplatform/animationand animation directories for: public RefCounted<and cross-referencing against types stored by the accelerated-effect classes. - Leaf types adopted by a newer cross-thread subsystem without a thread-safety audit. Review which WebCore platform types are touched on the scrolling thread (e.g. via
ScrollAnimationSmooth, scroll-driven animation timelines) and confirm each isThreadSafeRefCounted. Trace callers oftransformProgress/resolvethat execute off the main thread. - Verify whether other style/animation value types passed into the threaded pipeline (timing functions, spring parameters, custom easing point vectors) are immutable and thread-safe once handed off. Grep for
RefCounted<among types referenced byAcceleratedEffectValuesto find further single-threaded leaves shared across threads.