Skip to content

Commit 0fbe3bb

Browse files
[3.15] gh-161060: Fix pyexpat crash when the default handler is cleared (GH-161061) (#161842)
gh-161060: Fix pyexpat crash when the default handler is cleared (GH-161061) Expat calls the default handler once per 1024-byte piece of a token that needs encoding conversion. Install a no-op instead of NULL when the handler is cleared after an error or unset from within a callback, as is already done for the character data handler. ANT-2026-0SJB3WWC (cherry picked from commit eb21dea) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
1 parent 88d137a commit 0fbe3bb

3 files changed

Lines changed: 137 additions & 9 deletions

File tree

‎Lib/test/test_pyexpat.py‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,103 @@ def handler(text):
785785

786786
self.assertRaises(SpecificException, parser.Parse, xml.encode('iso8859'))
787787

788+
789+
class DefaultHandlerExceptionTest(unittest.TestCase):
790+
# In a document that is not UTF-8, Expat passes a long piece of text such
791+
# as this comment to the default handler in chunks of at most 1024 bytes,
792+
# re-reading the handler between chunks. Clearing the handler from within
793+
# the callback, by raising or by setting it to None, must not crash.
794+
xml = ("<?xml version='1.0' encoding='iso8859'?><s><!--%s--></s>"
795+
% ('a' * 2000)).encode('iso8859')
796+
797+
class SpecificException(Exception):
798+
pass
799+
800+
def check(self, name):
801+
def handler(text):
802+
if text.startswith('<!--'):
803+
raise self.SpecificException
804+
805+
parser = expat.ParserCreate()
806+
setattr(parser, name, handler)
807+
self.assertRaises(self.SpecificException, parser.Parse, self.xml, True)
808+
809+
def test_DefaultHandler(self):
810+
self.check('DefaultHandler')
811+
812+
def test_DefaultHandlerExpand(self):
813+
self.check('DefaultHandlerExpand')
814+
815+
def check_unset_in_callback(self, name):
816+
parser = expat.ParserCreate()
817+
calls = []
818+
def handler(text):
819+
calls.append(text)
820+
if text.startswith('<!--'):
821+
setattr(parser, name, None)
822+
setattr(parser, name, handler)
823+
parser.Parse(self.xml, True)
824+
self.assertTrue(calls[-1].startswith('<!--'), calls)
825+
826+
def test_unset_DefaultHandler_in_callback(self):
827+
self.check_unset_in_callback('DefaultHandler')
828+
829+
def test_unset_DefaultHandlerExpand_in_callback(self):
830+
self.check_unset_in_callback('DefaultHandlerExpand')
831+
832+
def check_unset_in_result_destructor(self, name):
833+
# The handler's result is released after the handler has returned,
834+
# but while Expat is still passing the comment to the default handler.
835+
parser = expat.ParserCreate()
836+
class Result:
837+
def __del__(self):
838+
setattr(parser, name, None)
839+
calls = []
840+
def handler(text):
841+
calls.append(text)
842+
if text.startswith('<!--'):
843+
return Result()
844+
setattr(parser, name, handler)
845+
parser.Parse(self.xml, True)
846+
self.assertTrue(calls[-1].startswith('<!--'), calls)
847+
848+
def test_unset_DefaultHandler_in_result_destructor(self):
849+
self.check_unset_in_result_destructor('DefaultHandler')
850+
851+
def test_unset_DefaultHandlerExpand_in_result_destructor(self):
852+
self.check_unset_in_result_destructor('DefaultHandlerExpand')
853+
854+
def test_unset_DefaultHandler_in_CharacterDataHandler_result(self):
855+
# With buffer_text, the CharacterDataHandler is called, and its
856+
# result released, just before the default handler is called.
857+
parser = expat.ParserCreate()
858+
class Result:
859+
def __del__(self):
860+
parser.DefaultHandler = None
861+
parser.buffer_text = True
862+
parser.CharacterDataHandler = lambda text: Result()
863+
parser.DefaultHandler = lambda text: None
864+
xml = ("<?xml version='1.0' encoding='iso8859'?><s>a<!--%s--></s>"
865+
% ('a' * 2000)).encode('iso8859')
866+
parser.Parse(xml, True)
867+
868+
def test_CharacterDataHandler_with_buffer_text(self):
869+
# With buffer_text, buffered character data is passed to the
870+
# CharacterDataHandler just before the default handler is called;
871+
# an exception there clears the handlers while Expat is still
872+
# passing the comment to the default handler.
873+
def handler(text):
874+
raise self.SpecificException
875+
876+
parser = expat.ParserCreate()
877+
parser.buffer_text = True
878+
parser.CharacterDataHandler = handler
879+
parser.DefaultHandler = lambda text: None
880+
xml = ("<?xml version='1.0' encoding='iso8859'?><s>a<!--%s--></s>"
881+
% ('a' * 2000)).encode('iso8859')
882+
self.assertRaises(self.SpecificException, parser.Parse, xml, True)
883+
884+
788885
class ChardataBufferTest(unittest.TestCase):
789886
"""
790887
test setting of chardata buffer size
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a crash in :mod:`xml.parsers.expat` when a handler raises an exception,
2+
or the :meth:`~xml.parsers.expat.xmlparser.DefaultHandler` or
3+
:meth:`~xml.parsers.expat.xmlparser.DefaultHandlerExpand` handler is unset,
4+
while a token longer than 1024 bytes, such as a comment, is being reported to
5+
the default handler from a document that is not encoded in UTF-8.

‎Modules/pyexpat.c‎

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,13 @@ error_external_entity_ref_handler(XML_Parser parser,
292292

293293
/* Dummy character data handler used when an error (exception) has
294294
been detected, and the actual parsing can be terminated early.
295-
This is needed since character data handler can't be safely removed
296-
from within the character data handler, but can be replaced. It is
297-
used only from the character data handler trampoline, and must be
298-
used right after `flag_error()` is called. */
295+
This is needed since the character data and default handlers can't be
296+
safely removed from within a callback, only replaced: Expat may call
297+
them several times for one piece of text (a long comment in a document
298+
that is not UTF-8, for example). The default handler has the same
299+
signature, so this function serves for both. clear_handlers() installs
300+
it after `flag_error()`, and the handler setter installs it when a
301+
handler is set to None from within a callback. */
299302
static void
300303
noop_character_data_handler(void *userData, const XML_Char *data, int len)
301304
{
@@ -381,15 +384,18 @@ call_character_handler(xmlparseobject *self, const XML_Char *buffer, int len)
381384
temp = call_with_frame("CharacterData", __LINE__,
382385
self->handlers[CharacterData], args, self);
383386
/* temp is an owned reference again, or NULL */
384-
self->in_callback = 0;
385387
Py_DECREF(args);
386388
if (temp == NULL) {
389+
self->in_callback = 0;
387390
flag_error(self);
388391
XML_SetCharacterDataHandler(self->itself,
389392
noop_character_data_handler);
390393
return -1;
391394
}
395+
/* Releasing the result can run arbitrary code, which must still be
396+
treated as being within the callback by the handler setter. */
392397
Py_DECREF(temp);
398+
self->in_callback = 0;
393399
return 0;
394400
}
395401

@@ -578,14 +584,18 @@ my_ ## NAME ## Handler PARAMS { \
578584
PyObject *rv = call_with_frame( \
579585
#NAME, __LINE__, \
580586
self->handlers[NAME], args, self); \
581-
self->in_callback = 0; \
582587
Py_DECREF(args); \
583588
if (rv == NULL) { \
589+
self->in_callback = 0; \
584590
flag_error(self); \
585591
return RETURN_VARIABLE; \
586592
} \
587593
CONVERSION \
594+
/* Releasing the result can run arbitrary code, \
595+
which must still be treated as being within \
596+
the callback by the handler setter. */ \
588597
Py_DECREF(rv); \
598+
self->in_callback = 0; \
589599
if (PyErr_Occurred()) { \
590600
invalid_expat_handler_rv(#NAME); \
591601
} \
@@ -1724,8 +1734,14 @@ xmlparse_handler_setter(PyObject *op, PyObject *v, void *closure)
17241734
active, so Expat does a little more work than it
17251735
otherwise would, but that's really an odd case. A more
17261736
elaborate system of handlers and state could remove the
1727-
C handler more effectively. */
1728-
if (handlernum == CharacterData && self->in_callback) {
1737+
C handler more effectively. The same applies to the
1738+
default handler, which Expat can call several times for one
1739+
piece of text, such as a long comment in a document that is
1740+
not UTF-8. */
1741+
if (self->in_callback
1742+
&& (handlernum == CharacterData
1743+
|| handlernum == Default
1744+
|| handlernum == DefaultHandlerExpand)) {
17291745
c_handler = (xmlhandler)noop_character_data_handler;
17301746
}
17311747
v = NULL;
@@ -2607,7 +2623,17 @@ clear_handlers(xmlparseobject *self, int initial)
26072623
}
26082624
else {
26092625
Py_CLEAR(self->handlers[i]);
2610-
CALL_XML_HANDLER_SETTER(&handler_info[i], self->itself, NULL);
2626+
xmlhandler c_handler = NULL;
2627+
if (i == Default || i == DefaultHandlerExpand) {
2628+
/* This may be called from within a callback, where the
2629+
default handler can't be removed: for a document that is
2630+
not UTF-8, Expat passes a long piece of text to it in
2631+
several calls and re-reads the handler before each one
2632+
(reportDefault() in xmlparse.c). The no-op handler is
2633+
harmless otherwise. */
2634+
c_handler = (xmlhandler)noop_character_data_handler;
2635+
}
2636+
CALL_XML_HANDLER_SETTER(&handler_info[i], self->itself, c_handler);
26112637
}
26122638
}
26132639
}

0 commit comments

Comments
 (0)