https://github.com/python/cpython/commit/310c4454655357c39f32b4573227f523a0909305
commit: 310c4454655357c39f32b4573227f523a0909305
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: picnixz <[email protected]>
date: 2026-10-23T12:25:33-07:00
summary:

[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 dfd2c7cce8023995d3da9704318951c0d89f9681)

Co-authored-by: Bénédikt Tran <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-03-28-10-27-46.gh-issue-146169.RBF1xp.rst
M Lib/test/test_pyexpat.py
M Modules/pyexpat.c

diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index d1aa0a8f868eaa..97ed07d759c00c 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -275,6 +275,89 @@ def test_parse_again(self):
         self.assertEqual(expat.ErrorString(cm.exception.code),
                           expat.errors.XML_ERROR_FINISHED)
 
+    @support.subTests("encoding", ("utf-8", "utf-16"))
+    def test_parse_reentrancy_with_encoding(self, encoding):
+        # See https://github.com/python/cpython/issues/146169.
+        parser = expat.ParserCreate(encoding=encoding)
+
+        def CharacterDataHandler(data):
+            return parser.Parse(data, False)
+        CharacterDataHandler = mock.Mock(wraps=CharacterDataHandler)
+        parser.CharacterDataHandler = CharacterDataHandler
+
+        payload = "<a>x".encode(encoding)
+        msg = re.escape("cannot call Parse() from within a handler")
+        with self.assertRaisesRegex(RuntimeError, msg):
+            for i in range(len(payload)):
+                parser.Parse(payload[i:i+1], i == len(payload) - 1)
+        CharacterDataHandler.assert_called_once_with("x")
+
+    @support.subTests("encoding", ("utf-8", "utf-16"))
+    def test_parse_file_reentrancy_with_encoding(self, encoding):
+        # See https://github.com/python/cpython/issues/146169.
+        parser = expat.ParserCreate(encoding=encoding)
+
+        def CharacterDataHandler(data):
+            return parser.ParseFile(BytesIO(data.encode(encoding)))
+        CharacterDataHandler = mock.Mock(wraps=CharacterDataHandler)
+        parser.CharacterDataHandler = CharacterDataHandler
+
+        payload = "<a>x".encode(encoding)
+        payload_buffer = BytesIO(payload)
+        msg = re.escape("cannot call ParseFile() from within a handler")
+        with self.assertRaisesRegex(RuntimeError, msg):
+            parser.ParseFile(payload_buffer)
+        CharacterDataHandler.assert_called_once_with("x")
+
+    @support.subTests("encoding", ("utf-8", "utf-16"))
+    def test_parse_reentrancy_allowed_for_external_parser(self, encoding):
+        parser = expat.ParserCreate(encoding=encoding)
+        subparser = parser.ExternalEntityParserCreate(None, encoding)
+        payload_extstr = '<!ENTITY ext SYSTEM "entity.file">'
+
+        def ExternalEntityRefHandler(*args):
+            subparser.Parse(payload_extstr, True)
+            # return a nonzero integer to indicate that parsing continues
+            return 1
+        ExternalEntityRefHandler = mock.Mock(wraps=ExternalEntityRefHandler)
+        parser.ExternalEntityRefHandler = ExternalEntityRefHandler
+
+        payload = textwrap.dedent(f"""\
+            <?xml version="1.0" standalone="no"?>
+            <!DOCTYPE quotations SYSTEM "quotations.dtd" [{payload_extstr}]>
+            <root>&ext;</root>
+        """).encode(encoding)
+
+        # Check that external parsers be called from parent's handlers.
+        for i in range(len(payload)):
+            parser.Parse(payload[i:i+1], i == len(payload) - 1)
+        external_ref_args = ('ext', None, 'entity.file', None)
+        ExternalEntityRefHandler.assert_called_once_with(*external_ref_args)
+
+    @support.subTests("encoding", ("utf-8", "utf-16"))
+    def test_parse_file_reentrancy_allowed_for_external_parser(self, encoding):
+        parser = expat.ParserCreate(encoding=encoding)
+        subparser = parser.ExternalEntityParserCreate(None, encoding)
+        payload_extstr = '<!ENTITY ext SYSTEM "entity.file">'
+
+        def ExternalEntityRefHandler(*args):
+            subparser.ParseFile(BytesIO(payload_extstr.encode(encoding)))
+            # return a nonzero integer to indicate that parsing continues
+            return 1
+        ExternalEntityRefHandler = mock.Mock(wraps=ExternalEntityRefHandler)
+        parser.ExternalEntityRefHandler = ExternalEntityRefHandler
+
+        payload = textwrap.dedent(f"""\
+            <?xml version="1.0" standalone="no"?>
+            <!DOCTYPE quotations SYSTEM "quotations.dtd" [{payload_extstr}]>
+            <root>&ext;</root>
+        """).encode(encoding)
+
+        # Check that external parsers be called from parent's handlers.
+        parser.ParseFile(BytesIO(payload))
+        external_ref_args = ('ext', None, 'entity.file', None)
+        ExternalEntityRefHandler.assert_called_once_with(*external_ref_args)
+
     @support.subTests('encoding', [
         'utf-8', 'utf-16', 'utf-16be', 'utf-16le',
         'iso8859-1', 'iso8859-2', 'iso8859-3', 'iso8859-4', 'iso8859-5',
diff --git 
a/Misc/NEWS.d/next/Library/2026-03-28-10-27-46.gh-issue-146169.RBF1xp.rst 
b/Misc/NEWS.d/next/Library/2026-03-28-10-27-46.gh-issue-146169.RBF1xp.rst
new file mode 100644
index 00000000000000..d9460540a5a7d3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-03-28-10-27-46.gh-issue-146169.RBF1xp.rst
@@ -0,0 +1,4 @@
+:mod:`xml.parsers.expat`: raise :exc:`RuntimeError` when an Expat handler
+calls :meth:`parser.Parse <xml.parsers.expat.xmlparser.Parse>` or
+:meth:`parser.ParseFile <xml.parsers.expat.xmlparser.ParseFile>` on the parser
+that called the handler. Patch by Bénédikt Tran.
diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c
index f82325276c77c1..08acfb135c5b29 100644
--- a/Modules/pyexpat.c
+++ b/Modules/pyexpat.c
@@ -819,6 +819,18 @@ VOID_HANDLER(StartDoctypeDecl,
 
 VOID_HANDLER(EndDoctypeDecl, (void *userData), ("()"))
 
+/* check that the current function is not called from within a handler */
+#define CHECK_NOT_IN_HANDLER(PARSER, FUNCNAME)              \
+    do {                                                    \
+        if (PARSER->in_callback) {                          \
+            PyErr_SetString(PyExc_RuntimeError,             \
+                            "cannot call " FUNCNAME "() "   \
+                            "from within a handler");       \
+            return NULL;                                    \
+        }                                                   \
+    } while (0)
+
+
 /* ---------------------------------------------------------------- */
 /*[clinic input]
 class pyexpat.xmlparser "xmlparseobject *" "&Xmlparsetype"
@@ -899,6 +911,9 @@ pyexpat_xmlparser_Parse_impl(xmlparseobject *self, 
PyTypeObject *cls,
                              PyObject *data, int isfinal)
 /*[clinic end generated code: output=8faffe07fe1f862a input=f8f3ba4eadf837e5]*/
 {
+    // avoid re-entrant calls to XML_Parse()
+    CHECK_NOT_IN_HANDLER(self, "Parse");
+
     const char *s;
     Py_ssize_t slen;
     Py_buffer view;
@@ -1000,6 +1015,9 @@ pyexpat_xmlparser_ParseFile_impl(xmlparseobject *self, 
PyTypeObject *cls,
                                  PyObject *file)
 /*[clinic end generated code: output=34780a094c8ca3ae input=c96030cf5e4f577e]*/
 {
+    // avoid re-entrant calls to XML_GetBuffer() or XML_ParseBuffer()
+    CHECK_NOT_IN_HANDLER(self, "ParseFile");
+
     int rv = 1;
     PyObject *readmethod = NULL;
 

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to