Skip to content

Commit 310c445

Browse files
[3.15] gh-146169: correctly handle re-entrant parsing calls in Expat handlers (GH-146566) (#160410)
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 c4f6007 commit 310c445

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
'utf-8', 'utf-16', 'utf-16be', 'utf-16le',
280363
'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
@@ -819,6 +819,18 @@ VOID_HANDLER(StartDoctypeDecl,
819819

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

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

0 commit comments

Comments
 (0)