Skip to content

Commit 0622638

Browse files
[3.13] gh-83895: Accept input larger than 2 GiB in the C implementation of ElementTree (GH-156746) (GH-156773)
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 16f4dee commit 0622638

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
@@ -1053,6 +1053,41 @@ def test_parse_text_source_multiple_chunks(self):
10531053
xml = "<?xml version='1.0' encoding='ISO-8859-1'?><xml>%s</xml>" % body
10541054
self.assertEqual(ET.parse(io.StringIO(xml)).getroot().text, body)
10551055

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

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
@@ -3894,6 +3894,27 @@ expat_parse(elementtreestate *st, XMLParserObject *self, const char *data,
38943894
Py_RETURN_NONE;
38953895
}
38963896

3897+
/* Expat takes the length as an int, feed larger data in chunks. */
3898+
#define MAX_CHUNK_SIZE (1 << 20)
3899+
3900+
LOCAL(PyObject*)
3901+
expat_parse_large(elementtreestate *st, XMLParserObject *self,
3902+
const char *data, Py_ssize_t data_len, int final)
3903+
{
3904+
static_assert(MAX_CHUNK_SIZE <= INT_MAX,
3905+
"MAX_CHUNK_SIZE is larger than INT_MAX");
3906+
while (data_len > MAX_CHUNK_SIZE) {
3907+
PyObject *res = expat_parse(st, self, data, MAX_CHUNK_SIZE, 0);
3908+
if (res == NULL) {
3909+
return NULL;
3910+
}
3911+
Py_DECREF(res);
3912+
data += MAX_CHUNK_SIZE;
3913+
data_len -= MAX_CHUNK_SIZE;
3914+
}
3915+
return expat_parse(st, self, data, (int)data_len, final);
3916+
}
3917+
38973918
/*[clinic input]
38983919
_elementtree.XMLParser.close
38993920
@@ -3985,26 +4006,17 @@ _elementtree_XMLParser_feed(XMLParserObject *self, PyObject *data)
39854006
const char *data_ptr = PyUnicode_AsUTF8AndSize(data, &data_len);
39864007
if (data_ptr == NULL)
39874008
return NULL;
3988-
if (data_len > INT_MAX) {
3989-
PyErr_SetString(PyExc_OverflowError, "size does not fit in an int");
3990-
return NULL;
3991-
}
39924009
/* Explicitly set UTF-8 encoding. Return code ignored. */
39934010
(void)EXPAT(st, SetEncoding)(self->parser, "utf-8");
39944011

3995-
return expat_parse(st, self, data_ptr, (int)data_len, 0);
4012+
return expat_parse_large(st, self, data_ptr, data_len, 0);
39964013
}
39974014
else {
39984015
Py_buffer view;
39994016
PyObject *res;
40004017
if (PyObject_GetBuffer(data, &view, PyBUF_SIMPLE) < 0)
40014018
return NULL;
4002-
if (view.len > INT_MAX) {
4003-
PyBuffer_Release(&view);
4004-
PyErr_SetString(PyExc_OverflowError, "size does not fit in an int");
4005-
return NULL;
4006-
}
4007-
res = expat_parse(st, self, view.buf, (int)view.len, 0);
4019+
res = expat_parse_large(st, self, view.buf, view.len, 0);
40084020
PyBuffer_Release(&view);
40094021
return res;
40104022
}
@@ -4073,14 +4085,8 @@ _elementtree_XMLParser__parse_whole(XMLParserObject *self, PyObject *file)
40734085
break;
40744086
}
40754087

4076-
if (PyBytes_GET_SIZE(buffer) > INT_MAX) {
4077-
Py_DECREF(buffer);
4078-
Py_DECREF(reader);
4079-
PyErr_SetString(PyExc_OverflowError, "size does not fit in an int");
4080-
return NULL;
4081-
}
4082-
res = expat_parse(
4083-
st, self, PyBytes_AS_STRING(buffer), (int)PyBytes_GET_SIZE(buffer),
4088+
res = expat_parse_large(
4089+
st, self, PyBytes_AS_STRING(buffer), PyBytes_GET_SIZE(buffer),
40844090
0);
40854091
first = 0;
40864092

0 commit comments

Comments
 (0)