[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
Source/WebCore/animation/AnimationEffect.h
LayoutTests/webanimations/custom-effect/custom-effect-set-to-null-in-callback.html
Patch Details
Animation tick 처리 과정에서 JavaScript re-entrancy 경계를 넘은 뒤 멤버 포인터 재검증이 누락된 패턴.
이 patch는 WebAnimation.cpp의 WebAnimation::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)
Background
Web Animations API는 CustomEffect를 통해 JavaScript callback으로 동작하는 animation을 생성할 수 있습니다. document.timeline.animate(callback, duration)을 호출하면 CustomEffect가 생성되며, 등록된 callback은 매 animation tick마다 실행됩니다. CSS 속성을 선언적으로 보간하는 KeyframeEffect와 달리, CustomEffect는 매 프레임마다 임의의 JS 함수를 호출합니다.
매 animation 프레임마다 DocumentTimeline은 WebAnimation::tick()을 호출하여 모든 활성 animation을 tick합니다. 이 메서드는 animation의 effect에 대해 m_effect->animationDidTick()을 호출합니다. CustomEffect의 경우, 이 호출이 JS callback을 실행하게 됩니다. 즉, native 코드가 JavaScript로 진입하는 re-entrancy 경계가 형성되며, 이 JS는 반환 전에 동기적으로 객체 상태를 변경할 수 있습니다.
WebAnimation의 m_effect는 RefPtr<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을 역참조하게 됩니다.
Analysis
근본 원인은 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_effect가 RefPtr<AnimationEffect>로 선언되어 있다고 가정할 수 있습니다. WebAnimation.cpp의 사용 패턴으로 볼 때 합리적인 추정이지만, diff에서 직접 확인되는 사항은 아닙니다. animationDidTick()이 반환된 뒤에는 m_effect가 null 상태입니다. 이후 m_effect->animationBecameReady() — virtual dispatch — 를 호출하면 주소 0에서 vtable pointer를 로드하려다 fault가 발생합니다.