← All issues

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

Severity: Medium | Component: WebCore platform animation | c541bf0

Medium으로 평가합니다. diff는 두 스레드에서 실제로 접근된다는 것이 확인된 reference count를 atomic으로 전환하여, logical reference가 살아있는 상태에서 count가 0으로 떨어지는 data race를 차단합니다. 다만 활용 가능한 UAF에 도달하려면 renderer에서 접근 가능한 heap object를 대상으로 race를 안정적으로 획득해야 합니다. 공격자가 제어 가능한 상태의 폭이 좁아, 즉각적인 결과는 확인된 corruption primitive가 아닌 crash에 머뭅니다.

macOS에서 accelerated effect는 main thread와 scrolling thread 양쪽에서 접근될 수 있습니다. 따라서 TimingFunctionAcceleratedEffectAcceleratedEffectValues에서 사용하는 다른 ref-counted 타입들처럼 ThreadSafeRefCounted를 사용해야 합니다. 이 crash는 기존 threaded-animations layout test를 ASan 하에서 실행하는 과정에서 드러났습니다. commit에는 fix가 bug 분석 중 LLM의 제안으로 도출되었고, 작성자가 이를 검증했다는 내용이 기록되어 있습니다.

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)));
+ // 두 경로 모두 AnimationEffectTiming::resolve()를 호출하며, 이를 통해 effect의
+ // TimingFunction이 보호됩니다. RefCounted threading check가 발생해서는 안 됩니다.
+ for (let i = 0; i < 50; ++i)
+ await UIHelper.remoteAnimationStackForElement(target);

이 patch는 TimingFunction의 base class를 RefCounted<TimingFunction>에서 ThreadSafeRefCounted<TimingFunction>으로 변경하며, include도 그에 맞게 교체되었습니다. transformProgress, clone, 그리고 서브클래스(LinearTimingFunction, CubicBezierTimingFunction, StepsTimingFunction, SpringTimingFunction)의 로직은 변경되지 않았습니다. 추가된 테스트는 cubic-bezier easing을 사용하는 accelerated animation 4개를 생성하고, scrolling thread가 effect를 동시에 적용하는 동안 main thread에서 animation stack을 반복적으로 resolve합니다. RefCounted threading check가 트리거되지 않는지를 검증하는 방식입니다.

스레드 간 공유 객체에서 non-atomic reference counting이 사용되어, data race로 인한 refcount 손상이 가능한 패턴.

RefCounted<T>는 WTF의 단일 스레드용 reference-counted base 클래스입니다. ref()/deref()는 count를 non-atomic하게 변경하며, assertion이 활성화된 빌드에서는 소유 스레드가 아닌 다른 스레드에서 count에 접근할 경우 즉시 trap하는 thread-ownership check를 포함합니다. ThreadSafeRefCounted<T>는 그 atomic 대응 버전으로, count에 대해 atomic read-modify-write를 사용하므로 여러 스레드에서 동시에 ref/deref를 수행해도 안전합니다.

macOS에서 threaded(accelerated) animation은 animation effect 데이터의 복사본을 scrolling thread에서 실행합니다. scroll-driven 및 time-driven animation을 main thread 없이도 resolve할 수 있도록 하기 위해서입니다. AcceleratedEffectAcceleratedEffectValues는 effect의 TimingFunction을 보유합니다. TimingFunction::transformProgress()는 linear progress 값을 easing curve(예: cubic-bezier)를 통해 매핑하는 함수입니다. main thread의 keyframe interpolation 경로와 scrolling thread의 scroll-animation 경로 양쪽에서 호출되며, 각 호출 측은 호출 기간 동안 공유된 TimingFunction에 대해 transient ref를 획득합니다.

non-thread-safe reference count에서 발생하는 data race로, use-after-free 또는 double-free로 이어질 수 있습니다.

패치 이전에는 TimingFunctionRefCounted를 상속하고 있었습니다. 이 클래스의 ref()/deref()는 단순한 non-atomic increment/decrement를 수행하며, thread-ownership assertion을 내장합니다. macOS에서 accelerated animation은 동일한 TimingFunction 인스턴스를 두 스레드에서 동시에 접근 가능한 상태로 만듭니다. main thread는 AnimationEffectTiming::resolve()KeyframeInterpolation을 통해 timing을 resolve하고, scrolling thread는 ScrollAnimationSmooth를 통해 effect를 적용합니다. 두 경로 모두 transformProgress를 호출하기 전에 transient RefPtr/protect() ref를 획득합니다. 이 transient ref를 두 스레드가 동시에 획득하고 해제하면, non-atomic read-modify-write에서 race가 발생합니다.

🔒

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.

더 확인하려면 구독해 주세요

🔒

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

더 확인하려면 구독해 주세요