Skip to content

Commit 75e8f80

Browse files
[3.14] gh-146169: correctly handle re-entrant parsing calls in Expat handlers (GH-146566) (#160411)
gh-146169: correctly handle re-entrant parsing calls in Expat handlers (GH-146566) (cherry picked from commit dfd2c7c) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent e5bf734 commit 75e8f80

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

‎Lib/test/test_pyexpat.py‎

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from io import BytesIO
1414
from test import support
1515
from test.support import import_helper, os_helper
16-
16+
from unittest import mock
1717
from xml.parsers import expat
1818
from xml.parsers.expat import errors
1919

@@ -276,6 +276,89 @@ def test_parse_again(self):
276276
self.assertEqual(expat.ErrorString(cm.exception.code),
277277
expat.errors.XML_ERROR_FINISHED)
278278

279+
@support.subTests("encoding", ("utf-8", "utf-16"))
280+
def test_parse_reentrancy_with_encoding(self, encoding):
281+
# See https://github.com/python/cpython/issues/146169.
282+
parser = expat.ParserCreate(encoding=encoding)
283+
284+
def CharacterDataHandler(data):
285+
return parser.Parse(data, False)
286+
CharacterDataHandler = mock.Mock(wraps=CharacterDataHandler)
287+
parser.CharacterDataHandler = CharacterDataHandler
288+
289+
payload = "<a>x".encode(encoding)
290+
msg = re.escape("cannot call Parse() from within a handler")
291+
with self.assertRaisesRegex(RuntimeError, msg):
292+
for i in range(len(payload)):
293+
parser.Parse(payload[i:i+1], i == len(payload) - 1)
294+
CharacterDataHandler.assert_called_once_with("x")
295+
296+
@support.subTests("encoding", ("utf-8", "utf-16"))
297+
def test_parse_file_reentrancy_with_encoding(self, encoding):
298+
# See https://github.com/python/cpython/issues/146169.
299+
parser = expat.ParserCreate(encoding=encoding)
300+
301+
def CharacterDataHandler(data):
302+
return parser.ParseFile(BytesIO(data.encode(encoding)))
303+
CharacterDataHandler = mock.Mock(wraps=CharacterDataHandler)
304+
parser.CharacterDataHandler = CharacterDataHandler
305+
306+
payload = "<a>x".encode(encoding)
307+
payload_buffer = BytesIO(payload)
308+
msg = re.escape("cannot call ParseFile() from within a handler")
309+
with self.assertRaisesRegex(RuntimeError, msg):
310+
parser.ParseFile(payload_buffer)
311+
CharacterDataHandler.assert_called_once_with("x")
312+
313+
@support.subTests("encoding", ("utf-8", "utf-16"))
314+
def test_parse_reentrancy_allowed_for_external_parser(self, encoding):
315+
parser = expat.ParserCreate(encoding=encoding)
316+
subparser = parser.ExternalEntityParserCreate(None, encoding)
317+
payload_extstr = '<!ENTITY ext SYSTEM "entity.file">'
318+
319+
def ExternalEntityRefHandler(*args):
320+
subparser.Parse(payload_extstr, True)
321+
# return a nonzero integer to indicate that parsing continues
322+
return 1
323+
ExternalEntityRefHandler = mock.Mock(wraps=ExternalEntityRefHandler)
324+
parser.ExternalEntityRefHandler = ExternalEntityRefHandler
325+
326+
payload = textwrap.dedent(f"""\
327+
<?xml version="1.0" standalone="no"?>
328+
<!DOCTYPE quotations SYSTEM "quotations.dtd" [{payload_extstr}]>
329+
<root>&ext;</root>
330+
""").encode(encoding)
331+
332+
# Check that external parsers be called from parent's handlers.
333+
for i in range(len(payload)):
334+
parser.Parse(payload[i:i+1], i == len(payload) - 1)
335+
external_ref_args = ('ext', None, 'entity.file', None)
336+
ExternalEntityRefHandler.assert_called_once_with(*external_ref_args)
337+
338+
@support.subTests("encoding", ("utf-8", "utf-16"))
339+
def test_parse_file_reentrancy_allowed_for_external_parser(self, encoding):
340+
parser = expat.ParserCreate(encoding=encoding)
341+
subparser = parser.ExternalEntityParserCreate(None, encoding)
342+
payload_extstr = '<!ENTITY ext SYSTEM "entity.file">'
343+
344+
def ExternalEntityRefHandler(*args):
345+
subparser.ParseFile(BytesIO(payload_extstr.encode(encoding)))
346+
# return a nonzero integer to indicate that parsing continues
347+
return 1
348+
ExternalEntityRefHandler = mock.Mock(wraps=ExternalEntityRefHandler)
349+
parser.ExternalEntityRefHandler = ExternalEntityRefHandler
350+
351+
payload = textwrap.dedent(f"""\
352+
<?xml version="1.0" standalone="no"?>
353+
<!DOCTYPE quotations SYSTEM "quotations.dtd" [{payload_extstr}]>
354+
<root>&ext;</root>
355+
""").encode(encoding)
356+
357+
# Check that external parsers be called from parent's handlers.
358+
parser.ParseFile(BytesIO(payload))
359+
external_ref_args = ('ext', None, 'entity.file', None)
360+
ExternalEntityRefHandler.assert_called_once_with(*external_ref_args)
361+
279362
@support.subTests('encoding', [
280363
'utf-8', 'utf-16', 'utf-16be', 'utf-16le',
281364
'iso8859-1', 'iso8859-2', 'iso8859-3', 'iso8859-4', 'iso8859-5',
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`xml.parsers.expat`: raise :exc:`RuntimeError` when an Expat handler
2+
calls :meth:`parser.Parse <xml.parsers.expat.xmlparser.Parse>` or
3+
:meth:`parser.ParseFile <xml.parsers.expat.xmlparser.ParseFile>` on the parser
4+
that called the handler. Patch by Bénédikt Tran.

‎Modules/pyexpat.c‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,18 @@ VOID_HANDLER(StartDoctypeDecl,
790790

791791
VOID_HANDLER(EndDoctypeDecl, (void *userData), ("()"))
792792

793+
/* check that the current function is not called from within a handler */
794+
#define CHECK_NOT_IN_HANDLER(PARSER, FUNCNAME) \
795+
do { \
796+
if (PARSER->in_callback) { \
797+
PyErr_SetString(PyExc_RuntimeError, \
798+
"cannot call " FUNCNAME "() " \
799+
"from within a handler"); \
800+
return NULL; \
801+
} \
802+
} while (0)
803+
804+
793805
/* ---------------------------------------------------------------- */
794806
/*[clinic input]
795807
class pyexpat.xmlparser "xmlparseobject *" "&Xmlparsetype"
@@ -868,6 +880,9 @@ pyexpat_xmlparser_Parse_impl(xmlparseobject *self, PyTypeObject *cls,
868880
PyObject *data, int isfinal)
869881
/*[clinic end generated code: output=8faffe07fe1f862a input=f8f3ba4eadf837e5]*/
870882
{
883+
// avoid re-entrant calls to XML_Parse()
884+
CHECK_NOT_IN_HANDLER(self, "Parse");
885+
871886
const char *s;
872887
Py_ssize_t slen;
873888
Py_buffer view;
@@ -969,6 +984,9 @@ pyexpat_xmlparser_ParseFile_impl(xmlparseobject *self, PyTypeObject *cls,
969984
PyObject *file)
970985
/*[clinic end generated code: output=34780a094c8ca3ae input=c96030cf5e4f577e]*/
971986
{
987+
// avoid re-entrant calls to XML_GetBuffer() or XML_ParseBuffer()
988+
CHECK_NOT_IN_HANDLER(self, "ParseFile");
989+
972990
int rv = 1;
973991
PyObject *readmethod = NULL;
974992

0 commit comments

Comments
 (0)