[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.
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 양쪽에서 접근될 수 있습니다. 따라서 TimingFunction도 AcceleratedEffect와 AcceleratedEffectValues에서 사용하는 다른 ref-counted 타입들처럼 ThreadSafeRefCounted를 사용해야 합니다. 이 crash는 기존 threaded-animations layout test를 ASan 하에서 실행하는 과정에서 드러났습니다. commit에는 fix가 bug 분석 중 LLM의 제안으로 도출되었고, 작성자가 이를 검증했다는 내용이 기록되어 있습니다.
Source/WebCore/platform/animation/TimingFunction.h
LayoutTests/webanimations/threaded-animations/timing-function-threading-check.html
Patch Details
이 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 손상이 가능한 패턴.
Background
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할 수 있도록 하기 위해서입니다. AcceleratedEffect와 AcceleratedEffectValues는 effect의 TimingFunction을 보유합니다. TimingFunction::transformProgress()는 linear progress 값을 easing curve(예: cubic-bezier)를 통해 매핑하는 함수입니다. main thread의 keyframe interpolation 경로와 scrolling thread의 scroll-animation 경로 양쪽에서 호출되며, 각 호출 측은 호출 기간 동안 공유된 TimingFunction에 대해 transient ref를 획득합니다.
Analysis
non-thread-safe reference count에서 발생하는 data race로, use-after-free 또는 double-free로 이어질 수 있습니다.
패치 이전에는 TimingFunction이 RefCounted를 상속하고 있었습니다. 이 클래스의 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가 발생합니다.
두 개의 deref() 호출이 동시에 실행되면 같은 count 값을 읽고 각각 동일한 결과로 감소시켜, decrement 하나를 잃게 되므로 객체가 누수됩니다. 반대 순서로 interleaving이 발생하면, increment 하나를 잃어 다른 스레드가 아직 logical reference를 보유한 상태에서 count가 0에 도달합니다. 이때 TimingFunction이 사용 중에 해제됩니다. transformProgress는 const이지만, 이미 해제되었을 수 있는 객체 위에서 호출되는 셈입니다. debug 빌드에서 RefCounted threading-check assertion이 발생하는 이유도 바로 이 때문입니다. deref/ref가 소유 스레드 밖에서 수행되기 때문으로, ASan으로 감지된 테스트 crash를 통해 드러난 사실이기도 합니다.
TimingFunction의 lifetime이 일관된 reference count에 의해 관리된다는 묵시적 invariant를 위반함으로써, WebContent process 내부의 memory safety가 약화됩니다. 패치 이전에는 main thread와 scrolling thread 간에 공유된 객체의 refcount가 손상될 가능성이 있었습니다. 그 결과 동시 접근 상황에서 "살아있고 참조된 객체는 해제되지 않는다"는 전제가 깨질 수 있었습니다. 공격자가 race를 안정적으로 획득하여 count가 조기에 0에 도달하게 만들 수 있다면, web content에서 접근 가능한 heap object에 대한 use-after-free를 획득할 가능성이 있습니다. renderer memory corruption을 향한 공격 기반으로 활용될 수 있는 primitive입니다.
이번 사례는 WebKit에서 반복적으로 나타나는 패턴에 해당합니다. 원래 단일 스레드용으로 설계된 타입(RefCounted)이 나중에 추가된 cross-thread subsystem(threaded animation)에 채택될 때, 의존 타입 전체가 thread-safe한지 점검하지 않는 경우입니다. AcceleratedEffect와 AcceleratedEffectValues는 이미 ThreadSafeRefCounted를 사용하고 있었지만, leaf 의존 타입인 TimingFunction이 누락되었습니다. 이번 fix의 본질은 "도달 가능한 객체 그래프 전체를 thread-safe하게 만드는 것"입니다.
Note: KeyframeInterpolation.cpp와 ScrollAnimationSmooth.cpp의 transient-ref 동작, 부모 effect 클래스의 기존 thread-safe 상태, macOS에 특화된 scrolling thread interleaving은 diff 자체가 아닌 caller context와 테스트 주석을 통해 추론된 내용입니다. non-atomic refcount가 root cause라는 점과 fix 내용은 patch에서 직접 확인됩니다.
Audit directions
- 스레드 간 공유되는 객체 그래프에서는 root뿐 아니라 ref-counted 노드 전체가
ThreadSafeRefCounted를 사용해야 합니다.AcceleratedEffect와AcceleratedEffectValues에서 도달 가능한 모든 멤버를 점검하여, 여전히 plainRefCounted를 상속하는 타입이 없는지 확인해야 합니다.platform/animation및 animation 디렉터리에서: public RefCounted<를 검색한 뒤, accelerated-effect 클래스가 보유한 타입과 대조하는 방식으로 시작할 수 있습니다. - thread-safety 점검 없이 신규 cross-thread subsystem에 채택된 leaf 타입. scrolling thread에서 접근되는 WebCore platform 타입(예:
ScrollAnimationSmooth, scroll-driven animation timeline 경유)을 검토하고, 각 타입이ThreadSafeRefCounted를 사용하는지 확인해야 합니다. main thread 밖에서 실행되는transformProgress/resolve호출 지점을 추적하는 것이 유효한 접근입니다. - threaded pipeline에 전달되는 다른 style/animation 값 타입(timing function, spring parameter, custom easing point vector 등)이 전달 이후 immutable하고 thread-safe한지 검증해야 합니다.
AcceleratedEffectValues에서 참조하는 타입 중RefCounted<를 상속하는 것을 검색하면, 스레드 간 공유되는 추가적인 single-threaded leaf를 발견할 수 있습니다.