Skip to content

Commit 2c98cae

Browse files
committed
handle evil mutations in Element.remove
1 parent 9b6f559 commit 2c98cae

File tree

2 files changed

+75
-33
lines changed

2 files changed

+75
-33
lines changed

Lib/test/test_xml_etree.py

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2680,18 +2680,50 @@ class Y(X, ET.Element):
26802680
e = ET.Element('foo')
26812681
e.extend(L)
26822682

2683-
def test_remove_with_mutating(self):
2684-
class X(ET.Element):
2683+
def test_remove_with_clear_children(self):
2684+
# See: https://github.com/python/cpython/issues/126033
2685+
2686+
class EvilElement(ET.Element):
26852687
def __eq__(self, o):
2686-
del e[:]
2688+
root.clear()
26872689
return False
2688-
e = ET.Element('foo')
2689-
e.extend([X('bar')])
2690-
self.assertRaises(ValueError, e.remove, ET.Element('baz'))
26912690

2692-
e = ET.Element('foo')
2693-
e.extend([ET.Element('bar')])
2694-
self.assertRaises(ValueError, e.remove, X('baz'))
2691+
# The pure Python implementation raises a ValueError but the C
2692+
# implementation raises a RuntimeError (like OrderedDict does).
2693+
exc_type = ValueError if ET is pyET else RuntimeError
2694+
2695+
root = ET.Element('.')
2696+
root.append(EvilElement('foo'))
2697+
root.append(ET.Element('bar'))
2698+
self.assertRaises(exc_type, root.remove, ET.Element('pouet'))
2699+
2700+
root = ET.Element('.')
2701+
root.append(ET.Element('foo'))
2702+
root.append(EvilElement('bar'))
2703+
self.assertRaises(exc_type, root.remove, EvilElement('pouet'))
2704+
2705+
def test_remove_with_mutate_children(self):
2706+
# See: https://github.com/python/cpython/issues/126033
2707+
2708+
class EvilElement(ET.Element):
2709+
def __eq__(self, o):
2710+
# Remove the first element so that the list size changes.
2711+
# This causes an infinite recursion error in the Python
2712+
# implementation, but we do not really care about it.
2713+
root.remove(ET.Element('foo'))
2714+
return False
2715+
2716+
# The pure Python implementation raises a ValueError but the C
2717+
# implementation raises a RuntimeError (like OrderedDict does).
2718+
exc_type = (RecursionError, ValueError) if ET is pyET else RuntimeError
2719+
2720+
root = ET.Element('.')
2721+
root.extend([ET.Element('foo'), EvilElement('bar')])
2722+
self.assertRaises(exc_type, root.remove, ET.Element('baz'))
2723+
2724+
root = ET.Element('.')
2725+
root.extend([ET.Element('foo'), EvilElement('bar')])
2726+
self.assertRaises(exc_type, root.remove, EvilElement('baz'))
26952727

26962728
@support.infinite_recursion(25)
26972729
def test_recursive_repr(self):

Modules/_elementtree.c

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1642,48 +1642,58 @@ static PyObject *
16421642
_elementtree_Element_remove_impl(ElementObject *self, PyObject *subelement)
16431643
/*[clinic end generated code: output=38fe6c07d6d87d1f input=6133e1d05597d5ee]*/
16441644
{
1645-
Py_ssize_t i;
1646-
int rc;
1647-
PyObject *found;
1648-
1649-
if (!self->extra) {
1645+
if (self->extra == NULL) {
16501646
/* element has no children, so raise exception */
1651-
PyErr_SetString(
1652-
PyExc_ValueError,
1653-
"list.remove(x): x not in list"
1654-
);
1655-
return NULL;
1647+
goto not_found;
16561648
}
16571649

1658-
// TODO(picnixz): check against evil __eq__
1659-
for (i = 0; i < self->extra->length; i++) {
1660-
if (self->extra->children[i] == subelement)
1650+
Py_ssize_t i;
1651+
size_t old_version = self->extra->version;
1652+
for (i = 0; self->extra && i < self->extra->length; i++) {
1653+
if (old_version != self->extra->version) {
1654+
goto fail;
1655+
}
1656+
else if (self->extra->children[i] == subelement) {
16611657
break;
1662-
rc = PyObject_RichCompareBool(self->extra->children[i], subelement, Py_EQ);
1663-
if (rc > 0)
1658+
}
1659+
PyObject *child = Py_NewRef(self->extra->children[i]);
1660+
int rc = PyObject_RichCompareBool(child, subelement, Py_EQ);
1661+
Py_DECREF(child);
1662+
if (rc > 0) {
16641663
break;
1665-
if (rc < 0)
1664+
}
1665+
else if (rc < 0) {
16661666
return NULL;
1667+
}
16671668
}
16681669

1669-
if (i >= self->extra->length) {
1670+
// An extra check must be done if the mutation occurs at the very last step.
1671+
if (self->extra == NULL || old_version != self->extra->version) {
1672+
goto fail;
1673+
}
1674+
else if (i >= self->extra->length) {
16701675
/* subelement is not in children, so raise exception */
1671-
PyErr_SetString(
1672-
PyExc_ValueError,
1673-
"list.remove(x): x not in list"
1674-
);
1675-
return NULL;
1676+
goto not_found;
16761677
}
16771678

1678-
found = self->extra->children[i];
1679+
PyObject *found = self->extra->children[i];
16791680

16801681
self->extra->length--;
1681-
for (; i < self->extra->length; i++)
1682+
for (; i < self->extra->length; i++) {
16821683
self->extra->children[i] = self->extra->children[i+1];
1684+
}
16831685
self->extra->version++;
16841686

16851687
Py_DECREF(found);
16861688
Py_RETURN_NONE;
1689+
1690+
not_found:
1691+
PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list");
1692+
return NULL;
1693+
1694+
fail:
1695+
PyErr_SetString(PyExc_RuntimeError, "children mutated during iteration");
1696+
return NULL;
16871697
}
16881698

16891699
static PyObject*

0 commit comments

Comments
 (0)