← All issues

[1] Web Animations null dereference via CustomEffect callback

Severity: Medium | Component: WebCore Web Animations | 106edd0

Medium으로 분류한 이유는 다음과 같습니다. 모든 웹 콘텐츠에서 renderer process를 항상 crash시키는 null-pointer dereference가 안정적으로 재현 가능하지만, denial of service 이상의 피해 확대는 모든 WebKit 출시 플랫폼의 null-page protection에 의해 차단됩니다. null page를 매핑하는 별도의 취약점 없이는 memory corruption primitive에 도달할 수 없습니다.

CustomEffect의 callback은 CustomEffect::animationDidTick() 호출 중에 실행되며, 연결된 animation의 effect를 null로 초기화할 수 있습니다. 이 수정은 이후에 AnimationEffect::animationBecameReady()가 유효한 effect를 대상으로 호출되도록 보장합니다. animationBecameReady()의 구체적인 구현은 KeyframeEffect에만 존재했기 때문에, 해당 메서드는 상위 클래스의 virtual 함수에서 KeyframeEffect의 non-virtual 함수로 이동되었습니다.

Source/WebCore/animation/WebAnimation.cpp

if (!isEffectInvalidationSuspended() && m_effect) {
m_effect->animationDidTick();
- if (wasPending && !pending())
- m_effect->animationBecameReady();
+ if (RefPtr keyframeEffect = this->keyframeEffect()) {
+ if (wasPending && !pending())
+ keyframeEffect->animationBecameReady();
+ }
}

Source/WebCore/animation/AnimationEffect.h

virtual void animationDidTick() { };
- virtual void animationBecameReady() { };
virtual void animationDidChangeTimingProperties() { };

LayoutTests/webanimations/custom-effect/custom-effect-set-to-null-in-callback.html

+ promise_test(async t => {
+ const animation = document.timeline.animate(progress => animation.effect = null, 1000);
+ await waitForAnimationFrames(1);
+ }, "Setting an animation's effect to null in a CustomEffect callback.");

Animation tick 처리 과정에서 JavaScript re-entrancy 경계를 넘은 뒤 멤버 포인터 재검증이 누락된 패턴.

이 patch는 WebAnimation.cppWebAnimation::tick()을 수정합니다. 수정 전에는 m_effect->animationDidTick()을 호출한 뒤, 블록 상단에서 null 검사를 마친 동일한 m_effect pointer에 대해 m_effect->animationBecameReady()를 직접 virtual dispatch 방식으로 호출했습니다. 수정 후에는 이 부분이 this->keyframeEffect() 호출로 대체되었습니다. 이 함수는 사용 시점에 m_effect를 dynamic cast하여 RefPtr<KeyframeEffect>를 반환하며, animationBecameReady() 호출 전에 null 검사가 수행됩니다.

patch의 두 번째 부분에서는 animationBecameReady()를 base 클래스인 AnimationEffect에서 완전히 제거하고, KeyframeEffect의 non-virtual 메서드로 이동시켰습니다. 이를 통해 vtable load를 통한 null dereference crash를 유발하는 virtual dispatch 경로 자체가 제거됩니다.

Before:                                    After:
tick()                                     tick()
  ├─ null-check m_effect                     ├─ null-check m_effect
  ├─ m_effect->animationDidTick()            ├─ m_effect->animationDidTick()
  │     └─► JS callback (re-entrancy)        │     └─► JS callback (re-entrancy)
  │           animation.effect = null         │           animation.effect = null
  │           m_effect ← nullptr              │           m_effect ← nullptr
  └─ m_effect->animationBecameReady()        ├─ keyframeEffect = this->keyframeEffect()
        └─► vtable load from nullptr         ├─ null-check keyframeEffect
              └─► CRASH                      └─ keyframeEffect->animationBecameReady()
                                                   └─► safe (non-virtual, valid ptr)

Web Animations API는 CustomEffect를 통해 JavaScript callback으로 동작하는 animation을 생성할 수 있습니다. document.timeline.animate(callback, duration)을 호출하면 CustomEffect가 생성되며, 등록된 callback은 매 animation tick마다 실행됩니다. CSS 속성을 선언적으로 보간하는 KeyframeEffect와 달리, CustomEffect는 매 프레임마다 임의의 JS 함수를 호출합니다.

매 animation 프레임마다 DocumentTimelineWebAnimation::tick()을 호출하여 모든 활성 animation을 tick합니다. 이 메서드는 animation의 effect에 대해 m_effect->animationDidTick()을 호출합니다. CustomEffect의 경우, 이 호출이 JS callback을 실행하게 됩니다. 즉, native 코드가 JavaScript로 진입하는 re-entrancy 경계가 형성되며, 이 JS는 반환 전에 동기적으로 객체 상태를 변경할 수 있습니다.

WebAnimationm_effectRefPtr<AnimationEffect>로 animation의 effect를 보관합니다. RefPtr은 WebKit에서 객체 생명 주기를 관리하는 reference-counted smart pointer입니다. 객체에 대한 마지막 RefPtr이 해제되면 객체가 소멸됩니다. JavaScript에서 animation.effect = null을 설정하면 WebAnimation::setEffect(nullptr)를 거쳐 m_effect RefPtr이 초기화됩니다.

Virtual dispatch는 vtable pointer 조회를 통해 메서드를 호출하는 방식입니다. CPU가 메모리 내 객체의 첫 번째 워드에서 pointer를 로드하고, vtable을 인덱싱하여 해당 function pointer를 호출합니다. 객체 pointer가 null이면 vtable 로드 시 주소 0을 역참조하게 됩니다.

근본 원인은 WebAnimation::tick()의 re-entrancy 위험입니다. 수정 전 이 메서드는 m_effect의 null 여부를 확인한 뒤 m_effect->animationDidTick()을 호출하고, 이후 동일한 pointer로 m_effect->animationBecameReady()를 무조건적으로 호출했습니다.

CustomEffect의 경우 animationDidTick()이 JS callback을 호출합니다. 테스트 케이스에서 확인된 것처럼, 이 callback은 animation.effect = null을 실행할 수 있으며, setEffect(nullptr)를 거쳐 m_effect가 초기화됩니다. m_effectRefPtr<AnimationEffect>로 선언되어 있다고 가정할 수 있습니다. WebAnimation.cpp의 사용 패턴으로 볼 때 합리적인 추정이지만, diff에서 직접 확인되는 사항은 아닙니다. animationDidTick()이 반환된 뒤에는 m_effect가 null 상태입니다. 이후 m_effect->animationBecameReady() — virtual dispatch — 를 호출하면 주소 0에서 vtable pointer를 로드하려다 fault가 발생합니다.

🔒

Explores the ownership model and re-entrancy dynamics behind this crash, with a detailed feasibility assessment

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

🔒

Multiple re-entrancy audit patterns identified, applicable across several WebCore animation and observer subsystems

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