From 55c3a8aa2f7b06151aa0e460f629518402bd2ea5 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 22 Oct 2026 16:25:05 -0700 Subject: [PATCH] gh-159739: Handle pyexpat configuration changes during callbacks A character data callback can disable buffering or remove the next event handler during a flush. Interning an attribute name can also change ordered_attributes while pyexpat is filling the attribute container. Using the freed buffer, cleared handler, or wrong container type can crash the interpreter. Clear the buffered length before dispatch, keep the active handler alive, recheck the buffer and handlers after callbacks, and use one attribute mode for the whole start event. --- Lib/test/test_pyexpat.py | 28 +++++++ ...-10-22-16-23-01.gh-issue-159739.XmlH3a.rst | 3 + Modules/pyexpat.c | 75 ++++++++++++------- 3 files changed, 77 insertions(+), 29 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-22-16-23-01.gh-issue-159739.XmlH3a.rst diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index 2c4048f4c9f560..c3ad5345d89b44 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -827,6 +827,34 @@ def test_disabling_buffer(self): parser.Parse(xml3, True) self.assertEqual(self.n, 12) + def test_disable_buffer_in_character_handler(self): + parser = expat.ParserCreate() + parser.buffer_size = 2 + parser.buffer_text = True + seen = [] + + def consume(text): + seen.append(text) + if len(seen) == 1: + parser.buffer_text = False + + parser.CharacterDataHandler = consume + parser.Parse(b'aAbc', True) + self.assertEqual(''.join(seen), 'aAbc') + + def test_clear_element_handler_during_flush(self): + parser = expat.ParserCreate() + parser.buffer_text = True + elements = [] + + def consume(text): + parser.StartElementHandler = None + + parser.CharacterDataHandler = consume + parser.StartElementHandler = lambda name, attrs: elements.append(name) + parser.Parse(b'abc', True) + self.assertEqual(elements, ['r']) + def counting_handler(self, text): self.n += 1 diff --git a/Misc/NEWS.d/next/Library/2026-10-22-16-23-01.gh-issue-159739.XmlH3a.rst b/Misc/NEWS.d/next/Library/2026-10-22-16-23-01.gh-issue-159739.XmlH3a.rst new file mode 100644 index 00000000000000..bda00a7f01f276 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-16-23-01.gh-issue-159739.XmlH3a.rst @@ -0,0 +1,3 @@ +Fix crashes in :mod:`xml.parsers.expat` when a character data callback +disables buffering or clears another handler during a flush, or when interning +an attribute name changes :attr:`~xml.parsers.expat.xmlparser.ordered_attributes`. diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c index 00fbe579ec1c5a..9c8c04aa4ffa17 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -317,7 +317,9 @@ call_with_frame(const char *funcname, int lineno, PyObject* func, PyObject* args { PyObject *res; + Py_INCREF(func); res = PyObject_Call(func, args, NULL); + Py_DECREF(func); if (res == NULL) { _PyTraceback_Add(funcname, __FILE__, lineno); XML_StopParser(self->itself, XML_FALSE); @@ -357,19 +359,25 @@ call_character_handler(xmlparseobject *self, const XML_Char *buffer, int len) if (!have_handler(self, CharacterData)) return -1; - args = PyTuple_New(1); - if (args == NULL) - return -1; - temp = (conv_string_len_to_unicode(buffer, len)); + /* Copy the buffer before tuple allocation can trigger a GC callback. */ + temp = conv_string_len_to_unicode(buffer, len); if (temp == NULL) { - Py_DECREF(args); flag_error(self); XML_SetCharacterDataHandler(self->itself, noop_character_data_handler); return -1; } + args = PyTuple_New(1); + if (args == NULL) { + Py_DECREF(temp); + return -1; + } PyTuple_SET_ITEM(args, 0, temp); /* temp is now a borrowed reference; consider it unused. */ + if (!have_handler(self, CharacterData)) { + Py_DECREF(args); + return 0; + } self->in_callback = 1; temp = call_with_frame("CharacterData", __LINE__, self->handlers[CharacterData], args, self); @@ -389,12 +397,13 @@ call_character_handler(xmlparseobject *self, const XML_Char *buffer, int len) static int flush_character_buffer(xmlparseobject *self) { - int rc; if (self->buffer == NULL || self->buffer_used == 0) return 0; - rc = call_character_handler(self, self->buffer, self->buffer_used); + int len = self->buffer_used; + /* The callback may change buffering or install a new handler, which + also flushes. Do not expose the same pending data a second time. */ self->buffer_used = 0; - return rc; + return call_character_handler(self, self->buffer, len); } static void @@ -405,27 +414,22 @@ my_CharacterDataHandler(void *userData, const XML_Char *data, int len) if (PyErr_Occurred()) return; - if (self->buffer == NULL) + if (self->buffer != NULL && + len > (self->buffer_size - self->buffer_used)) { + if (flush_character_buffer(self) < 0) + return; + /* The callback may replace the handler or disable buffering. */ + if (!have_handler(self, CharacterData)) + return; + } + if (self->buffer == NULL || + len > (self->buffer_size - self->buffer_used)) { call_character_handler(self, data, len); + } else { - if (len > (self->buffer_size - self->buffer_used)) { - if (flush_character_buffer(self) < 0) - return; - /* handler might have changed; drop the rest on the floor - * if there isn't a handler anymore - */ - if (!have_handler(self, CharacterData)) - return; - } - if (len > self->buffer_size) { - call_character_handler(self, data, len); - self->buffer_used = 0; - } - else { - memcpy(self->buffer + self->buffer_used, - data, len * sizeof(XML_Char)); - self->buffer_used += len; - } + memcpy(self->buffer + self->buffer_used, + data, len * sizeof(XML_Char)); + self->buffer_used += len; } } @@ -456,7 +460,9 @@ my_StartElementHandler(void *userData, max += 2; } /* Build the container. */ - if (self->ordered_attributes) + /* Intern-dictionary comparisons can change the mode mid-loop. */ + int ordered_attributes = self->ordered_attributes; + if (ordered_attributes) container = PyList_New(max); else container = PyDict_New(); @@ -479,7 +485,7 @@ my_StartElementHandler(void *userData, Py_DECREF(n); return; } - if (self->ordered_attributes) { + if (ordered_attributes) { PyList_SET_ITEM(container, i, n); PyList_SET_ITEM(container, i+1, v); } @@ -505,6 +511,10 @@ my_StartElementHandler(void *userData, return; } /* Container is now a borrowed reference; ignore it. */ + if (!have_handler(self, StartElement)) { + Py_DECREF(args); + return; + } self->in_callback = 1; rv = call_with_frame("StartElement", __LINE__, self->handlers[StartElement], args, self); @@ -561,6 +571,10 @@ my_ ## NAME ## Handler PARAMS { \ flag_error(self); \ return RETURN_VARIABLE; \ } \ + if (!have_handler(self, NAME)) { \ + Py_DECREF(args); \ + return RETURN_VARIABLE; \ + } \ self->in_callback = 1; \ PyObject *rv = call_with_frame( \ #NAME, __LINE__, \ @@ -709,6 +723,9 @@ my_ElementDeclHandler(void *userData, flag_error(self); goto finally; } + if (!have_handler(self, ElementDecl)) { + goto finally; + } self->in_callback = 1; rv = call_with_frame("ElementDecl", __LINE__, self->handlers[ElementDecl], args, self);