NEW322975
animVal of a list attribute keeps reporting the old items after the content attribute changes, if it was read once before the change
https://bugs.webkit.org/show_bug.cgi?id=322975
Summary animVal of a list attribute keeps reporting the old items after the content a...
Karl Dubost
Reported 2026-08-31 01:57:03 PDT
Read animVal on a list attribute, then change the attribute. animVal keeps reporting the item count and the values from before the change, forever, while baseVal reports the new ones. Read animVal for the first time after the change and it is correct, so the object is a snapshot taken at first access rather than a wrong computation. No animation is involved anywhere in this, so it is not the SMIL question. Firefox 156 and Chrome 154 both report the new values, on all four list interfaces. ### Testcase <svg xmlns="http://www.w3.org/2000/svg"> <text id="t" rotate="10 20 30" x="5" y="20">abc</text> </svg> t.rotate.animVal.numberOfItems; // 3, and this read is what makes the bug appear t.setAttribute("rotate", "40 50"); t.rotate.baseVal.numberOfItems; // 2, correct t.rotate.animVal.numberOfItems; // 2 per spec. Safari says 3. t.rotate.animVal.getItem(0).value; // 40 per spec. Safari says 10. Delete the first line and every later line is correct, which is the diagnosis: the animVal list is built once, on first access, and nothing refreshes it when the attribute is parsed again. All four list interfaces are affected, on nine attributes: rotate, tableValues, kernelMatrix, values, x, y, dx, dy, points, transform, gradientTransform, patternTransform. polyline.animatedPoints has it too, since it is the same animVal. A DOM change to baseVal does NOT show the bug: t.rotate.baseVal.removeItem(2) leaves animVal correct. Only the attribute path is affected. ### Measured, 3 engines, 2026-08-29 Probe attached, group A. All 7 controls pass in all three runs, no errors. Firefox 156.0 fails nothing in the whole file, 144 of 144. Safari 27.0 Firefox 156.0 Chrome 154 animVal read BEFORE the change reports the new item count afterwards no YES YES ... and the new value at index 0 no YES YES animVal read AFTER the change is correct YES YES YES animVal list object is stable across the change YES YES YES animVal items are read only YES YES YES animVal follows a baseVal DOM change YES YES YES All four lists, same pattern. Safari's detail on the count row is "before=3 after=3 baseVal=2": animVal and baseVal disagree about how many items the element has. Safari is alone. The two rows that pass everywhere are the diagnosis: read animVal after the change and it is right, so it is a snapshot that is never refreshed rather than a wrong computation, and the DOM path already refreshes it. ### Spec Both readings of animVal give the same answer here, because nothing is animating. SVG 2, https://w3c.github.io/svgwg/svg2-draft/types.html#InterfaceSVGAnimatedNumberList "The baseVal and animVal IDL attributes represent the current non-animated value of the reflected attribute. On getting baseVal or animVal, an SVGNumberList object is returned that reflects the base value of the reflected attribute." and https://w3c.github.io/svgwg/svg2-draft/types.html#DOMInterfacesForReflectingSVGAttributes "In SVG 1.1, the animVal attribute of the SVG DOM interfaces represented the current animated value of the reflected attribute. In this version of SVG, animVal no longer represents the current animated value and is instead an alias of baseVal." SVG 1.1 said animVal holds the animated value while animating and otherwise the same value as baseVal. With no animation in the document, both say animVal must report what the attribute now says. ### Cause The refresh already exists. Every non-list animated property does it inside the setter the attribute parse uses: https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/properties/SVGAnimatedValueProperty.h#L50-L57 // Used by SVGElement::parseAttribute(). void setBaseValInternal(const ValueType& baseVal) { m_baseVal->setValue(baseVal); if (m_animVal) m_animVal->setValue(baseVal); } The list version has the same two lines, but only on the DOM mutation path: https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/properties/SVGAnimatedPropertyList.h#L142-L148 // Called when m_baseVal changes or an item in m_baseVal changes. void commitPropertyChange(SVGProperty* property) override { if (m_animVal) *m_animVal = m_baseVal; Base::commitPropertyChange(property); } The attribute path never reaches it. attributeChanged calls baseVal()->parse(newValue) directly, for example SVGTextPositioningElement.cpp:74, and parse() does not call commitChange(), so commitPropertyChange never runs and the animVal clone made at first access is never touched again: https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/properties/SVGAnimatedPropertyList.h#L127-L132 ListType& ensureAnimVal() { if (!m_animVal) m_animVal = ListType::create(m_baseVal, SVGPropertyAccess::ReadOnly); return *m_animVal; } So the list case is the only animated property shape with neither of the two refreshes on the attribute path. Every SVGAnimatedNumber, SVGAnimatedLength, SVGAnimatedRect, SVGAnimatedAngle, SVGAnimatedString, SVGAnimatedEnumeration and SVGAnimatedPreserveAspectRatio was checked in the browser and all of them are correct. Suggested shape: refresh animVal after the parse, the way commitPropertyChange already does. Giving SVGAnimatedPropertyList its own parse entry point keeps the twelve attributeChanged call sites one line each. Making animVal() return m_baseVal when nothing is animating would be smaller and matches the SVG 2 alias wording, but it changes what an animVal item is, and LayoutTests/svg/dom/animated-tearoff-list-remove-target.html depends on the current shape: it holds an animVal item, retargets the <animate> away, and asserts the held item kept its own copy of the value. ### Tests No WPT coverage: nothing in the corpus reads animVal on a list, changes the attribute, and reads animVal again. The SMIL list tests re-read animVal.getItem(i).value at every sample, which is a fresh read each time and correct either way. No WebKit-local test covers it either, so nothing needs rebaselining. animated-tearoff-list-remove-target.html constrains the shape of the fix but does not assert this behaviour.
Attachments
testcase-probe.html (5.42 KB, text/html)
2026-08-31 02:16 PDT, Karl Dubost
no flags
Karl Dubost
Comment 1 2026-08-31 02:16:31 PDT
Created attachment 481223 [details] testcase-probe.html
Radar WebKit Bug Importer
Comment 2 2026-09-07 01:57:12 PDT
Note You need to log in before you can comment on or make changes to this bug.