Skip to content

Commit 8becd16

Browse files
[3.14] gh-83895: Accept input larger than 2 GiB in the C implementation of ElementTree (GH-156746) (GH-156772)
XMLParser.feed() and ElementTree.parse() raised OverflowError, because Expat takes the length as an int. The data is now fed to Expat in chunks of 1 MiB, as xml.parsers.expat does since bpo-17089, so the pure Python implementation already accepted such input. (cherry picked from commit 9259e8b)
1 parent 4e8bce4 commit 8becd16

4 files changed

Lines changed: 64 additions & 29 deletions

File tree

Lib/test/test_xml_etree.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,41 @@ def test_parse_text_source_multiple_chunks(self):
10521052
xml = "<?xml version='1.0' encoding='ISO-8859-1'?><xml>%s</xml>" % body
10531053
self.assertEqual(ET.parse(io.StringIO(xml)).getroot().text, body)
10541054

1055+
def test_parse_input_larger_than_chunk(self):
1056+
# gh-83895: the C implementation feeds Expat in chunks of 1 MiB
1057+
size = 3 * (1 << 20)
1058+
xml = '<r><a>%s</a><b/></r>' % ('x' * size)
1059+
for source in xml, xml.encode():
1060+
with self.subTest(type=type(source).__name__):
1061+
root = ET.fromstring(source)
1062+
self.assertEqual(len(root[0].text), size)
1063+
self.assertEqual(root[1].tag, 'b')
1064+
1065+
# gh-83895: input larger than INT_MAX is fed to Expat in chunks.
1066+
# memuse is 3 for the Python implementation, which joins the collected
1067+
# data, 2 would be enough for the C implementation.
1068+
@support.bigmemtest(size=support._2G + 100, memuse=3, dry_run=False)
1069+
def test_large_input(self, size):
1070+
data = b'<r>' + b'x' * size + b'</r>'
1071+
root = None
1072+
try:
1073+
parser = ET.XMLParser()
1074+
parser.feed(data)
1075+
data = None
1076+
root = parser.close()
1077+
self.assertEqual(len(root.text), size)
1078+
finally:
1079+
data = None
1080+
root = None
1081+
1082+
def test_parse_error_after_chunk_boundary(self):
1083+
# the reported position accounts for the preceding chunks
1084+
size = 2 * (1 << 20)
1085+
with self.assertRaises(ET.ParseError) as cm:
1086+
ET.fromstring('<r>%s<</r>' % ('x' * size))
1087+
self.assertEqual(cm.exception.position, (1, size + 4))
1088+
1089+
10551090
def test_methods(self):
10561091
# Test serialization methods.
10571092

Lib/test/test_xml_etree_c.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,6 @@
1515

1616
@unittest.skipUnless(cET, 'requires _elementtree')
1717
class MiscTests(unittest.TestCase):
18-
# Issue #8651.
19-
@support.bigmemtest(size=support._2G + 100, memuse=1, dry_run=False)
20-
def test_length_overflow(self, size):
21-
data = b'x' * size
22-
parser = cET.XMLParser()
23-
try:
24-
self.assertRaises(OverflowError, parser.feed, data)
25-
finally:
26-
data = None
27-
2818
def test_del_attribute(self):
2919
element = cET.Element('tag')
3020

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`xml.etree.ElementTree` now accepts input larger than 2 GiB
2+
in the C implementation.
3+
The data is fed to Expat in chunks, as :mod:`xml.parsers.expat` already did,
4+
instead of raising :exc:`OverflowError`.

Modules/_elementtree.c

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3928,6 +3928,27 @@ expat_parse(elementtreestate *st, XMLParserObject *self, const char *data,
39283928
Py_RETURN_NONE;
39293929
}
39303930

3931+
/* Expat takes the length as an int, feed larger data in chunks. */
3932+
#define MAX_CHUNK_SIZE (1 << 20)
3933+
3934+
LOCAL(PyObject*)
3935+
expat_parse_large(elementtreestate *st, XMLParserObject *self,
3936+
const char *data, Py_ssize_t data_len, int final)
3937+
{
3938+
static_assert(MAX_CHUNK_SIZE <= INT_MAX,
3939+
"MAX_CHUNK_SIZE is larger than INT_MAX");
3940+
while (data_len > MAX_CHUNK_SIZE) {
3941+
PyObject *res = expat_parse(st, self, data, MAX_CHUNK_SIZE, 0);
3942+
if (res == NULL) {
3943+
return NULL;
3944+
}
3945+
Py_DECREF(res);
3946+
data += MAX_CHUNK_SIZE;
3947+
data_len -= MAX_CHUNK_SIZE;
3948+
}
3949+
return expat_parse(st, self, data, (int)data_len, final);
3950+
}
3951+
39313952
/*[clinic input]
39323953
_elementtree.XMLParser.close
39333954
@@ -4019,26 +4040,17 @@ _elementtree_XMLParser_feed_impl(XMLParserObject *self, PyObject *data)
40194040
const char *data_ptr = PyUnicode_AsUTF8AndSize(data, &data_len);
40204041
if (data_ptr == NULL)
40214042
return NULL;
4022-
if (data_len > INT_MAX) {
4023-
PyErr_SetString(PyExc_OverflowError, "size does not fit in an int");
4024-
return NULL;
4025-
}
40264043
/* Explicitly set UTF-8 encoding. Return code ignored. */
40274044
(void)EXPAT(st, SetEncoding)(self->parser, "utf-8");
40284045

4029-
return expat_parse(st, self, data_ptr, (int)data_len, 0);
4046+
return expat_parse_large(st, self, data_ptr, data_len, 0);
40304047
}
40314048
else {
40324049
Py_buffer view;
40334050
PyObject *res;
40344051
if (PyObject_GetBuffer(data, &view, PyBUF_SIMPLE) < 0)
40354052
return NULL;
4036-
if (view.len > INT_MAX) {
4037-
PyBuffer_Release(&view);
4038-
PyErr_SetString(PyExc_OverflowError, "size does not fit in an int");
4039-
return NULL;
4040-
}
4041-
res = expat_parse(st, self, view.buf, (int)view.len, 0);
4053+
res = expat_parse_large(st, self, view.buf, view.len, 0);
40424054
PyBuffer_Release(&view);
40434055
return res;
40444056
}
@@ -4108,14 +4120,8 @@ _elementtree_XMLParser__parse_whole_impl(XMLParserObject *self,
41084120
break;
41094121
}
41104122

4111-
if (PyBytes_GET_SIZE(buffer) > INT_MAX) {
4112-
Py_DECREF(buffer);
4113-
Py_DECREF(reader);
4114-
PyErr_SetString(PyExc_OverflowError, "size does not fit in an int");
4115-
return NULL;
4116-
}
4117-
res = expat_parse(
4118-
st, self, PyBytes_AS_STRING(buffer), (int)PyBytes_GET_SIZE(buffer),
4123+
res = expat_parse_large(
4124+
st, self, PyBytes_AS_STRING(buffer), PyBytes_GET_SIZE(buffer),
41194125
0);
41204126
first = 0;
41214127

0 commit comments

Comments
 (0)