From df996ce4b9860f8d0c391d876ffe6d0143132a81 Mon Sep 17 00:00:00 2001 From: Seth Michael Larson Date: Wed, 3 Dec 2025 01:16:37 -0600 Subject: [PATCH 1/4] gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146) * Remove quadratic behavior in node ID cache clearing Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com> * Add news fragment --------- (cherry picked from commit 08d8e18ad81cd45bc4a27d6da478b51ea49486e4) Co-authored-by: Seth Michael Larson Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com> --- Lib/test/test_minidom.py | 18 ++++++++++++++++++ Lib/xml/dom/minidom.py | 9 +-------- ...5-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 1 + 3 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 699265ccadc7f9..a83cb88ef9be79 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -2,6 +2,7 @@ import copy import pickle +import time import io from test import support import unittest @@ -176,6 +177,23 @@ def testAppendChild(self): self.confirm(dom.documentElement.childNodes[-1].data == "Hello") dom.unlink() + def testAppendChildNoQuadraticComplexity(self): + impl = getDOMImplementation() + + newdoc = impl.createDocument(None, "some_tag", None) + top_element = newdoc.documentElement + children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)] + element = top_element + + start = time.time() + for child in children: + element.appendChild(child) + element = child + end = time.time() + + # This example used to take at least 30 seconds. + self.assertLess(end - start, 1) + def testAppendChildFragment(self): dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes() dom.documentElement.appendChild(frag) diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index ef8a159833bbc0..83f717eeb5d043 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -292,13 +292,6 @@ def _append_child(self, node): childNodes.append(node) node.parentNode = self -def _in_document(node): - # return True iff node is part of a document tree - while node is not None: - if node.nodeType == Node.DOCUMENT_NODE: - return True - node = node.parentNode - return False def _write_data(writer, data): "Writes datachars to writer." @@ -1539,7 +1532,7 @@ def _clear_id_cache(node): if node.nodeType == Node.DOCUMENT_NODE: node._id_cache.clear() node._id_search_stack = None - elif _in_document(node): + elif node.ownerDocument: node.ownerDocument._id_cache.clear() node.ownerDocument._id_search_stack= None diff --git a/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst new file mode 100644 index 00000000000000..440bc7794c69ef --- /dev/null +++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst @@ -0,0 +1 @@ +Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. From 1ff4985dccfe14ccb59eaac7f2611764a635f828 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Sun, 21 Dec 2025 00:56:47 +0100 Subject: [PATCH 2/4] [3.14] gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794) (#142818) gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794) (cherry picked from commit 1cc7551b3f9f71efbc88d96dce90f82de98b2454) Co-authored-by: Petr Viktorin Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Lib/test/test_minidom.py | 10 +++++++++- Lib/xml/dom/minidom.py | 2 ++ .../2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index a83cb88ef9be79..5a1f96dd257417 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -9,7 +9,7 @@ import xml.dom.minidom -from xml.dom.minidom import parse, Attr, Node, Document, parseString +from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString from xml.dom.minidom import getDOMImplementation from xml.parsers.expat import ExpatError @@ -194,6 +194,14 @@ def testAppendChildNoQuadraticComplexity(self): # This example used to take at least 30 seconds. self.assertLess(end - start, 1) + def testSetAttributeNodeWithoutOwnerDocument(self): + # regression test for gh-142754 + elem = Element("test") + attr = Attr("id") + attr.value = "test-id" + elem.setAttributeNode(attr) + self.assertEqual(elem.getAttribute("id"), "test-id") + def testAppendChildFragment(self): dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes() dom.documentElement.appendChild(frag) diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py index 83f717eeb5d043..cada981f39f3ee 100644 --- a/Lib/xml/dom/minidom.py +++ b/Lib/xml/dom/minidom.py @@ -348,6 +348,7 @@ class Attr(Node): def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None, prefix=None): self.ownerElement = None + self.ownerDocument = None self._name = qName self.namespaceURI = namespaceURI self._prefix = prefix @@ -673,6 +674,7 @@ class Element(Node): def __init__(self, tagName, namespaceURI=EMPTY_NAMESPACE, prefix=None, localName=None): + self.ownerDocument = None self.parentNode = None self.tagName = self.nodeName = tagName self.prefix = prefix diff --git a/Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst b/Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst new file mode 100644 index 00000000000000..d4e158ccb8c9e6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst @@ -0,0 +1,4 @@ +Add the *ownerDocument* attribute to :mod:`xml.dom.minidom` elements and attributes +created by directly instantiating the ``Element`` or ``Attr`` class. Note that +this way of creating nodes is not supported; creator functions like +:py:meth:`xml.dom.Document.documentElement` should be used instead. From d9c479e5ed98b2f019b4fb9658f0093b800b9f2e Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Sat, 20 Dec 2025 15:42:06 -0800 Subject: [PATCH 3/4] gh-142145: relax the no-longer-quadratic test timing (GH-143030) * gh-142145: relax the no-longer-quadratic test timing * require cpu resource (cherry picked from commit 8d2d7bb2e754f8649a68ce4116271a4932f76907) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> --- Lib/test/test_minidom.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py index 5a1f96dd257417..ab4823c8315e57 100644 --- a/Lib/test/test_minidom.py +++ b/Lib/test/test_minidom.py @@ -177,6 +177,7 @@ def testAppendChild(self): self.confirm(dom.documentElement.childNodes[-1].data == "Hello") dom.unlink() + @support.requires_resource('cpu') def testAppendChildNoQuadraticComplexity(self): impl = getDOMImplementation() @@ -185,14 +186,18 @@ def testAppendChildNoQuadraticComplexity(self): children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)] element = top_element - start = time.time() + start = time.monotonic() for child in children: element.appendChild(child) element = child - end = time.time() + end = time.monotonic() # This example used to take at least 30 seconds. - self.assertLess(end - start, 1) + # Conservative assertion due to the wide variety of systems and + # build configs timing based tests wind up run under. + # A --with-address-sanitizer --with-pydebug build on a rpi5 still + # completes this loop in <0.5 seconds. + self.assertLess(end - start, 4) def testSetAttributeNodeWithoutOwnerDocument(self): # regression test for gh-142754 From c2d62026344bd3d8282385c2545991f8bfc8b9bd Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Sun, 21 Dec 2025 00:05:37 +0000 Subject: [PATCH 4/4] merge NEWS entries into one --- .../Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst | 4 ---- .../2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst diff --git a/Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst b/Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst deleted file mode 100644 index d4e158ccb8c9e6..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-12-16-11-55-55.gh-issue-142754.xuCrt3.rst +++ /dev/null @@ -1,4 +0,0 @@ -Add the *ownerDocument* attribute to :mod:`xml.dom.minidom` elements and attributes -created by directly instantiating the ``Element`` or ``Attr`` class. Note that -this way of creating nodes is not supported; creator functions like -:py:meth:`xml.dom.Document.documentElement` should be used instead. diff --git a/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst index 440bc7794c69ef..05c7df35d14bef 100644 --- a/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst +++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst @@ -1 +1,6 @@ -Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. +Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. In order +to do this without breaking existing users, we also add the *ownerDocument* +attribute to :mod:`xml.dom.minidom` elements and attributes created by directly +instantiating the ``Element`` or ``Attr`` class. Note that this way of creating +nodes is not supported; creator functions like +:py:meth:`xml.dom.Document.documentElement` should be used instead.