diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index cace780f79f515..af3c7651ae96ac 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -276,6 +276,52 @@ 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) + + CharacterDataHandler = lambda data: parser.Parse(data, False) + CharacterDataHandler = mock.Mock(wraps=CharacterDataHandler) + def StartElementHandler(name, attrs): + parser.CharacterDataHandler = CharacterDataHandler + parser.StartElementHandler = StartElementHandler + + payload = "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_reentrancy_allowed_for_external_parser(self, encoding): + parser = expat.ParserCreate(encoding=encoding) + subparser = parser.ExternalEntityParserCreate(None, encoding) + payload_extstr = '' + + def ExternalEntityRefHandler(*args): + subparser.Parse(payload_extstr, True) + return 1 # return an integer to indicate that parsing continues + ExternalEntityRefHandler = mock.Mock(wraps=ExternalEntityRefHandler) + + def StartElementHandler(*args): + parser.ExternalEntityRefHandler = ExternalEntityRefHandler + parser.StartElementHandler = StartElementHandler + + payload = f"""\ + + +&ext; +""".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) + + class NamespaceSeparatorTest(unittest.TestCase): def test_legal(self): # Tests that make sure we get errors when the namespace_separator value 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..6274004cb47374 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-28-10-27-46.gh-issue-146169.RBF1xp.rst @@ -0,0 +1,3 @@ +:mod:`xml.parsers.expat`: raise :exc:`RuntimeError` when an Expat handler +calls :meth:`parser.Parse ` on the parser +that called the handler. Patch by Bénédikt Tran. diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 31b883fe8bd548..86dad1849ad5f2 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -863,6 +863,12 @@ pyexpat_xmlparser_Parse_impl(xmlparseobject *self, PyTypeObject *cls, int rc; pyexpat_state *state = PyType_GetModuleState(cls); + if (self->in_callback) { + PyErr_SetString(PyExc_RuntimeError, + "cannot call Parse() from within a handler"); + return NULL; + } + if (PyUnicode_Check(data)) { view.buf = NULL; s = PyUnicode_AsUTF8AndSize(data, &slen);