Skip to content

Commit eb21dea

Browse files
authored
gh-161060: Fix pyexpat crash when the default handler is cleared (#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
1 parent f112215 commit eb21dea

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
@@ -834,6 +834,103 @@ def handler(text):
834834

835835
self.assertRaises(SpecificException, parser.Parse, xml.encode('iso8859'))
836836

837+
838+
class DefaultHandlerExceptionTest(unittest.TestCase):
839+
# In a document that is not UTF-8, Expat passes a long piece of text such
840+
# as this comment to the default handler in chunks of at most 1024 bytes,
841+
# re-reading the handler between chunks. Clearing the handler from within
842+
# the callback, by raising or by setting it to None, must not crash.
843+
xml = ("<?xml version='1.0' encoding='iso8859'?><s><!--%s--></s>"
844+
% ('a' * 2000)).encode('iso8859')
845+
846+
class SpecificException(Exception):
847+
pass
848+
849+
def check(self, name):
850+
def handler(text):
851+
if text.startswith('<!--'):
852+
raise self.SpecificException
853+
854+
parser = expat.ParserCreate()
855+
setattr(parser, name, handler)
856+
self.assertRaises(self.SpecificException, parser.Parse, self.xml, True)
857+
858+
def test_DefaultHandler(self):
859+
self.check('DefaultHandler')
860+
861+
def test_DefaultHandlerExpand(self):
862+
self.check('DefaultHandlerExpand')
863+
864+
def check_unset_in_callback(self, name):
865+
parser = expat.ParserCreate()
866+
calls = []
867+
def handler(text):
868+
calls.append(text)
869+
if text.startswith('<!--'):
870+
setattr(parser, name, None)
871+
setattr(parser, name, handler)
872+
parser.Parse(self.xml, True)
873+
self.assertTrue(calls[-1].startswith('<!--'), calls)
874+
875+
def test_unset_DefaultHandler_in_callback(self):
876+
self.check_unset_in_callback('DefaultHandler')
877+
878+
def test_unset_DefaultHandlerExpand_in_callback(self):
879+
self.check_unset_in_callback('DefaultHandlerExpand')
880+
881+
def check_unset_in_result_destructor(self, name):
882+
# The handler's result is released after the handler has returned,
883+
# but while Expat is still passing the comment to the default handler.
884+
parser = expat.ParserCreate()
885+
class Result:
886+
def __del__(self):
887+
setattr(parser, name, None)
888+
calls = []
889+
def handler(text):
890+
calls.append(text)
891+
if text.startswith('<!--'):
892+
return Result()
893+
setattr(parser, name, handler)
894+
parser.Parse(self.xml, True)
895+
self.assertTrue(calls[-1].startswith('<!--'), calls)
896+
897+
def test_unset_DefaultHandler_in_result_destructor(self):
898+
self.check_unset_in_result_destructor('DefaultHandler')
899+
900+
def test_unset_DefaultHandlerExpand_in_result_destructor(self):
901+
self.check_unset_in_result_destructor('DefaultHandlerExpand')
902+
903+
def test_unset_DefaultHandler_in_CharacterDataHandler_result(self):
904+
# With buffer_text, the CharacterDataHandler is called, and its
905+
# result released, just before the default handler is called.
906+
parser = expat.ParserCreate()
907+
class Result:
908+
def __del__(self):
909+
parser.DefaultHandler = None
910+
parser.buffer_text = True
911+
parser.CharacterDataHandler = lambda text: Result()
912+
parser.DefaultHandler = lambda text: None
913+
xml = ("<?xml version='1.0' encoding='iso8859'?><s>a<!--%s--></s>"
914+
% ('a' * 2000)).encode('iso8859')
915+
parser.Parse(xml, True)
916+
917+
def test_CharacterDataHandler_with_buffer_text(self):
918+
# With buffer_text, buffered character data is passed to the
919+
# CharacterDataHandler just before the default handler is called;
920+
# an exception there clears the handlers while Expat is still
921+
# passing the comment to the default handler.
922+
def handler(text):
923+
raise self.SpecificException
924+
925+
parser = expat.ParserCreate()
926+
parser.buffer_text = True
927+
parser.CharacterDataHandler = handler
928+
parser.DefaultHandler = lambda text: None
929+
xml = ("<?xml version='1.0' encoding='iso8859'?><s>a<!--%s--></s>"
930+
% ('a' * 2000)).encode('iso8859')
931+
self.assertRaises(self.SpecificException, parser.Parse, xml, True)
932+
933+
837934
class ChardataBufferTest(unittest.TestCase):
838935
"""
839936
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
@@ -293,10 +293,13 @@ error_external_entity_ref_handler(XML_Parser parser,
293293

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

@@ -579,14 +585,18 @@ my_ ## NAME ## Handler PARAMS { \
579585
PyObject *rv = call_with_frame( \
580586
#NAME, __LINE__, \
581587
self->handlers[NAME], args, self); \
582-
self->in_callback = 0; \
583588
Py_DECREF(args); \
584589
if (rv == NULL) { \
590+
self->in_callback = 0; \
585591
flag_error(self); \
586592
return RETURN_VARIABLE; \
587593
} \
588594
CONVERSION \
595+
/* Releasing the result can run arbitrary code, \
596+
which must still be treated as being within \
597+
the callback by the handler setter. */ \
589598
Py_DECREF(rv); \
599+
self->in_callback = 0; \
590600
if (PyErr_Occurred()) { \
591601
invalid_expat_handler_rv(#NAME); \
592602
} \
@@ -1838,8 +1848,14 @@ xmlparse_handler_setter(PyObject *op, PyObject *v, void *closure)
18381848
active, so Expat does a little more work than it
18391849
otherwise would, but that's really an odd case. A more
18401850
elaborate system of handlers and state could remove the
1841-
C handler more effectively. */
1842-
if (handlernum == CharacterData && self->in_callback) {
1851+
C handler more effectively. The same applies to the
1852+
default handler, which Expat can call several times for one
1853+
piece of text, such as a long comment in a document that is
1854+
not UTF-8. */
1855+
if (self->in_callback
1856+
&& (handlernum == CharacterData
1857+
|| handlernum == Default
1858+
|| handlernum == DefaultHandlerExpand)) {
18431859
c_handler = (xmlhandler)noop_character_data_handler;
18441860
}
18451861
v = NULL;
@@ -2786,7 +2802,17 @@ clear_handlers(xmlparseobject *self, int initial)
27862802
}
27872803
else {
27882804
Py_CLEAR(self->handlers[i]);
2789-
CALL_XML_HANDLER_SETTER(&handler_info[i], self->itself, NULL);
2805+
xmlhandler c_handler = NULL;
2806+
if (i == Default || i == DefaultHandlerExpand) {
2807+
/* This may be called from within a callback, where the
2808+
default handler can't be removed: for a document that is
2809+
not UTF-8, Expat passes a long piece of text to it in
2810+
several calls and re-reads the handler before each one
2811+
(reportDefault() in xmlparse.c). The no-op handler is
2812+
harmless otherwise. */
2813+
c_handler = (xmlhandler)noop_character_data_handler;
2814+
}
2815+
CALL_XML_HANDLER_SETTER(&handler_info[i], self->itself, c_handler);
27902816
}
27912817
}
27922818
}

0 commit comments

Comments
 (0)