RESOLVED FIXED322973
Changing a list attribute detaches the SVGNumber, SVGLength and SVGPoint objects a script is holding instead of updating them
https://bugs.webkit.org/show_bug.cgi?id=322973
Summary Changing a list attribute detaches the SVGNumber, SVGLength and SVGPoint obje...
Karl Dubost
Reported 2026-08-31 01:54:05 PDT
Hold an item out of a reflected list, change the content attribute, and the object you are holding stops tracking the element. It keeps the old number, it is no longer in the list, and writing to it silently does nothing. SVG 2 says the object survives and its value is updated, and detaching happens only for the items a shorter list no longer has room for. Firefox 156 does the specified thing on every branch of the algorithm and on all four object-typed lists. WebKit does it for SVGTransformList, and only while the transform functions stay the same, which is a separate bug. This one is about the other three: rotate, x/y/dx/dy, points, and the filter list attributes. ### Testcase <svg xmlns="http://www.w3.org/2000/svg"> <text id="t" x="1 2 3" y="20">abc</text> </svg> const list = t.x.baseVal; const held = list.getItem(0); t.setAttribute("x", "7 8 9"); // same number of items held.value; // 7 per spec. Safari says 1. list.getItem(0) === held; // true per spec. Safari says false. held.value = 42; // must put 42 at index 0 of the list t.x.baseVal.getItem(0).value; // 42 per spec. Safari says 7. The last two lines are the ones that hurt an author: the write is accepted, nothing throws, nothing is logged, and it goes nowhere. ### Spec Setting the attribute changes the base value of a reflected content attribute, which triggers: "Whenever a reflected content attribute's base value changes, then the reflecting object must be synchronized, immediately after the value changed" step 1: "If the reflecting object is a list interface object, then run the steps for synchronizing a list interface object." https://w3c.github.io/svgwg/svg2-draft/types.html#SynchronizingReflectedValues https://w3c.github.io/svgwg/svg2-draft/types.html#TermSynchronizeList step 4.1 "If length > new length" then "Detach each object in the list at an index greater than or equal to new length". This is the only detach in the algorithm. step 4.2 "While length < new length" create and append new objects. step 4.4 For every index below length: "Let item be the object in the list at index index", "Let v be the value in value at index index", "Set item's value to v". Three items becoming three items runs neither 4.1 nor 4.2, so nothing is detached and nothing is created, and 4.4 updates the objects already there. ### Measured, 3 engines, 2026-08-29 Probe attached. All 7 controls pass in all three runs, no errors. Firefox 156.0 fails nothing in the whole file, 144 of 144. held item follows the change, per branch, on SVGNumberList / SVGLengthList / SVGPointList: Safari 27.0 Firefox 156.0 Chrome 154 same count 3 -> 3 no YES no grows 3 -> 4 no YES no shrinks 3 -> 2 no YES no (the item still in range) shrinks, the truncated item is detached and keeps its value YES YES YES attribute removed, list emptied YES YES YES Only the last two rows are right in Safari today, and they are the branch where NOT following the change is what the algorithm asks for: step 4.1 detaches "each object in the list at an index greater than or equal to new length", which for 3 items becoming 2 is index 2 alone. A detached object keeping its value is correct, per https://w3c.github.io/svgwg/svg2-draft/types.html#TermDetach So the shrink case is the interesting one: Safari detaches the two survivors along with the one item it should. Chrome fails the same rows, and separately returns a fresh wrapper object from every getItem call, so identity there is not observable at all. That is a different defect and the probe measures it separately. ### Cause SVGNumberList::parse, SVGLengthList::parse and SVGPointList::parse each begin by clearing the list, and clearing detaches every item: https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/SVGNumberList.cpp#L34-L38 https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/SVGLengthList.cpp#L34-L38 https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/SVGPointList.cpp#L34-L38 https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/properties/SVGList.h#L146-L152 https://github.com/WebKit/WebKit/blob/4ae53427d5d8e52791da2cfe0c0c98b877b2b5d5/Source/WebCore/svg/properties/SVGPropertyList.h#L57-L61 That clear is from 2009, 39298@main, bug 28673, and it fixed a real bug: without it a re-parse appended, so rotate="10 20 30" changed to "40 50" left five items. Resizing fixes the count too, and without detaching the survivors. The test landed with it, LayoutTests/svg/dom/text-rotate-live.html, re-reads getItem(i) on every assertion and keeps passing either way. SVGTransformList already updates its items in place, as long as the transform function at each index stays the same, and it got there as a performance patch rather than a conformance one: 292545@main, bug 290192, "Content that animates SVG transforms will often keep the same list of transform operations, but just change the values ... this involves a lot of object churn: each SVGTransform is heap-allocated, and itself has a heap-allocated SVGMatrix. In addition, detach() and attach() have overhead." The same argument applies unchanged to rotate, points and the filter lists. So the conformance fix and the allocation-churn fix are one patch. The whole affected surface, from grep -rn "baseVal()->parse(" Source/WebCore/svg/: SVGNumberList rotate SVGTextPositioningElement.cpp:74 SVGNumberList tableValues SVGComponentTransferFunctionElement.cpp:60 SVGNumberList kernelMatrix SVGFEConvolveMatrixElement.cpp:98 SVGNumberList values SVGFEColorMatrixElement.cpp:64 SVGLengthList x, y, dx, dy SVGTextPositioningElement.cpp:62,65,68,71 SVGPointList points SVGPolyElement.cpp:53 Nine attributes. feColorMatrix values and feFuncR tableValues are the ones a hand-written filter animation writes on every frame. Suggested shape, mirroring SVGTransformList::parseGeneric: write into the item already at the index when there is one, append when there is not, and call the existing SVGValuePropertyList::resize at the end so the tail is detached by removeAt, which already calls detach(). Keep clearItems() on the parse-failure path: that is bug 308744 and it is what step 1 asks for, since the initial value of these attributes is empty. ### Tests No WPT test covers this, in either direction. Nothing in the corpus holds a list item across a re-parse; the near misses all re-fetch, so they pass under both behaviours. 42 WPT files under svg/ and css/css-transforms/ use getItem, so that search was not blind. One WebKit-local test asserts the current behaviour and needs rewriting rather than rebaselining: LayoutTests/svg/dom/SVGLengthList-xml-dom-modifications.xhtml, from 2010. Its case is a 3 to 2 shrink, so index 2 is genuinely detached and that assertion is correct and should stay. Six of its fifteen assertions change, and because the held items are written to again afterwards, text1 ends with x="150 200" instead of "50 100", so the three -expected.png baselines change too and the prose "both elements on top look the same" stops being true. ### Related - Bug 314992, same algorithm, opposite conclusion, answered in comment #2 there. - SVGTransformList has a second, narrower form of this bug: it detaches when the transform function at an index changes. Filed separately. - animVal for all four lists goes stale after an attribute change. Filed separately.
Attachments
testcase-probe.html (4.84 KB, text/html)
2026-08-31 02:15 PDT, Karl Dubost
no flags
probe-list-reparse-synchronize.html (52.56 KB, text/html)
2026-08-31 02:24 PDT, Karl Dubost
no flags
run-2026-08-29-chrome-154.0.0.0-probe-list-reparse-synchronize.json (44.88 KB, application/json)
2026-08-31 02:25 PDT, Karl Dubost
no flags
run-2026-08-29-firefox-156.0-probe-list-reparse-synchronize.json (44.79 KB, application/json)
2026-08-31 02:25 PDT, Karl Dubost
no flags
run-2026-08-29-safari-27.0-probe-list-reparse-synchronize.json (44.88 KB, application/json)
2026-08-31 02:26 PDT, Karl Dubost
no flags
Karl Dubost
Comment 1 2026-08-31 02:15:22 PDT
Created attachment 481221 [details] testcase-probe.html
Karl Dubost
Comment 2 2026-08-31 02:24:26 PDT
Created attachment 481226 [details] probe-list-reparse-synchronize.html
Karl Dubost
Comment 3 2026-08-31 02:25:10 PDT
Created attachment 481227 [details] run-2026-08-29-chrome-154.0.0.0-probe-list-reparse-synchronize.json
Karl Dubost
Comment 4 2026-08-31 02:25:50 PDT
Created attachment 481228 [details] run-2026-08-29-firefox-156.0-probe-list-reparse-synchronize.json
Karl Dubost
Comment 5 2026-08-31 02:26:22 PDT
Created attachment 481229 [details] run-2026-08-29-safari-27.0-probe-list-reparse-synchronize.json
Radar WebKit Bug Importer
Comment 6 2026-09-07 01:55:14 PDT
Karl Dubost
Comment 7 2026-10-05 18:14:01 PDT
EWS
Comment 8 2026-10-07 17:38:44 PDT
Committed 323046@main (b75776a843e5): <https://commits.webkit.org/323046@main> Reviewed commits have been landed. Closing PR #75981 and removing active labels.
Karl Dubost
Comment 9 2026-10-07 22:05:38 PDT
Submitted web-platform-tests pull request: https://github.com/web-platform-tests/wpt/pull/63389
Note You need to log in before you can comment on or make changes to this bug.