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);