← All issues

[3] SVGProperty animVal detach after stopAnimation

The destructor cleaned up the back-pointer. The reset paths forgot.

Severity: High | Component: WebCore SVG property model | eb617fc

Severity를 High로 평가한 이유는, script가 animVal SVGProperty에 대한 Ref를 소유 SVGAnimated*Property가 파괴된 이후까지 들고 있을 수 있고, 이후 len.value 읽기가 freed 메모리를 통해 dangling된 m_owner back-pointer를 역참조하기 때문입니다. Controlled read로의 확장은 freed된 owner slot을 heap grooming을 통해 재확보해야 하는데, 이는 diff에서 직접 보여주지 않습니다. 다만 dangling read 자체는 안정적으로 도달 가능합니다.

SVGAnimatedValueProperty<T>::ensureAnimVal()은 animVal SVGProperty를 생성하면서 this를 raw SVGProperty::m_owner back-pointer로 설정합니다. 소멸자는 detach()를 통해 이를 해제하지만, stopAnimation()instanceStopAnimationImpl()은 detach 없이 m_animVal = nullptr만 수행합니다. animVal은 SVGAnimatedLength.animVal을 통해 script에 노출되고 JS wrapper가 독립적인 Ref를 보유하기 때문에, SVGAnimatedValueProperty보다 더 오래 살아남을 수 있고 결과적으로 m_owner가 dangling 상태가 됩니다. 이후 len.value 읽기가 freed 메모리를 거쳐 SVGProperty::contextElement()에 도달합니다. SVGAnimatedPropertyList<T>에도 동일한 패턴이 존재하며, 소멸자의 동작을 그대로 반영하는 detachAnimVal() helper를 통해 같은 방식으로 수정되었습니다.

Source/WebCore/svg/properties/SVGAnimatedValueProperty.h

void stopAnimation(SVGAttributeAnimator& animator) override
{
Base::stopAnimation(animator);
if (!this->isAnimating())
- m_animVal = nullptr;
+ detachAnimVal();
else if (m_animVal)
m_animVal->setValue(m_baseVal->value());
}
...
void instanceStartAnimationImpl(...) override
{
- if (!this->isAnimating())
+ if (!this->isAnimating()) {
+ detachAnimVal();
m_animVal = animated.animVal();
+ }
Base::startAnimation(animator);
}
...
void instanceStopAnimationImpl(SVGAttributeAnimator& animator) override
{
Base::stopAnimation(animator);
if (!this->isAnimating())
- m_animVal = nullptr;
+ detachAnimVal();
}
+ void detachAnimVal()
+ {
+ // m_animVal may be retained by the bindings after we drop it. Detach it now so its
+ // raw SVGProperty::m_owner back-pointer cannot dangle once |this| is destroyed.
+ if (RefPtr animVal = std::exchange(m_animVal, nullptr))
+ animVal->detach();
+ }

LayoutTests/svg/animations/animVal-detach-after-stopAnimation-crash.html

+function step1() {
+ s.pauseAnimations(); s.setCurrentTime(0.5); // enter active interval
+ len = grab(); // r.x.animVal, held by JS
+ a.remove(); r.remove(); s.remove(); // reaches stopAnimation() -> m_animVal=nullptr branch
+}
+function step2() {
+ GCController.collect();
+ len.value; // SVGProperty::contextElement() through freed m_owner
+}

SVGAnimatedValueProperty<T>SVGAnimatedPropertyList<T> 양쪽에 detachAnimVal() helper가 추가되었습니다. 이 helper는 std::exchange(m_animVal, nullptr)를 수행하고, 값이 존재했다면 animVal->detach()를 호출하여 소멸자가 기존에 하던 동작을 그대로 반영합니다. 이전에 m_animVal = nullptr를 직접 기록하던 세 곳의 호출 지점(stopAnimation()instanceStopAnimationImpl()의 non-animating 분기)은 이제 detachAnimVal()을 대신 호출합니다. 이에 더해 instanceStartAnimationImpl()m_animVal = animated.animVal()로 덮어쓰기 전에 detachAnimVal()을 호출하도록 변경되어, 기존에 보유하던 animVal이 조용히 버려지지 않고 detach된 뒤 교체됩니다.

독립적인 reference가 존재해 pointee가 back-pointer의 target보다 오래 살아남을 수 있는 상황에서, pointee를 소유하는 smart-pointer를 버리기 전에 raw back-pointer를 끊지 않은 패턴.

SVG animated attribute는 두 개의 SVGProperty 객체를 보유하는 template화된 SVGAnimated*Property 컨테이너로 모델링됩니다. 하나는 정적 author 값을 담는 m_baseVal(Ref)이고, 다른 하나는 SMIL/<animate> 애니메이션이 활성화된 동안의 presentation 값을 담는 m_animVal(RefPtr)입니다. 각 SVGProperty는 자신을 소유하는 SVGAnimated*Property를 가리키는 raw m_owner back-pointer를 가지고 있으며, 이는 SVGProperty::contextElement()SVGLength::valueForBindings와 같은 값 변환에서 소유 element를 찾는 데 사용됩니다. m_owner를 해제하는 동작이 바로 detach()입니다. script가 element.x.animVal을 읽으면 m_animVal SVGProperty를 감싸는 JS wrapper가 생성되고 자체적으로 Ref를 보유하게 되므로, 해당 property의 lifetime은 owner 혼자만으로 제어되지 않습니다. SMIL lifecycle에서는 active interval에 진입할 때 startAnimation()이 호출되어 m_animVal이 채워지고, interval이 종료되면 stopAnimation() / instanceStopAnimationImpl()이 호출되어 non-animating 분기에서 m_animVal을 해제합니다. script는 pauseAnimations(), setCurrentTime(), node remove()를 통해 이 전환을 동기적으로 유발할 수 있습니다.

이 취약점은 lifetime/ownership 위반에서 비롯된 use-after-free입니다. ensureAnimVal()은 animVal을 생성하면서 this를 raw m_owner로 설정합니다. 소유 컨테이너는 RefPtr m_animVal을 통해 animVal을 보유하며, 소멸자는 this가 죽기 전에 m_animVal->detach()를 호출해 back-pointer를 끊습니다. 그런데 stopAnimation()instanceStopAnimationImpl()은 detach 없이 m_animVal = nullptr로 reference만 해제했습니다. animVal이 script에 노출되고 JS wrapper가 같은 SVGProperty에 대해 독립적인 Ref를 보유하고 있기 때문에, owner의 RefPtr을 버려도 property 자체는 파괴되지 않습니다. 대신 이미 stale해진 m_owner가 여전히 컨테이너를 가리킨 채로 살아남습니다. 이후 컨테이너와 그 소유 element가 파괴되면, m_owner는 dangling 상태가 됩니다.

회귀 테스트는 이 과정을 정확하게 재현합니다. 먼저 element의 x<animate>를 생성하고 pauseAnimations()setCurrentTime(0.5)를 호출해 active interval에 진입시켜 m_animVal이 존재하도록 만듭니다. 그다음 r.x.animVal을 JS 변수로 가져와(wrapper의 독립적인 Ref를 통해 SVGProperty를 고정), animate/rect/svg를 remove()하여 stopAnimation()!isAnimating() 분기를 타면서 detach 없이 owner의 RefPtr을 해제하도록 만듭니다. 소유 컨테이너와 element가 해제된 뒤 len.value를 읽으면, dangling된 m_owner를 통해 SVGProperty::contextElement()로 흐름이 이어집니다. 여기서 직접 관찰되는 영향은 web content에서 안정적으로 유발 가능한 dangling-pointer read입니다. 이를 무기화하려면, 공격자가 free와 .value 읽기 사이에 freed된 owner allocation을 조작된 데이터로 재확보해야 합니다. 그러면 m_owner 역참조와 이어지는 contextElement() pointer chain이 공격자가 구성한 메모리 위에서 동작하게 됩니다.

이 취약점은 WebContent renderer 내부의 메모리 안전성을 약화시킵니다. 보안 모델은 SVGPropertym_owner가 역참조되는 모든 시점에 유효하다는 전제를 두고 있습니다. 즉 owner가 도달 가능한 모든 사용보다 오래 살아있거나, owner가 파괴되기 전에 pointer가 detach되어야 합니다. 패치 이전에는 script가 owner의 소멸 이후까지 animVal에 대한 Ref를 보유할 수 있었습니다. 여기서 얻어지는 primitive는 renderer sandbox 내부에 국한되며, 별도의 sandbox escape가 필요합니다.

동일한 결함이 두 개의 형제 template class에 그대로 나타나며 동일한 방식으로 수정되었다는 점은, SVG property 계열 전반에 걸쳐 teardown idiom이 복사-붙여넣기 된 정황을 강하게 시사합니다. 구조적인 문제는 SVGProperty가 무효화를 수동으로 처리해야 하는 raw back-pointer를 사용한다는 데 있습니다. 소멸자만이 이를 일관되게 수행했고, animation stop/restart 경로는 각자 m_animVal = nullptr를 직접 작성하는 방식으로 개별 구현되어 있었습니다.

Note: wrapper가 독립적인 Ref를 보유한다는 점과 정확한 contextElement()/valueForBindings 역참조 chain은 DOM 모델과 테스트로부터 추론된 것으로, 제공된 소스에 직접 드러나 있지 않습니다. 반면 reset 경로에서 detach가 누락된 부분은 diff에서 직접 확인됩니다.