Skip to content

Commit 0ab39af

Browse files
[3.15] gh-159739: Handle pyexpat configuration changes during callbacks (GH-160399) (#160475)
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 0ba58e7) Co-authored-by: Jelle Zijlstra <jelle@openai.com>
1 parent 17e09a1 commit 0ab39af

3 files changed

Lines changed: 77 additions & 29 deletions

File tree

‎Lib/test/test_pyexpat.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,34 @@ def test_disabling_buffer(self):
861861
parser.Parse(xml3, True)
862862
self.assertEqual(self.n, 12)
863863

864+
def test_disable_buffer_in_character_handler(self):
865+
parser = expat.ParserCreate()
866+
parser.buffer_size = 2
867+
parser.buffer_text = True
868+
seen = []
869+
870+
def consume(text):
871+
seen.append(text)
872+
if len(seen) == 1:
873+
parser.buffer_text = False
874+
875+
parser.CharacterDataHandler = consume
876+
parser.Parse(b'<r>a&#65;bc</r>', True)
877+
self.assertEqual(''.join(seen), 'aAbc')
878+
879+
def test_clear_element_handler_during_flush(self):
880+
parser = expat.ParserCreate()
881+
parser.buffer_text = True
882+
elements = []
883+
884+
def consume(text):
885+
parser.StartElementHandler = None
886+
887+
parser.CharacterDataHandler = consume
888+
parser.StartElementHandler = lambda name, attrs: elements.append(name)
889+
parser.Parse(b'<r>abc<s/></r>', True)
890+
self.assertEqual(elements, ['r'])
891+
864892
def counting_handler(self, text):
865893
self.n += 1
866894

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix crashes in :mod:`xml.parsers.expat` when a character data callback
2+
disables buffering or clears another handler during a flush, or when interning
3+
an attribute name changes :attr:`~xml.parsers.expat.xmlparser.ordered_attributes`.

‎Modules/pyexpat.c‎

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,9 @@ call_with_frame(const char *funcname, int lineno, PyObject* func, PyObject* args
316316
{
317317
PyObject *res;
318318

319+
Py_INCREF(func);
319320
res = PyObject_Call(func, args, NULL);
321+
Py_DECREF(func);
320322
if (res == NULL) {
321323
_PyTraceback_Add(funcname, __FILE__, lineno);
322324
XML_StopParser(self->itself, XML_FALSE);
@@ -356,19 +358,25 @@ call_character_handler(xmlparseobject *self, const XML_Char *buffer, int len)
356358
if (!have_handler(self, CharacterData))
357359
return -1;
358360

359-
args = PyTuple_New(1);
360-
if (args == NULL)
361-
return -1;
362-
temp = (conv_string_len_to_unicode(buffer, len));
361+
/* Copy the buffer before tuple allocation can trigger a GC callback. */
362+
temp = conv_string_len_to_unicode(buffer, len);
363363
if (temp == NULL) {
364-
Py_DECREF(args);
365364
flag_error(self);
366365
XML_SetCharacterDataHandler(self->itself,
367366
noop_character_data_handler);
368367
return -1;
369368
}
369+
args = PyTuple_New(1);
370+
if (args == NULL) {
371+
Py_DECREF(temp);
372+
return -1;
373+
}
370374
PyTuple_SET_ITEM(args, 0, temp);
371375
/* temp is now a borrowed reference; consider it unused. */
376+
if (!have_handler(self, CharacterData)) {
377+
Py_DECREF(args);
378+
return 0;
379+
}
372380
self->in_callback = 1;
373381
temp = call_with_frame("CharacterData", __LINE__,
374382
self->handlers[CharacterData], args, self);
@@ -388,12 +396,13 @@ call_character_handler(xmlparseobject *self, const XML_Char *buffer, int len)
388396
static int
389397
flush_character_buffer(xmlparseobject *self)
390398
{
391-
int rc;
392399
if (self->buffer == NULL || self->buffer_used == 0)
393400
return 0;
394-
rc = call_character_handler(self, self->buffer, self->buffer_used);
401+
int len = self->buffer_used;
402+
/* The callback may change buffering or install a new handler, which
403+
also flushes. Do not expose the same pending data a second time. */
395404
self->buffer_used = 0;
396-
return rc;
405+
return call_character_handler(self, self->buffer, len);
397406
}
398407

399408
static void
@@ -404,27 +413,22 @@ my_CharacterDataHandler(void *userData, const XML_Char *data, int len)
404413
if (PyErr_Occurred())
405414
return;
406415

407-
if (self->buffer == NULL)
416+
if (self->buffer != NULL &&
417+
len > (self->buffer_size - self->buffer_used)) {
418+
if (flush_character_buffer(self) < 0)
419+
return;
420+
/* The callback may replace the handler or disable buffering. */
421+
if (!have_handler(self, CharacterData))
422+
return;
423+
}
424+
if (self->buffer == NULL ||
425+
len > (self->buffer_size - self->buffer_used)) {
408426
call_character_handler(self, data, len);
427+
}
409428
else {
410-
if (len > (self->buffer_size - self->buffer_used)) {
411-
if (flush_character_buffer(self) < 0)
412-
return;
413-
/* handler might have changed; drop the rest on the floor
414-
* if there isn't a handler anymore
415-
*/
416-
if (!have_handler(self, CharacterData))
417-
return;
418-
}
419-
if (len > self->buffer_size) {
420-
call_character_handler(self, data, len);
421-
self->buffer_used = 0;
422-
}
423-
else {
424-
memcpy(self->buffer + self->buffer_used,
425-
data, len * sizeof(XML_Char));
426-
self->buffer_used += len;
427-
}
429+
memcpy(self->buffer + self->buffer_used,
430+
data, len * sizeof(XML_Char));
431+
self->buffer_used += len;
428432
}
429433
}
430434

@@ -455,7 +459,9 @@ my_StartElementHandler(void *userData,
455459
max += 2;
456460
}
457461
/* Build the container. */
458-
if (self->ordered_attributes)
462+
/* Intern-dictionary comparisons can change the mode mid-loop. */
463+
int ordered_attributes = self->ordered_attributes;
464+
if (ordered_attributes)
459465
container = PyList_New(max);
460466
else
461467
container = PyDict_New();
@@ -478,7 +484,7 @@ my_StartElementHandler(void *userData,
478484
Py_DECREF(n);
479485
return;
480486
}
481-
if (self->ordered_attributes) {
487+
if (ordered_attributes) {
482488
PyList_SET_ITEM(container, i, n);
483489
PyList_SET_ITEM(container, i+1, v);
484490
}
@@ -504,6 +510,10 @@ my_StartElementHandler(void *userData,
504510
return;
505511
}
506512
/* Container is now a borrowed reference; ignore it. */
513+
if (!have_handler(self, StartElement)) {
514+
Py_DECREF(args);
515+
return;
516+
}
507517
self->in_callback = 1;
508518
rv = call_with_frame("StartElement", __LINE__,
509519
self->handlers[StartElement], args, self);
@@ -560,6 +570,10 @@ my_ ## NAME ## Handler PARAMS { \
560570
flag_error(self); \
561571
return RETURN_VARIABLE; \
562572
} \
573+
if (!have_handler(self, NAME)) { \
574+
Py_DECREF(args); \
575+
return RETURN_VARIABLE; \
576+
} \
563577
self->in_callback = 1; \
564578
PyObject *rv = call_with_frame( \
565579
#NAME, __LINE__, \
@@ -708,6 +722,9 @@ my_ElementDeclHandler(void *userData,
708722
flag_error(self);
709723
goto finally;
710724
}
725+
if (!have_handler(self, ElementDecl)) {
726+
goto finally;
727+
}
711728
self->in_callback = 1;
712729
rv = call_with_frame("ElementDecl", __LINE__,
713730
self->handlers[ElementDecl], args, self);

0 commit comments

Comments
 (0)