Skip to content

Commit dfd2c7c

Browse files
authored
gh-146169: correctly handle re-entrant parsing calls in Expat handlers (#146566)
1 parent fd657b0 commit dfd2c7c

3 files changed

Lines changed: 105 additions & 0 deletions

File tree

‎Lib/test/test_pyexpat.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,89 @@ def test_parse_again(self):
275275
self.assertEqual(expat.ErrorString(cm.exception.code),
276276
expat.errors.XML_ERROR_FINISHED)
277277

278+
@support.subTests("encoding", ("utf-8", "utf-16"))
279+
def test_parse_reentrancy_with_encoding(self, encoding):
280+
# See https://github.com/python/cpython/issues/146169.
281+
parser = expat.ParserCreate(encoding=encoding)
282+
283+
def CharacterDataHandler(data):
284+
return parser.Parse(data, False)
285+
CharacterDataHandler = mock.Mock(wraps=CharacterDataHandler)
286+
parser.CharacterDataHandler = CharacterDataHandler
287+
288+
payload = "<a>x".encode(encoding)
289+
msg = re.escape("cannot call Parse() from within a handler")
290+
with self.assertRaisesRegex(RuntimeError, msg):
291+
for i in range(len(payload)):
292+
parser.Parse(payload[i:i+1], i == len(payload) - 1)
293+
CharacterDataHandler.assert_called_once_with("x")
294+
295+
@support.subTests("encoding", ("utf-8", "utf-16"))
296+
def test_parse_file_reentrancy_with_encoding(self, encoding):
297+
# See https://github.com/python/cpython/issues/146169.
298+
parser = expat.ParserCreate(encoding=encoding)
299+
300+
def CharacterDataHandler(data):
301+
return parser.ParseFile(BytesIO(data.encode(encoding)))
302+
CharacterDataHandler = mock.Mock(wraps=CharacterDataHandler)
303+
parser.CharacterDataHandler = CharacterDataHandler
304+
305+
payload = "<a>x".encode(encoding)
306+
payload_buffer = BytesIO(payload)
307+
msg = re.escape("cannot call ParseFile() from within a handler")
308+
with self.assertRaisesRegex(RuntimeError, msg):
309+
parser.ParseFile(payload_buffer)
310+
CharacterDataHandler.assert_called_once_with("x")
311+
312+
@support.subTests("encoding", ("utf-8", "utf-16"))
313+
def test_parse_reentrancy_allowed_for_external_parser(self, encoding):
314+
parser = expat.ParserCreate(encoding=encoding)
315+
subparser = parser.ExternalEntityParserCreate(None, encoding)
316+
payload_extstr = '<!ENTITY ext SYSTEM "entity.file">'
317+
318+
def ExternalEntityRefHandler(*args):
319+
subparser.Parse(payload_extstr, True)
320+
# return a nonzero integer to indicate that parsing continues
321+
return 1
322+
ExternalEntityRefHandler = mock.Mock(wraps=ExternalEntityRefHandler)
323+
parser.ExternalEntityRefHandler = ExternalEntityRefHandler
324+
325+
payload = textwrap.dedent(f"""\
326+
<?xml version="1.0" standalone="no"?>
327+
<!DOCTYPE quotations SYSTEM "quotations.dtd" [{payload_extstr}]>
328+
<root>&ext;</root>
329+
""").encode(encoding)
330+
331+
# Check that external parsers be called from parent's handlers.
332+
for i in range(len(payload)):
333+
parser.Parse(payload[i:i+1], i == len(payload) - 1)
334+
external_ref_args = ('ext', None, 'entity.file', None)
335+
ExternalEntityRefHandler.assert_called_once_with(*external_ref_args)
336+
337+
@support.subTests("encoding", ("utf-8", "utf-16"))
338+
def test_parse_file_reentrancy_allowed_for_external_parser(self, encoding):
339+
parser = expat.ParserCreate(encoding=encoding)
340+
subparser = parser.ExternalEntityParserCreate(None, encoding)
341+
payload_extstr = '<!ENTITY ext SYSTEM "entity.file">'
342+
343+
def ExternalEntityRefHandler(*args):
344+
subparser.ParseFile(BytesIO(payload_extstr.encode(encoding)))
345+
# return a nonzero integer to indicate that parsing continues
346+
return 1
347+
ExternalEntityRefHandler = mock.Mock(wraps=ExternalEntityRefHandler)
348+
parser.ExternalEntityRefHandler = ExternalEntityRefHandler
349+
350+
payload = textwrap.dedent(f"""\
351+
<?xml version="1.0" standalone="no"?>
352+
<!DOCTYPE quotations SYSTEM "quotations.dtd" [{payload_extstr}]>
353+
<root>&ext;</root>
354+
""").encode(encoding)
355+
356+
# Check that external parsers be called from parent's handlers.
357+
parser.ParseFile(BytesIO(payload))
358+
external_ref_args = ('ext', None, 'entity.file', None)
359+
ExternalEntityRefHandler.assert_called_once_with(*external_ref_args)
360+
278361
@support.subTests('encoding', [
279362
# built-in Expat encodings
280363
'iso-8859-1', 'utf-8', 'utf-16', 'utf-16be', 'utf-16le',
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
@@ -820,6 +820,18 @@ VOID_HANDLER(StartDoctypeDecl,
820820

821821
VOID_HANDLER(EndDoctypeDecl, (void *userData), ("()"))
822822

823+
/* check that the current function is not called from within a handler */
824+
#define CHECK_NOT_IN_HANDLER(PARSER, FUNCNAME) \
825+
do { \
826+
if (PARSER->in_callback) { \
827+
PyErr_SetString(PyExc_RuntimeError, \
828+
"cannot call " FUNCNAME "() " \
829+
"from within a handler"); \
830+
return NULL; \
831+
} \
832+
} while (0)
833+
834+
823835
/* ---------------------------------------------------------------- */
824836
/*[clinic input]
825837
class pyexpat.xmlparser "xmlparseobject *" "&Xmlparsetype"
@@ -900,6 +912,9 @@ pyexpat_xmlparser_Parse_impl(xmlparseobject *self, PyTypeObject *cls,
900912
PyObject *data, int isfinal)
901913
/*[clinic end generated code: output=8faffe07fe1f862a input=f8f3ba4eadf837e5]*/
902914
{
915+
// avoid re-entrant calls to XML_Parse()
916+
CHECK_NOT_IN_HANDLER(self, "Parse");
917+
903918
const char *s;
904919
Py_ssize_t slen;
905920
Py_buffer view;
@@ -1001,6 +1016,9 @@ pyexpat_xmlparser_ParseFile_impl(xmlparseobject *self, PyTypeObject *cls,
10011016
PyObject *file)
10021017
/*[clinic end generated code: output=34780a094c8ca3ae input=c96030cf5e4f577e]*/
10031018
{
1019+
// avoid re-entrant calls to XML_GetBuffer() or XML_ParseBuffer()
1020+
CHECK_NOT_IN_HANDLER(self, "ParseFile");
1021+
10041022
int rv = 1;
10051023
PyObject *readmethod = NULL;
10061024

0 commit comments

Comments
 (0)