From fcbfaf47e1ed6ba4814905ef7475eb455e868ee7 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 20 Apr 2026 18:34:25 +0100 Subject: [PATCH 1/2] Fix unbound C recursion in `Element.__deepcopy__()` --- Lib/test/test_xml_etree.py | 13 +++++++++++++ ...026-04-20-18-29-21.gh-issue-148801.ROeNqs.rst | 2 ++ Modules/_elementtree.c | 16 ++++++++++++++-- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-20-18-29-21.gh-issue-148801.ROeNqs.rst diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index b380d0276b0169..0a31ffe7081f78 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -3190,6 +3190,19 @@ def __deepcopy__(self, memo): self.assertEqual([c.tag for c in children[3:]], [a.tag, b.tag, a.tag, b.tag]) + @support.skip_if_unlimited_stack_size + @support.skip_emscripten_stack_overflow() + @support.skip_wasi_stack_overflow() + def test_deeply_nested_deepcopy(self): + # This should raise a RecursionError and not crash. + # See https://github.com/python/cpython/issues/148801. + root = cur = ET.Element('s') + for _ in range(50_000): + cur = ET.SubElement(cur, 'u') + with support.infinite_recursion(): + with self.assertRaises(RecursionError): + copy.deepcopy(root) + class MutationDeleteElementPath(str): def __new__(cls, elem, *args): diff --git a/Misc/NEWS.d/next/Library/2026-04-20-18-29-21.gh-issue-148801.ROeNqs.rst b/Misc/NEWS.d/next/Library/2026-04-20-18-29-21.gh-issue-148801.ROeNqs.rst new file mode 100644 index 00000000000000..6fcd30e8f057b9 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-20-18-29-21.gh-issue-148801.ROeNqs.rst @@ -0,0 +1,2 @@ +:mod:`xml.etree.ElementTree`: Fix a crash in :meth:`Element.__deepcopy__ +` on deeply nested trees. diff --git a/Modules/_elementtree.c b/Modules/_elementtree.c index e2185c4bd03aad..0c3261826aa6b2 100644 --- a/Modules/_elementtree.c +++ b/Modules/_elementtree.c @@ -16,6 +16,7 @@ #endif #include "Python.h" +#include "pycore_ceval.h" // _Py_EnterRecursiveCall() #include "pycore_dict.h" // _PyDict_CopyAsDict() #include "pycore_pyhash.h" // _Py_HashSecret #include "pycore_tuple.h" // _PyTuple_FromPair @@ -818,18 +819,25 @@ _elementtree_Element___deepcopy___impl(ElementObject *self, PyObject *memo) PyObject* tail; PyObject* id; + if (_Py_EnterRecursiveCall(" in Element.__deepcopy__")) { + return NULL; + } + PyTypeObject *tp = Py_TYPE(self); elementtreestate *st = get_elementtree_state_by_type(tp); // The deepcopy() helper takes care of incrementing the refcount // of the object to copy so to avoid use-after-frees. tag = deepcopy(st, self->tag, memo); - if (!tag) + if (!tag) { + _Py_LeaveRecursiveCall(); return NULL; + } if (self->extra && self->extra->attrib) { attrib = deepcopy(st, self->extra->attrib, memo); if (!attrib) { Py_DECREF(tag); + _Py_LeaveRecursiveCall(); return NULL; } } else { @@ -841,8 +849,10 @@ _elementtree_Element___deepcopy___impl(ElementObject *self, PyObject *memo) Py_DECREF(tag); Py_XDECREF(attrib); - if (!element) + if (!element) { + _Py_LeaveRecursiveCall(); return NULL; + } text = deepcopy(st, JOIN_OBJ(self->text), memo); if (!text) @@ -904,9 +914,11 @@ _elementtree_Element___deepcopy___impl(ElementObject *self, PyObject *memo) if (i < 0) goto error; + _Py_LeaveRecursiveCall(); return (PyObject*) element; error: + _Py_LeaveRecursiveCall(); Py_DECREF(element); return NULL; } From 230b3481da2e07fad8d20d8ab6c2f0e06cd85395 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Mon, 20 Apr 2026 18:53:37 +0100 Subject: [PATCH 2/2] Bigger number = recursionError ? --- Lib/test/test_xml_etree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 0a31ffe7081f78..51af46f124cac6 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -3197,7 +3197,7 @@ def test_deeply_nested_deepcopy(self): # This should raise a RecursionError and not crash. # See https://github.com/python/cpython/issues/148801. root = cur = ET.Element('s') - for _ in range(50_000): + for _ in range(150_000): cur = ET.SubElement(cur, 'u') with support.infinite_recursion(): with self.assertRaises(RecursionError):