Skip to content

Commit 9718880

Browse files
[3.13] gh-133009: fix UAF in xml.etree.ElementTree.Element.__deepcopy__ (GH-133010) (#133806)
gh-133009: fix UAF in `xml.etree.ElementTree.Element.__deepcopy__` (GH-133010) (cherry picked from commit 116a9f9) Co-authored-by: Bénédikt Tran <[email protected]>
1 parent 57efb77 commit 9718880

File tree

3 files changed

+81
-7
lines changed

3 files changed

+81
-7
lines changed

Lib/test/test_xml_etree.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2960,6 +2960,50 @@ def element_factory(x, y):
29602960
del b
29612961
gc_collect()
29622962

2963+
def test_deepcopy_clear(self):
2964+
# Prevent crashes when __deepcopy__() clears the children list.
2965+
# See https://github.com/python/cpython/issues/133009.
2966+
class X(ET.Element):
2967+
def __deepcopy__(self, memo):
2968+
root.clear()
2969+
return self
2970+
2971+
root = ET.Element('a')
2972+
evil = X('x')
2973+
root.extend([evil, ET.Element('y')])
2974+
if is_python_implementation():
2975+
# Mutating a list over which we iterate raises an error.
2976+
self.assertRaises(RuntimeError, copy.deepcopy, root)
2977+
else:
2978+
c = copy.deepcopy(root)
2979+
# In the C implementation, we can still copy the evil element.
2980+
self.assertListEqual(list(c), [evil])
2981+
2982+
def test_deepcopy_grow(self):
2983+
# Prevent crashes when __deepcopy__() mutates the children list.
2984+
# See https://github.com/python/cpython/issues/133009.
2985+
a = ET.Element('a')
2986+
b = ET.Element('b')
2987+
c = ET.Element('c')
2988+
2989+
class X(ET.Element):
2990+
def __deepcopy__(self, memo):
2991+
root.append(a)
2992+
root.append(b)
2993+
return self
2994+
2995+
root = ET.Element('top')
2996+
evil1, evil2 = X('1'), X('2')
2997+
root.extend([evil1, c, evil2])
2998+
children = list(copy.deepcopy(root))
2999+
# mock deep copies
3000+
self.assertIs(children[0], evil1)
3001+
self.assertIs(children[2], evil2)
3002+
# true deep copies
3003+
self.assertEqual(children[1].tag, c.tag)
3004+
self.assertEqual([c.tag for c in children[3:]],
3005+
[a.tag, b.tag, a.tag, b.tag])
3006+
29633007

29643008
class MutationDeleteElementPath(str):
29653009
def __new__(cls, elem, *args):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`xml.etree.ElementTree`: Fix a crash in :meth:`Element.__deepcopy__
2+
<object.__deepcopy__>` when the element is concurrently mutated.
3+
Patch by Bénédikt Tran.

Modules/_elementtree.c

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,8 @@ _elementtree_Element___deepcopy___impl(ElementObject *self, PyObject *memo)
810810

811811
PyTypeObject *tp = Py_TYPE(self);
812812
elementtreestate *st = get_elementtree_state_by_type(tp);
813+
// The deepcopy() helper takes care of incrementing the refcount
814+
// of the object to copy so to avoid use-after-frees.
813815
tag = deepcopy(st, self->tag, memo);
814816
if (!tag)
815817
return NULL;
@@ -844,11 +846,13 @@ _elementtree_Element___deepcopy___impl(ElementObject *self, PyObject *memo)
844846

845847
assert(!element->extra || !element->extra->length);
846848
if (self->extra) {
847-
if (element_resize(element, self->extra->length) < 0)
849+
Py_ssize_t expected_count = self->extra->length;
850+
if (element_resize(element, expected_count) < 0) {
851+
assert(!element->extra->length);
848852
goto error;
853+
}
849854

850-
// TODO(picnixz): check for an evil child's __deepcopy__ on 'self'
851-
for (i = 0; i < self->extra->length; i++) {
855+
for (i = 0; self->extra && i < self->extra->length; i++) {
852856
PyObject* child = deepcopy(st, self->extra->children[i], memo);
853857
if (!child || !Element_Check(st, child)) {
854858
if (child) {
@@ -858,11 +862,24 @@ _elementtree_Element___deepcopy___impl(ElementObject *self, PyObject *memo)
858862
element->extra->length = i;
859863
goto error;
860864
}
865+
if (self->extra && expected_count != self->extra->length) {
866+
// 'self->extra' got mutated and 'element' may not have
867+
// sufficient space to hold the next iteration's item.
868+
expected_count = self->extra->length;
869+
if (element_resize(element, expected_count) < 0) {
870+
Py_DECREF(child);
871+
element->extra->length = i;
872+
goto error;
873+
}
874+
}
861875
element->extra->children[i] = child;
862876
}
863877

864878
assert(!element->extra->length);
865-
element->extra->length = self->extra->length;
879+
// The original 'self->extra' may be gone at this point if deepcopy()
880+
// has side-effects. However, 'i' is the number of copied items that
881+
// we were able to successfully copy.
882+
element->extra->length = i;
866883
}
867884

868885
/* add object to memo dictionary (so deepcopy won't visit it again) */
@@ -905,13 +922,20 @@ deepcopy(elementtreestate *st, PyObject *object, PyObject *memo)
905922
break;
906923
}
907924
}
908-
if (simple)
925+
if (simple) {
909926
return PyDict_Copy(object);
927+
}
910928
/* Fall through to general case */
911929
}
912930
else if (Element_CheckExact(st, object)) {
913-
return _elementtree_Element___deepcopy___impl(
931+
// The __deepcopy__() call may call arbitrary code even if the
932+
// object to copy is a built-in XML element (one of its children
933+
// any of its parents in its own __deepcopy__() implementation).
934+
Py_INCREF(object);
935+
PyObject *res = _elementtree_Element___deepcopy___impl(
914936
(ElementObject *)object, memo);
937+
Py_DECREF(object);
938+
return res;
915939
}
916940
}
917941

@@ -922,8 +946,11 @@ deepcopy(elementtreestate *st, PyObject *object, PyObject *memo)
922946
return NULL;
923947
}
924948

949+
Py_INCREF(object);
925950
PyObject *args[2] = {object, memo};
926-
return PyObject_Vectorcall(st->deepcopy_obj, args, 2, NULL);
951+
PyObject *res = PyObject_Vectorcall(st->deepcopy_obj, args, 2, NULL);
952+
Py_DECREF(object);
953+
return res;
927954
}
928955

929956

0 commit comments

Comments
 (0)