|
| 1 | +From 18fded65fbf6833e9c942c6ae9102a6b1e047d52 Mon Sep 17 00:00:00 2001 |
| 2 | +From: Stan Ulbrych <stan@python.org> |
| 3 | +Date: Wed, 8 Apr 2026 11:27:42 +0100 |
| 4 | +Subject: [PATCH] gh-145986: Avoid unbound C recursion in `conv_content_model` |
| 5 | + in `pyexpat.c` (CVE 2026-4224) (GH-145987) (#146002) |
| 6 | +MIME-Version: 1.0 |
| 7 | +Content-Type: text/plain; charset=UTF-8 |
| 8 | +Content-Transfer-Encoding: 8bit |
| 9 | + |
| 10 | +* [3.10] gh-145986: Avoid unbound C recursion in `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987) |
| 11 | + |
| 12 | +Fix C stack overflow (CVE-2026-4224) when an Expat parser |
| 13 | +with a registered `ElementDeclHandler` parses inline DTD |
| 14 | +containing deeply nested content model. |
| 15 | + |
| 16 | +--------- |
| 17 | +(cherry picked from commit eb0e8be3a7e11b87d198a2c3af1ed0eccf532768) |
| 18 | +(cherry picked from commit e5caf45faac74b0ed869e3336420cffd3510ce6e) |
| 19 | + |
| 20 | +Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> |
| 21 | +Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> |
| 22 | + |
| 23 | +* Update Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst |
| 24 | + |
| 25 | +--------- |
| 26 | + |
| 27 | +Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> |
| 28 | +Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> |
| 29 | +Upstream-reference: https://github.com/python/cpython/commit/af856a7177326ac25d9f66cc6dd28b554d914fee.patch |
| 30 | +--- |
| 31 | + Lib/test/test_pyexpat.py | 19 +++++++++++++++++++ |
| 32 | + .../2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst | 4 ++++ |
| 33 | + Modules/pyexpat.c | 8 +++++++- |
| 34 | + 3 files changed, 30 insertions(+), 1 deletion(-) |
| 35 | + create mode 100644 Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst |
| 36 | + |
| 37 | +diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py |
| 38 | +index 5212c7a..95d85bb 100644 |
| 39 | +--- a/Lib/test/test_pyexpat.py |
| 40 | ++++ b/Lib/test/test_pyexpat.py |
| 41 | +@@ -8,10 +8,12 @@ import sys |
| 42 | + import sysconfig |
| 43 | + import unittest |
| 44 | + import traceback |
| 45 | ++import textwrap |
| 46 | + |
| 47 | + from xml.parsers import expat |
| 48 | + from xml.parsers.expat import errors |
| 49 | + |
| 50 | ++from test import support |
| 51 | + from test.support import sortdict |
| 52 | + |
| 53 | + |
| 54 | +@@ -643,6 +645,23 @@ class ChardataBufferTest(unittest.TestCase): |
| 55 | + parser.Parse(xml2, True) |
| 56 | + self.assertEqual(self.n, 4) |
| 57 | + |
| 58 | ++class ElementDeclHandlerTest(unittest.TestCase): |
| 59 | ++ def test_deeply_nested_content_model(self): |
| 60 | ++ # This should raise a RecursionError and not crash. |
| 61 | ++ # See https://github.com/python/cpython/issues/145986. |
| 62 | ++ N = 500_000 |
| 63 | ++ data = ( |
| 64 | ++ b'<!DOCTYPE root [\n<!ELEMENT root ' |
| 65 | ++ + b'(a, ' * N + b'a' + b')' * N |
| 66 | ++ + b'>\n]>\n<root/>\n' |
| 67 | ++ ) |
| 68 | ++ |
| 69 | ++ parser = expat.ParserCreate() |
| 70 | ++ parser.ElementDeclHandler = lambda _1, _2: None |
| 71 | ++ with support.infinite_recursion(): |
| 72 | ++ with self.assertRaises(RecursionError): |
| 73 | ++ parser.Parse(data) |
| 74 | ++ |
| 75 | + class MalformedInputTest(unittest.TestCase): |
| 76 | + def test1(self): |
| 77 | + xml = b"\0\r\n" |
| 78 | +diff --git a/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst |
| 79 | +new file mode 100644 |
| 80 | +index 0000000..cb9dbad |
| 81 | +--- /dev/null |
| 82 | ++++ b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst |
| 83 | +@@ -0,0 +1,4 @@ |
| 84 | ++:mod:`xml.parsers.expat`: Fixed a crash caused by unbounded C recursion when |
| 85 | ++converting deeply nested XML content models with |
| 86 | ++:meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`. |
| 87 | ++This addresses `CVE-2026-4224 <https://www.cve.org/CVERecord?id=CVE-2026-4224>`_. |
| 88 | +diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c |
| 89 | +index e0c3e10..60a2561 100644 |
| 90 | +--- a/Modules/pyexpat.c |
| 91 | ++++ b/Modules/pyexpat.c |
| 92 | +@@ -515,6 +515,10 @@ static PyObject * |
| 93 | + conv_content_model(XML_Content * const model, |
| 94 | + PyObject *(*conv_string)(const XML_Char *)) |
| 95 | + { |
| 96 | ++ if (Py_EnterRecursiveCall(" in conv_content_model")) { |
| 97 | ++ return NULL; |
| 98 | ++ } |
| 99 | ++ |
| 100 | + PyObject *result = NULL; |
| 101 | + PyObject *children = PyTuple_New(model->numchildren); |
| 102 | + int i; |
| 103 | +@@ -526,7 +530,7 @@ conv_content_model(XML_Content * const model, |
| 104 | + conv_string); |
| 105 | + if (child == NULL) { |
| 106 | + Py_XDECREF(children); |
| 107 | +- return NULL; |
| 108 | ++ goto done; |
| 109 | + } |
| 110 | + PyTuple_SET_ITEM(children, i, child); |
| 111 | + } |
| 112 | +@@ -534,6 +538,8 @@ conv_content_model(XML_Content * const model, |
| 113 | + model->type, model->quant, |
| 114 | + conv_string,model->name, children); |
| 115 | + } |
| 116 | ++done: |
| 117 | ++ Py_LeaveRecursiveCall(); |
| 118 | + return result; |
| 119 | + } |
| 120 | + |
| 121 | +-- |
| 122 | +2.45.4 |
| 123 | + |
0 commit comments