From a46c10ec9d4050ab67b8a932e0859a2ea60c3cb8 Mon Sep 17 00:00:00 2001
From: "Miss Islington (bot)"
 <31488909+miss-islington@users.noreply.github.com>
Date: Sun, 25 Jan 2026 18:10:53 +0100
Subject: [PATCH] [3.11] gh-142145: Remove quadratic behavior in node ID cache
 clearing (GH-142146) (#142212)

* 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 <seth@python.org>
Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com>

* [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 <encukou@gmail.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>

* 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>

* merge NEWS entries into one

---------

Co-authored-by: Seth Michael Larson <seth@python.org>
Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com>
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Co-authored-by: Gregory P. Smith <greg@krypto.org>
---
 Lib/test/test_minidom.py                      | 33 ++++++++++++++++++-
 Lib/xml/dom/minidom.py                        | 11 ++-----
 ...-12-01-09-36-45.gh-issue-142145.tcAUhg.rst |  6 ++++
 3 files changed, 41 insertions(+), 9 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 ef38c362103fc6..c68bd990f723c9 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
@@ -9,7 +10,7 @@
 import pyexpat
 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
 
@@ -177,6 +178,36 @@ def testAppendChild(self):
         self.confirm(dom.documentElement.childNodes[-1].data == "Hello")
         dom.unlink()
 
+    @support.requires_resource('cpu')
+    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.monotonic()
+        for child in children:
+            element.appendChild(child)
+            element = child
+        end = time.monotonic()
+
+        # This example used to take at least 30 seconds.
+        # 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
+        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 ef8a159833bbc0..cada981f39f3ee 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."
@@ -355,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
@@ -680,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
@@ -1539,7 +1534,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..05c7df35d14bef
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
@@ -0,0 +1,6 @@
+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.
