Skip to content

Commit 1d22150

Browse files
committed
[3.14] 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 0fbe3bb)
1 parent 83b1e74 commit 1d22150

3 files changed

Lines changed: 138 additions & 10 deletions

File tree

‎Lib/test/test_pyexpat.py‎

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

746746
self.assertRaises(SpecificException, parser.Parse, xml.encode('iso8859'))
747747

748+
749+
class DefaultHandlerExceptionTest(unittest.TestCase):
750+
# In a document that is not UTF-8, Expat passes a long piece of text such
751+
# as this comment to the default handler in chunks of at most 1024 bytes,
752+
# re-reading the handler between chunks. Clearing the handler from within
753+
# the callback, by raising or by setting it to None, must not crash.
754+
xml = ("<?xml version='1.0' encoding='iso8859'?><s><!--%s--></s>"
755+
% ('a' * 2000)).encode('iso8859')
756+
757+
class SpecificException(Exception):
758+
pass
759+
760+
def check(self, name):
761+
def handler(text):
762+
if text.startswith('<!--'):
763+
raise self.SpecificException
764+
765+
parser = expat.ParserCreate()
766+
setattr(parser, name, handler)
767+
self.assertRaises(self.SpecificException, parser.Parse, self.xml, True)
768+
769+
def test_DefaultHandler(self):
770+
self.check('DefaultHandler')
771+
772+
def test_DefaultHandlerExpand(self):
773+
self.check('DefaultHandlerExpand')
774+
775+
def check_unset_in_callback(self, name):
776+
parser = expat.ParserCreate()
777+
calls = []
778+
def handler(text):
779+
calls.append(text)
780+
if text.startswith('<!--'):
781+
setattr(parser, name, None)
782+
setattr(parser, name, handler)
783+
parser.Parse(self.xml, True)
784+
self.assertTrue(calls[-1].startswith('<!--'), calls)
785+
786+
def test_unset_DefaultHandler_in_callback(self):
787+
self.check_unset_in_callback('DefaultHandler')
788+
789+
def test_unset_DefaultHandlerExpand_in_callback(self):
790+
self.check_unset_in_callback('DefaultHandlerExpand')
791+
792+
def check_unset_in_result_destructor(self, name):
793+
# The handler's result is released after the handler has returned,
794+
# but while Expat is still passing the comment to the default handler.
795+
parser = expat.ParserCreate()
796+
class Result:
797+
def __del__(self):
798+
setattr(parser, name, None)
799+
calls = []
800+
def handler(text):
801+
calls.append(text)
802+
if text.startswith('<!--'):
803+
return Result()
804+
setattr(parser, name, handler)
805+
parser.Parse(self.xml, True)
806+
self.assertTrue(calls[-1].startswith('<!--'), calls)
807+
808+
def test_unset_DefaultHandler_in_result_destructor(self):
809+
self.check_unset_in_result_destructor('DefaultHandler')
810+
811+
def test_unset_DefaultHandlerExpand_in_result_destructor(self):
812+
self.check_unset_in_result_destructor('DefaultHandlerExpand')
813+
814+
def test_unset_DefaultHandler_in_CharacterDataHandler_result(self):
815+
# With buffer_text, the CharacterDataHandler is called, and its
816+
# result released, just before the default handler is called.
817+
parser = expat.ParserCreate()
818+
class Result:
819+
def __del__(self):
820+
parser.DefaultHandler = None
821+
parser.buffer_text = True
822+
parser.CharacterDataHandler = lambda text: Result()
823+
parser.DefaultHandler = lambda text: None
824+
xml = ("<?xml version='1.0' encoding='iso8859'?><s>a<!--%s--></s>"
825+
% ('a' * 2000)).encode('iso8859')
826+
parser.Parse(xml, True)
827+
828+
def test_CharacterDataHandler_with_buffer_text(self):
829+
# With buffer_text, buffered character data is passed to the
830+
# CharacterDataHandler just before the default handler is called;
831+
# an exception there clears the handlers while Expat is still
832+
# passing the comment to the default handler.
833+
def handler(text):
834+
raise self.SpecificException
835+
836+
parser = expat.ParserCreate()
837+
parser.buffer_text = True
838+
parser.CharacterDataHandler = handler
839+
parser.DefaultHandler = lambda text: None
840+
xml = ("<?xml version='1.0' encoding='iso8859'?><s>a<!--%s--></s>"
841+
% ('a' * 2000)).encode('iso8859')
842+
self.assertRaises(self.SpecificException, parser.Parse, xml, True)
843+
844+
748845
class ChardataBufferTest(unittest.TestCase):
749846
"""
750847
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: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,13 @@ error_external_entity_ref_handler(XML_Parser parser,
288288

289289
/* Dummy character data handler used when an error (exception) has
290290
been detected, and the actual parsing can be terminated early.
291-
This is needed since character data handler can't be safely removed
292-
from within the character data handler, but can be replaced. It is
293-
used only from the character data handler trampoline, and must be
294-
used right after `flag_error()` is called. */
291+
This is needed since the character data and default handlers can't be
292+
safely removed from within a callback, only replaced: Expat may call
293+
them several times for one piece of text (a long comment in a document
294+
that is not UTF-8, for example). The default handler has the same
295+
signature, so this function serves for both. clear_handlers() installs
296+
it after `flag_error()`, and the handler setter installs it when a
297+
handler is set to None from within a callback. */
295298
static void
296299
noop_character_data_handler(void *userData, const XML_Char *data, int len)
297300
{
@@ -377,15 +380,18 @@ call_character_handler(xmlparseobject *self, const XML_Char *buffer, int len)
377380
temp = call_with_frame("CharacterData", __LINE__,
378381
self->handlers[CharacterData], args, self);
379382
/* temp is an owned reference again, or NULL */
380-
self->in_callback = 0;
381383
Py_DECREF(args);
382384
if (temp == NULL) {
385+
self->in_callback = 0;
383386
flag_error(self);
384387
XML_SetCharacterDataHandler(self->itself,
385388
noop_character_data_handler);
386389
return -1;
387390
}
391+
/* Releasing the result can run arbitrary code, which must still be
392+
treated as being within the callback by the handler setter. */
388393
Py_DECREF(temp);
394+
self->in_callback = 0;
389395
return 0;
390396
}
391397

@@ -552,14 +558,18 @@ my_ ## NAME ## Handler PARAMS { \
552558
PyObject *rv = call_with_frame( \
553559
#NAME, __LINE__, \
554560
self->handlers[NAME], args, self); \
555-
self->in_callback = 0; \
556561
Py_DECREF(args); \
557562
if (rv == NULL) { \
563+
self->in_callback = 0; \
558564
flag_error(self); \
559565
return RETURN_VARIABLE; \
560566
} \
561567
CONVERSION \
568+
/* Releasing the result can run arbitrary code, \
569+
which must still be treated as being within \
570+
the callback by the handler setter. */ \
562571
Py_DECREF(rv); \
572+
self->in_callback = 0; \
563573
return RETURN_VARIABLE; \
564574
}
565575

@@ -1680,9 +1690,15 @@ xmlparse_handler_setter(PyObject *op, PyObject *v, void *closure)
16801690
active, so Expat does a little more work than it
16811691
otherwise would, but that's really an odd case. A more
16821692
elaborate system of handlers and state could remove the
1683-
C handler more effectively. */
1684-
if (handlernum == CharacterData && self->in_callback) {
1685-
c_handler = noop_character_data_handler;
1693+
C handler more effectively. The same applies to the
1694+
default handler, which Expat can call several times for one
1695+
piece of text, such as a long comment in a document that is
1696+
not UTF-8. */
1697+
if (self->in_callback
1698+
&& (handlernum == CharacterData
1699+
|| handlernum == Default
1700+
|| handlernum == DefaultHandlerExpand)) {
1701+
c_handler = (xmlhandler)noop_character_data_handler;
16861702
}
16871703
v = NULL;
16881704
}
@@ -2562,7 +2578,17 @@ clear_handlers(xmlparseobject *self, int initial)
25622578
}
25632579
else {
25642580
Py_CLEAR(self->handlers[i]);
2565-
CALL_XML_HANDLER_SETTER(&handler_info[i], self->itself, NULL);
2581+
xmlhandler c_handler = NULL;
2582+
if (i == Default || i == DefaultHandlerExpand) {
2583+
/* This may be called from within a callback, where the
2584+
default handler can't be removed: for a document that is
2585+
not UTF-8, Expat passes a long piece of text to it in
2586+
several calls and re-reads the handler before each one
2587+
(reportDefault() in xmlparse.c). The no-op handler is
2588+
harmless otherwise. */
2589+
c_handler = (xmlhandler)noop_character_data_handler;
2590+
}
2591+
CALL_XML_HANDLER_SETTER(&handler_info[i], self->itself, c_handler);
25662592
}
25672593
}
25682594
}

0 commit comments

Comments
 (0)