https://github.com/python/cpython/commit/282a34e6fbfd4422727655e884c017c637ba2779
commit: 282a34e6fbfd4422727655e884c017c637ba2779
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-10-24T13:42:29-07:00
summary:

[3.14] gh-159739: Handle pyexpat configuration changes during callbacks 
(GH-160399) (#160474)



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.
(cherry picked from commit 0ba58e7e834c174b691dce58c9be9715477ea3a0)

Co-authored-by: Jelle Zijlstra <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-22-16-23-01.gh-issue-159739.XmlH3a.rst
M Lib/test/test_pyexpat.py
M Modules/pyexpat.c

diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index 3aa49f9fb6c484..564eeef358f643 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -821,6 +821,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'<r>a&#65;bc</r>', 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'<r>abc<s/></r>', 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 173b9b45afc6c5..61f3d1078bb71a 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);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to