[1] Web Animations null dereference via CustomEffect callback
Severity: Medium | Component: WebCore Web Animations | 106edd0
Rated Medium because the observable effect is a reliable null-pointer dereference crashing the renderer process from any web content, but escalation beyond denial of service is blocked by null-page protection on all shipping WebKit platforms — no memory corruption primitive is reachable without a separate vulnerability to map the null page.
The callback for a CustomEffect, which is called under CustomEffect::animationDidTick(), can reset the associated animation's effect to null. The fix ensures that the subsequent call to AnimationEffect::animationBecameReady() is performed on a valid effect. Since the only concrete implementation of animationBecameReady() was for KeyframeEffect, the method is moved from a virtual function on the superclass to a non-virtual function on KeyframeEffect.
Source/WebCore/animation/WebAnimation.cpp
Source/WebCore/animation/AnimationEffect.h
LayoutTests/webanimations/custom-effect/custom-effect-set-to-null-in-callback.html
Patch Details
Missing null recheck of a member pointer across a JavaScript re-entrancy boundary in animation tick processing.
The patch modifies WebAnimation::tick() in WebAnimation.cpp. Before the fix, after calling m_effect->animationDidTick(), the code called m_effect->animationBecameReady() directly — a virtual dispatch on the same m_effect pointer that was checked at the top of the block. The fix replaces this with a fresh call to this->keyframeEffect(), which returns a RefPtr<KeyframeEffect> obtained by dynamically casting m_effect at the point of use. The result is null-checked before calling animationBecameReady().
The second part of the patch removes animationBecameReady() from AnimationEffect (the base class) entirely and moves it to KeyframeEffect as a non-virtual method. This eliminates the virtual dispatch path that made the null dereference crash through a vtable load.
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
The Web Animations API allows creating animations driven by JavaScript callbacks via CustomEffect. When document.timeline.animate(callback, duration) is called, a CustomEffect is created whose callback runs on each animation tick. Unlike KeyframeEffect (which interpolates CSS properties declaratively), CustomEffect invokes an arbitrary JS function on every frame.
During each animation frame, DocumentTimeline ticks all active animations by calling WebAnimation::tick(). This method calls m_effect->animationDidTick() on the animation's effect. For a CustomEffect, this invocation runs the JS callback — a re-entrancy boundary where native code calls into JavaScript, which may synchronously re-enter and mutate object state before returning.
m_effect in WebAnimation holds the animation's effect via RefPtr<AnimationEffect>, a reference-counted smart pointer used in WebKit to manage object lifetime. When the last RefPtr to an object is cleared, the object is destroyed. Setting animation.effect = null from JavaScript flows through WebAnimation::setEffect(nullptr), which clears the m_effect RefPtr.
Virtual dispatch refers to calling a method via a vtable pointer lookup — the CPU loads a pointer from the object's first word in memory, indexes into the vtable, and calls the function pointer found there. When the object pointer is null, the vtable load dereferences address zero.
Analysis
The root cause is a re-entrancy hazard in WebAnimation::tick(). Before the fix, the method checked m_effect for null, called m_effect->animationDidTick(), and then unconditionally called m_effect->animationBecameReady() using the same pointer. For a CustomEffect, animationDidTick() invokes the JS callback — and the test case demonstrates that this callback can execute animation.effect = null, which flows through setEffect(nullptr) and clears m_effect (a RefPtr<AnimationEffect>, if the usage patterns in WebAnimation.cpp are representative of the actual declaration). After animationDidTick() returns, m_effect is null. The subsequent call to m_effect->animationBecameReady() — a virtual dispatch — loads the vtable pointer from address zero and faults.