diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py index c0a45764cc47ba..2996260916ac13 100644 --- a/Lib/test/test_pyexpat.py +++ b/Lib/test/test_pyexpat.py @@ -738,6 +738,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 18068f526c5314..5b46bef5ae1c2f 100644 --- a/Modules/pyexpat.c +++ b/Modules/pyexpat.c @@ -312,7 +312,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); @@ -352,19 +354,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); @@ -384,12 +392,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 @@ -400,27 +409,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; } } @@ -451,7 +455,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(); @@ -474,7 +480,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); } @@ -500,6 +506,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); @@ -534,6 +544,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__, \ @@ -679,6 +693,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);