From e6d196ad72a6e8220c69719ad8f7458d44a9e25b Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 31 Aug 2026 23:35:00 +0300 Subject: [PATCH] gh-83895: Accept input larger than 2 GiB in the C implementation of ElementTree 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. --- Lib/test/test_xml_etree.py | 34 ++++++++++++++ Lib/test/test_xml_etree_c.py | 10 ----- ...6-08-31-23-10-00.gh-issue-83895.Jm5tR3.rst | 4 ++ Modules/_elementtree.c | 44 +++++++++++-------- 4 files changed, 63 insertions(+), 29 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-31-23-10-00.gh-issue-83895.Jm5tR3.rst diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index 2af2d1fd64520b1..cd17e647ebeab01 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -1068,6 +1068,40 @@ def bxml(encoding, body=''): self.assertRaises(ValueError, ET.XML, xml('undefined').encode('ascii')) self.assertRaises(LookupError, ET.XML, xml('xxx').encode('ascii')) + def test_parse_input_larger_than_chunk(self): + # gh-83895: the C implementation feeds Expat in chunks of 1 MiB + size = 3 * (1 << 20) + xml = '%s' % ('x' * size) + for source in xml, xml.encode(): + with self.subTest(type=type(source).__name__): + root = ET.fromstring(source) + self.assertEqual(len(root[0].text), size) + self.assertEqual(root[1].tag, 'b') + + # gh-83895: input larger than INT_MAX is fed to Expat in chunks. + # memuse is 3 for the Python implementation, which joins the collected + # data, 2 would be enough for the C implementation. + @support.bigmemtest(size=support._2G + 100, memuse=3, dry_run=False) + def test_large_input(self, size): + data = b'' + b'x' * size + b'' + root = None + try: + parser = ET.XMLParser() + parser.feed(data) + data = None + root = parser.close() + self.assertEqual(len(root.text), size) + finally: + data = None + root = None + + def test_parse_error_after_chunk_boundary(self): + # the reported position accounts for the preceding chunks + size = 2 * (1 << 20) + with self.assertRaises(ET.ParseError) as cm: + ET.fromstring('%s<' % ('x' * size)) + self.assertEqual(cm.exception.position, (1, size + 4)) + @support.subTests('sample,exception', [ (b' \xa1', UnicodeDecodeError), # crashed (b' \xa1 MAX_CHUNK_SIZE) { + PyObject *res = expat_parse(st, self, data, MAX_CHUNK_SIZE, 0); + if (res == NULL) { + return NULL; + } + Py_DECREF(res); + data += MAX_CHUNK_SIZE; + data_len -= MAX_CHUNK_SIZE; + } + return expat_parse(st, self, data, (int)data_len, final); +} + /*[clinic input] _elementtree.XMLParser.close @@ -4031,26 +4052,17 @@ _elementtree_XMLParser_feed_impl(XMLParserObject *self, PyObject *data) const char *data_ptr = PyUnicode_AsUTF8AndSize(data, &data_len); if (data_ptr == NULL) return NULL; - if (data_len > INT_MAX) { - PyErr_SetString(PyExc_OverflowError, "size does not fit in an int"); - return NULL; - } /* Explicitly set UTF-8 encoding. Return code ignored. */ (void)EXPAT(st, SetEncoding)(self->parser, "utf-8"); - return expat_parse(st, self, data_ptr, (int)data_len, 0); + return expat_parse_large(st, self, data_ptr, data_len, 0); } else { Py_buffer view; PyObject *res; if (PyObject_GetBuffer(data, &view, PyBUF_SIMPLE) < 0) return NULL; - if (view.len > INT_MAX) { - PyBuffer_Release(&view); - PyErr_SetString(PyExc_OverflowError, "size does not fit in an int"); - return NULL; - } - res = expat_parse(st, self, view.buf, (int)view.len, 0); + res = expat_parse_large(st, self, view.buf, view.len, 0); PyBuffer_Release(&view); return res; } @@ -4114,14 +4126,8 @@ _elementtree_XMLParser__parse_whole_impl(XMLParserObject *self, break; } - if (PyBytes_GET_SIZE(buffer) > INT_MAX) { - Py_DECREF(buffer); - Py_DECREF(reader); - PyErr_SetString(PyExc_OverflowError, "size does not fit in an int"); - return NULL; - } - res = expat_parse( - st, self, PyBytes_AS_STRING(buffer), (int)PyBytes_GET_SIZE(buffer), + res = expat_parse_large( + st, self, PyBytes_AS_STRING(buffer), PyBytes_GET_SIZE(buffer), 0); Py_DECREF(buffer);