On EL 9, also supports reparse deferral in expat

This replaces one downstream-only patch with another.
Yet arguably, this is the safer thing to do.

Read the patch description for details; won't copy paste them here.

Also, require expat >= version-release when expat < 2.7.4.

Assisted-By: Claude Opus 4.6
(cherry picked from python3.13 commit 0ca3f59c07cb0d9bf01a0746d00773ed0c625755)
This commit is contained in:
Miro Hrončok 2026-08-12 17:31:43 +02:00
commit 9d987df726
4 changed files with 160 additions and 86 deletions

View file

@ -0,0 +1,133 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz>
Date: Wed, 12 Aug 2026 15:18:39 +0200
Subject: 00466: Downstream only: Lower XML_COMBINED_VERSION threshold for
reparse deferral
RHEL 9 expat 2.5.0 has XML_SetReparseDeferralEnabled backported
via the CVE-2023-52425 fix, but XML_COMBINED_VERSION remains 20500.
CPython's #if XML_COMBINED_VERSION >= 20600 guards compile the setter
as a no-op, so SetReparseDeferralEnabled silently does nothing and
GetReparseDeferralEnabled always returns False, even though the expat
library actually supports (and enables) reparse deferral.
Lower the threshold from 20600 to 20500 so that CPython uses the
backported function. This makes the Python API actually work on RHEL 9
and fixes test failures (test_reparse_deferral_disabled,
test_flush_reparse_deferral_disabled, test_simple_xml_chunk_*).
The spec file BuildRequires expat-devel >= 2.5.0-2 to ensure the
backported function is available.
---
Lib/test/test_pyexpat.py | 6 +++---
Lib/test/test_sax.py | 4 ++--
Lib/test/test_xml_etree.py | 4 ++--
Modules/pyexpat.c | 6 +++---
4 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index fd3077063b..34f86e2dbd 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -1005,7 +1005,7 @@ def test_error_path_no_crash(self):
class ReparseDeferralTest(unittest.TestCase):
def test_getter_setter_round_trip(self):
parser = expat.ParserCreate()
- enabled = (expat.version_info >= (2, 6, 0))
+ enabled = (expat.version_info >= (2, 5, 0))
self.assertIs(parser.GetReparseDeferralEnabled(), enabled)
parser.SetReparseDeferralEnabled(False)
@@ -1014,7 +1014,7 @@ def test_getter_setter_round_trip(self):
self.assertIs(parser.GetReparseDeferralEnabled(), enabled)
def test_reparse_deferral_enabled(self):
- if expat.version_info < (2, 6, 0):
+ if expat.version_info < (2, 5, 0):
self.skipTest(f'Expat {expat.version_info} does not '
'support reparse deferral')
@@ -1045,7 +1045,7 @@ def start_element(name, _):
parser = expat.ParserCreate()
parser.StartElementHandler = start_element
- if expat.version_info >= (2, 6, 0):
+ if expat.version_info >= (2, 5, 0):
parser.SetReparseDeferralEnabled(False)
self.assertFalse(parser.GetReparseDeferralEnabled())
diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py
index 5c10bcedc6..114f002b0b 100644
--- a/Lib/test/test_sax.py
+++ b/Lib/test/test_sax.py
@@ -1216,7 +1216,7 @@ def test_expat_incremental_reset(self):
self.assertEqual(result.getvalue(), start + b"<doc>text</doc>")
- @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
+ @unittest.skipIf(pyexpat.version_info < (2, 5, 0),
f'Expat {pyexpat.version_info} does not '
'support reparse deferral')
def test_flush_reparse_deferral_enabled(self):
@@ -1250,7 +1250,7 @@ def test_flush_reparse_deferral_disabled(self):
for chunk in ("<doc", ">"):
parser.feed(chunk)
- if pyexpat.version_info >= (2, 6, 0):
+ if pyexpat.version_info >= (2, 5, 0):
parser._parser.SetReparseDeferralEnabled(False)
self.assertEqual(result.getvalue(), start) # i.e. no elements started
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 6da7c34dc4..d846ddf4e2 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -1803,7 +1803,7 @@ def test_unknown_event(self):
with self.assertRaisesRegex(ValueError, "unknown event 'bogus'"):
ET.XMLPullParser(events=(x.decode() for x in (b'start', b'end', b'bogus')))
- @unittest.skipIf(pyexpat.version_info < (2, 6, 0),
+ @unittest.skipIf(pyexpat.version_info < (2, 5, 0),
f'Expat {pyexpat.version_info} does not '
'support reparse deferral')
def test_flush_reparse_deferral_enabled(self):
@@ -1833,7 +1833,7 @@ def test_flush_reparse_deferral_disabled(self):
for chunk in ("<doc", ">"):
parser.feed(chunk)
- if pyexpat.version_info >= (2, 6, 0):
+ if pyexpat.version_info >= (2, 5, 0):
if not ET is pyET:
self.skipTest(f'XMLParser.(Get|Set)ReparseDeferralEnabled '
'methods not available in C')
diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c
index f9fd7a7a5b..e88b7f16a8 100644
--- a/Modules/pyexpat.c
+++ b/Modules/pyexpat.c
@@ -814,7 +814,7 @@ pyexpat_xmlparser_SetReparseDeferralEnabled_impl(xmlparseobject *self,
int enabled)
/*[clinic end generated code: output=5ec539e3b63c8c49 input=021eb9e0bafc32c5]*/
{
-#if XML_COMBINED_VERSION >= 20600
+#if XML_COMBINED_VERSION >= 20500
XML_SetReparseDeferralEnabled(self->itself, enabled ? XML_TRUE : XML_FALSE);
self->reparse_deferral_enabled = (bool)enabled;
#endif
@@ -1478,7 +1478,7 @@ newxmlparseobject(pyexpat_state *state, const char *encoding,
self->ns_prefixes = 0;
self->handlers = NULL;
self->intern = Py_XNewRef(intern);
-#if XML_COMBINED_VERSION >= 20600
+#if XML_COMBINED_VERSION >= 20500
self->reparse_deferral_enabled = true;
#else
self->reparse_deferral_enabled = false;
@@ -2396,7 +2396,7 @@ pyexpat_exec(PyObject *mod)
#else
capi->SetHashSalt16Bytes = NULL;
#endif
-#if XML_COMBINED_VERSION >= 20600
+#if XML_COMBINED_VERSION >= 20500
capi->SetReparseDeferralEnabled = XML_SetReparseDeferralEnabled;
#else
capi->SetReparseDeferralEnabled = NULL;

View file

@ -1,69 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Karolina Surma <ksurma@redhat.com>
Date: Tue, 24 Jun 2025 11:12:13 +0200
Subject: 00466: Downstream only: Skip tests not working with older expat
version
We want to run these tests in Fedora and EPEL 10, but not in EPEL 9,
which has too old version of expat. We set the upper bound version
in the conditionalized skip to a release available in CentOS Stream 10,
which is tested as working.
---
Lib/test/test_pyexpat.py | 2 ++
Lib/test/test_sax.py | 2 ++
Lib/test/test_xml_etree.py | 6 ++++++
3 files changed, 10 insertions(+)
diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py
index fd3077063b..6e6468cb7d 100644
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -1037,6 +1037,8 @@ def start_element(name, _):
self.assertEqual(started, ['doc'])
+ @unittest.skipIf(expat.version_info < (2, 7, 1),
+ f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_reparse_deferral_disabled(self):
started = []
diff --git a/Lib/test/test_sax.py b/Lib/test/test_sax.py
index 5c10bcedc6..1fd7a273b5 100644
--- a/Lib/test/test_sax.py
+++ b/Lib/test/test_sax.py
@@ -1241,6 +1241,8 @@ def test_flush_reparse_deferral_enabled(self):
self.assertEqual(result.getvalue(), start + b"<doc></doc>")
+ @unittest.skipIf(pyexpat.version_info < (2, 7, 1),
+ f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_flush_reparse_deferral_disabled(self):
result = BytesIO()
xmlgen = XMLGenerator(result)
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 6da7c34dc4..e9912cb569 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -1598,9 +1598,13 @@ def test_simple_xml(self, chunk_size=None, flush=False):
self.assert_event_tags(parser, [('end', 'root')])
self.assertIsNone(parser.close())
+ @unittest.skipIf(pyexpat.version_info < (2, 7, 1),
+ f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_simple_xml_chunk_1(self):
self.test_simple_xml(chunk_size=1, flush=True)
+ @unittest.skipIf(pyexpat.version_info < (2, 7, 1),
+ f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_simple_xml_chunk_5(self):
self.test_simple_xml(chunk_size=5, flush=True)
@@ -1827,6 +1831,8 @@ def test_flush_reparse_deferral_enabled(self):
self.assert_event_tags(parser, [('end', 'doc')])
+ @unittest.skipIf(pyexpat.version_info < (2, 7, 1),
+ f"Skip for expat < 2.7.1 (version available in RHEL 10)")
def test_flush_reparse_deferral_disabled(self):
parser = ET.XMLPullParser(events=('start', 'end'))

View file

@ -24,12 +24,11 @@ discover:
test: "PYTHON=python${pybasever}d TOX=false VERSION=${pybasever} CYTHON=true ./venv.sh"
- name: selftest
path: /selftest
# test_subparser_inherits_reparse_deferral fails on RHEL 9: https://github.com/python/cpython/issues/155485
test: "VERSION=${pybasever} X='-i test_check_probes -i test_subparser_inherits_reparse_deferral' ./parallel.sh"
test: "VERSION=${pybasever} X='-i test_check_probes' ./parallel.sh"
- name: debugtest
path: /selftest
# test_base_interpreter: https://github.com/python/cpython/issues/131372
test: "VERSION=${pybasever} PYTHON=python${pybasever}d X='-i test_check_probes -i test_base_interpreter -i test_subparser_inherits_reparse_deferral' ./parallel.sh"
test: "VERSION=${pybasever} PYTHON=python${pybasever}d X='-i test_check_probes -i test_base_interpreter' ./parallel.sh"
- name: freethreadingtest
path: /selftest
test: "VERSION=${pybasever}t X='-i test_check_probes -i test_base_interpreter' ./parallel.sh"

View file

@ -49,7 +49,7 @@ URL: https://www.python.org/
#global prerel ...
%global upstream_version %{general_version}%{?prerel}
Version: %{general_version}%{?prerel:~%{prerel}}
Release: 1%{?dist}
Release: 2%{?dist}
License: Python-2.0.1
@ -261,7 +261,7 @@ Obsoletes: python%{pybasever}%{?1:-%{1}}\
BuildRequires: autoconf
BuildRequires: bluez-libs-devel
BuildRequires: bzip2-devel
BuildRequires: expat-devel
BuildRequires: expat-devel >= 2.5.0-2
BuildRequires: findutils
BuildRequires: gcc
BuildRequires: gdbm-devel
@ -392,14 +392,24 @@ Source34: Python-%{upstream_version}-x86_64-optimized-jit_stencils.h
# pypa/distutils integration: https://github.com/pypa/distutils/pull/70
Patch251: 00251-change-user-install-location.patch
# 00466 # e10760fb955ee33d2917f8a57bb4e24d71e5341c
# Downstream only: Skip tests not working with older expat version
# 00466 # 713a1368544eddd55088d67f88a23ce31722a4cb
# Downstream only: Lower XML_COMBINED_VERSION threshold for reparse deferral
#
# We want to run these tests in Fedora and EPEL 10, but not in EPEL 9,
# which has too old version of expat. We set the upper bound version
# in the conditionalized skip to a release available in CentOS Stream 10,
# which is tested as working.
Patch466: 00466-downstream-only-skip-tests-not-working-with-older-expat-version.patch
# RHEL 9 expat 2.5.0 has XML_SetReparseDeferralEnabled backported
# via the CVE-2023-52425 fix, but XML_COMBINED_VERSION remains 20500.
# CPython's #if XML_COMBINED_VERSION >= 20600 guards compile the setter
# as a no-op, so SetReparseDeferralEnabled silently does nothing and
# GetReparseDeferralEnabled always returns False, even though the expat
# library actually supports (and enables) reparse deferral.
#
# Lower the threshold from 20600 to 20500 so that CPython uses the
# backported function. This makes the Python API actually work on RHEL 9
# and fixes test failures (test_reparse_deferral_disabled,
# test_flush_reparse_deferral_disabled, test_simple_xml_chunk_*).
#
# The spec file BuildRequires expat-devel >= 2.5.0-2 to ensure the
# backported function is available.
Patch466: 00466-downstream-only-lower-xml_combined_version-threshold-for-reparse-deferral.patch
# 00475 # 91e12ebfb2a88b265f3764a0d852b6fa53b2386a
# CVE-2025-15367
@ -619,10 +629,12 @@ Requires: tzdata
# This breaks many things, including python -m venv.
# We avoid this problem by requiring at least the same version of expat that
# was used during the build time.
# We also include release, in case pyxpat uses ABI that was backported
# (e.g. XML_SetReparseDeferralEnabled was added in c9s expat 2.5.0-2).
# Other subpackages (like -debug) also need this, but they all depend on -libs.
# Since expat 2.7.4, the library has versioned symbols and this is no longer needed,
# as the generated requirement will be in the form of libexpat.so.1(LIBEXPAT_2.7.2) etc.
%global expat_version %(LANG=C rpm -q --qf '%%{version}' expat.%{_target_cpu} | sed 's/.*not installed/0/')
%global expat_version %(LANG=C rpm -q --qf '%%{version}-%%{release}' expat.%{_target_cpu} | sed 's/.*not installed/0/')
%if v"%{expat_version}" < v"2.7.4"
Requires: expat%{?_isa} >= %{expat_version}
%endif
@ -1485,7 +1497,6 @@ CheckPython() {
# test_check_probes is failing since it was introduced in 3.12.0rc1,
# the test is skipped until it is fixed in upstream.
# see: https://github.com/python/cpython/issues/104280#issuecomment-1669249980
# test_subparser_inherits_reparse_deferral: https://github.com/python/cpython/issues/155485
LD_LIBRARY_PATH=$ConfDir $ConfDir/python -m test.regrtest \
-wW --slowest %{_smp_mflags} \
%ifarch riscv64
@ -1494,9 +1505,6 @@ CheckPython() {
--timeout=2700 \
%endif
-i test_check_probes \
%if 0%{?rhel} == 9
-i test_subparser_inherits_reparse_deferral \
%endif
echo FINISHED: CHECKING OF PYTHON FOR CONFIGURATION: $ConfName
@ -1986,6 +1994,9 @@ CheckPython freethreading
# ======================================================
%changelog
* Tue Aug 18 2026 Miro Hrončok <mhroncok@redhat.com> - 3.14.7-2
- On RHEL 9, also supports reparse deferral in expat
* Mon Aug 10 2026 Karolina Surma <ksurma@redhat.com> - 3.14.7-1
- Update to Python 3.14.7