[3] SVGProperty animVal detach after stopAnimation
The destructor cleaned up the back-pointer. The reset paths forgot.
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
LayoutTests/svg/animations/animVal-detach-after-stopAnimation-crash.html
Patch Details
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를 끊지 않은 패턴.
Background
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()를 통해 이 전환을 동기적으로 유발할 수 있습니다.
Analysis
이 취약점은 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 내부의 메모리 안전성을 약화시킵니다. 보안 모델은 SVGProperty의 m_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에서 직접 확인됩니다.
Audit directions
- 독립적인 reference가 pointee를 살려두는 동안, 그 target이 해제될 수 있는 raw back-pointer. SVG property tree 안에서
m_animVal/m_baseVal계열 RefPtr에nullptr를 대입(또는 덮어쓰기)하는 모든 지점을 점검하고, 각 지점 앞에detach()가 선행되는지 확인해야 합니다.Source/WebCore/svg/properties/에서m_animVal =와= nullptr를 검색해 각 지점을 해당 class의 소멸자와 비교하십시오. 소멸자는detach()를 호출하지만 reset 경로는 그렇지 않은 class가 후보입니다. - 모든 teardown 경로에서 소멸자와 동일하게 반영되어야 하는 수동 back-pointer 무효화.
SVGProperty::detach()/m_owner해제가~SVGAnimated*Property뿐 아니라 owner가 해제되는 모든 경로에서 호출되는지 확인해야 합니다.SVGProperty::contextElement()와 그 호출자들(예:SVGLength::valueForBindings)을 살펴보고 bindings에서 도달 가능한 어떤 읽기가m_owner를 역참조하는지 정리한 뒤, wrapperRef가 유지되는 동안 owner가 죽을 수 있는 모든 지점을 역추적해야 합니다. - C++ owner보다 오래 살아남는, 독립적인
Ref를 보유한 bindings wrapper. JS에 노출되는 하위 객체가 부모 컨테이너를 가리키는 raw back-pointer를 저장하는 다른 WebCore 객체 계열을 점검하고, parent teardown이 child를 detach하는지 확인해야 합니다. 동일한 owner/animVal ownership 구조를 따르는 SVG list item property(SVGListProperty/SVGPathSegListitem)부터 시작하는 것이 적절합니다. instanceStartAnimationImpl이 공유/<use>-instance 애니메이션에서m_animVal을 덮어쓸 때, script가 보유한 animVal을 detach 없이 버릴 수 있었는지 점검할 필요가 있습니다. 패치가 이 지점에도detachAnimVal()을 추가했다는 사실은, instance-animation 경로가 별도의 trigger가 될 수 있음을 시사하며,<use>로 복제된 tree를 대상으로 별도로 검증해볼 가치가 있습니다.