11 Commits

Author SHA256 Message Date
efa8febd12 Accepting request 1197437 from devel:languages:python:Factory
- Add CVE-2024-8088-inf-loop-zipfile_Path.patch to prevent
  malformed payload to cause infinite loops in zipfile.Path
  (bsc#1229704, CVE-2024-8088).

OBS-URL: https://build.opensuse.org/request/show/1197437
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python310?expand=0&rev=48
2024-08-30 11:29:22 +00:00
59c649b520 Update patch
OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=143
2024-08-29 12:14:10 +00:00
8a73c83002 - Add CVE-2024-8088-inf-loop-zipfile_Path.patch to prevent
malformed payload to cause infinite loops in zipfile.Path
  (bsc#1229704, CVE-2024-8088).

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=142
2024-08-29 12:04:00 +00:00
db9a09b8c2 Accepting request 1192675 from devel:languages:python:Factory
- Add CVE-2024-6923-email-hdr-inject.patch to prevent email
  header injection due to unquoted newlines (bsc#1228780,
  CVE-2024-6923).
- Adding bso1227999-reproducible-builds.patch fixing bsc#1227999
  adding reproducibility patches from gh#python/cpython!121872
  and gh#python/cpython!121883.
- %{profileopt} variable is set according to the variable
  %{do_profiling} (bsc#1227999)
- Update bluez-devel-vendor.tar.xz

OBS-URL: https://build.opensuse.org/request/show/1192675
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python310?expand=0&rev=47
2024-08-10 17:06:06 +00:00
1716dfe088 - Adding bso1227999-reproducible-builds.patch fixing bsc#1227999
adding reproducibility patches from gh#python/cpython!121872
  and gh#python/cpython!121883.

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=140
2024-08-07 20:30:36 +00:00
e761be5380 - Add CVE-2024-6923-email-hdr-inject.patch to prevent email
header injection due to unquoted newlines (bsc#1228780,
  CVE-2024-6923).
- %{profileopt} variable is set according to the variable
  %{do_profiling} (bsc#1227999)
- Update bluez-devel-vendor.tar.xz

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=139
2024-08-07 15:06:12 +00:00
47f6e77cdd Accepting request 1189131 from devel:languages:python:Factory
- Remove %suse_update_desktop_file macro as it is not useful any
  more.

- Stop using %%defattr, it seems to be breaking proper executable
  attributes on /usr/bin/ scripts (bsc#1227378).

OBS-URL: https://build.opensuse.org/request/show/1189131
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python310?expand=0&rev=46
2024-07-24 13:33:10 +00:00
8a3ae3ab7b - Remove %suse_update_desktop_file macro as it is not useful any
more.

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=137
2024-07-22 21:25:49 +00:00
c2077eafb7 - Stop using %%defattr, it seems to be breaking proper executable
attributes on /usr/bin/ scripts (bsc#1227378).

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=136
2024-07-15 12:15:29 +00:00
3d3eac0a3e Accepting request 1185398 from devel:languages:python:Factory
OBS-URL: https://build.opensuse.org/request/show/1185398
OBS-URL: https://build.opensuse.org/package/show/openSUSE:Factory/python310?expand=0&rev=45
2024-07-05 17:45:12 +00:00
f4d7952bf7 - Update F00251-change-user-install-location.patch to make pip and
modern tools install directly in /usr/local when used by the user.
  bsc#1225660

OBS-URL: https://build.opensuse.org/package/show/devel:languages:python:Factory/python310?expand=0&rev=134
2024-07-04 13:17:05 +00:00
32 changed files with 1962 additions and 2672 deletions

1
.gitattributes vendored
View File

@@ -21,4 +21,3 @@
*.xz filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*.changes merge=merge-changes

4
.gitignore vendored
View File

@@ -1,5 +1 @@
.osc
*.obscpio
_build*
.pbuild
python310-*-build/

View File

@@ -0,0 +1,461 @@
---
Doc/library/email.utils.rst | 19 -
Lib/email/utils.py | 151 +++++++-
Lib/test/test_email/test_email.py | 187 +++++++++-
Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst | 8
4 files changed, 344 insertions(+), 21 deletions(-)
--- a/Doc/library/email.utils.rst
+++ b/Doc/library/email.utils.rst
@@ -60,13 +60,18 @@ of the new API.
begins with angle brackets, they are stripped off.
-.. function:: parseaddr(address)
+.. function:: parseaddr(address, *, strict=True)
Parse address -- which should be the value of some address-containing field such
as :mailheader:`To` or :mailheader:`Cc` -- into its constituent *realname* and
*email address* parts. Returns a tuple of that information, unless the parse
fails, in which case a 2-tuple of ``('', '')`` is returned.
+ If *strict* is true, use a strict parser which rejects malformed inputs.
+
+ .. versionchanged:: 3.13
+ Add *strict* optional parameter and reject malformed inputs by default.
+
.. function:: formataddr(pair, charset='utf-8')
@@ -84,12 +89,15 @@ of the new API.
Added the *charset* option.
-.. function:: getaddresses(fieldvalues)
+.. function:: getaddresses(fieldvalues, *, strict=True)
This method returns a list of 2-tuples of the form returned by ``parseaddr()``.
*fieldvalues* is a sequence of header field values as might be returned by
- :meth:`Message.get_all <email.message.Message.get_all>`. Here's a simple
- example that gets all the recipients of a message::
+ :meth:`Message.get_all <email.message.Message.get_all>`.
+
+ If *strict* is true, use a strict parser which rejects malformed inputs.
+
+ Here's a simple example that gets all the recipients of a message::
from email.utils import getaddresses
@@ -99,6 +107,9 @@ of the new API.
resent_ccs = msg.get_all('resent-cc', [])
all_recipients = getaddresses(tos + ccs + resent_tos + resent_ccs)
+ .. versionchanged:: 3.13
+ Add *strict* optional parameter and reject malformed inputs by default.
+
.. function:: parsedate(date)
--- a/Lib/email/utils.py
+++ b/Lib/email/utils.py
@@ -48,6 +48,7 @@ TICK = "'"
specialsre = re.compile(r'[][\\()<>@,:;".]')
escapesre = re.compile(r'[\\"]')
+
def _has_surrogates(s):
"""Return True if s contains surrogate-escaped binary data."""
# This check is based on the fact that unless there are surrogates, utf8
@@ -106,12 +107,127 @@ def formataddr(pair, charset='utf-8'):
return address
+def _iter_escaped_chars(addr):
+ pos = 0
+ escape = False
+ for pos, ch in enumerate(addr):
+ if escape:
+ yield (pos, '\\' + ch)
+ escape = False
+ elif ch == '\\':
+ escape = True
+ else:
+ yield (pos, ch)
+ if escape:
+ yield (pos, '\\')
+
+
+def _strip_quoted_realnames(addr):
+ """Strip real names between quotes."""
+ if '"' not in addr:
+ # Fast path
+ return addr
+
+ start = 0
+ open_pos = None
+ result = []
+ for pos, ch in _iter_escaped_chars(addr):
+ if ch == '"':
+ if open_pos is None:
+ open_pos = pos
+ else:
+ if start != open_pos:
+ result.append(addr[start:open_pos])
+ start = pos + 1
+ open_pos = None
-def getaddresses(fieldvalues):
- """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
- all = COMMASPACE.join(str(v) for v in fieldvalues)
- a = _AddressList(all)
- return a.addresslist
+ if start < len(addr):
+ result.append(addr[start:])
+
+ return ''.join(result)
+
+
+supports_strict_parsing = True
+
+def getaddresses(fieldvalues, *, strict=True):
+ """Return a list of (REALNAME, EMAIL) or ('','') for each fieldvalue.
+
+ When parsing fails for a fieldvalue, a 2-tuple of ('', '') is returned in
+ its place.
+
+ If strict is true, use a strict parser which rejects malformed inputs.
+ """
+
+ # If strict is true, if the resulting list of parsed addresses is greater
+ # than the number of fieldvalues in the input list, a parsing error has
+ # occurred and consequently a list containing a single empty 2-tuple [('',
+ # '')] is returned in its place. This is done to avoid invalid output.
+ #
+ # Malformed input: getaddresses(['alice@example.com <bob@example.com>'])
+ # Invalid output: [('', 'alice@example.com'), ('', 'bob@example.com')]
+ # Safe output: [('', '')]
+
+ if not strict:
+ all = COMMASPACE.join(str(v) for v in fieldvalues)
+ a = _AddressList(all)
+ return a.addresslist
+
+ fieldvalues = [str(v) for v in fieldvalues]
+ fieldvalues = _pre_parse_validation(fieldvalues)
+ addr = COMMASPACE.join(fieldvalues)
+ a = _AddressList(addr)
+ result = _post_parse_validation(a.addresslist)
+
+ # Treat output as invalid if the number of addresses is not equal to the
+ # expected number of addresses.
+ n = 0
+ for v in fieldvalues:
+ # When a comma is used in the Real Name part it is not a deliminator.
+ # So strip those out before counting the commas.
+ v = _strip_quoted_realnames(v)
+ # Expected number of addresses: 1 + number of commas
+ n += 1 + v.count(',')
+ if len(result) != n:
+ return [('', '')]
+
+ return result
+
+
+def _check_parenthesis(addr):
+ # Ignore parenthesis in quoted real names.
+ addr = _strip_quoted_realnames(addr)
+
+ opens = 0
+ for pos, ch in _iter_escaped_chars(addr):
+ if ch == '(':
+ opens += 1
+ elif ch == ')':
+ opens -= 1
+ if opens < 0:
+ return False
+ return (opens == 0)
+
+
+def _pre_parse_validation(email_header_fields):
+ accepted_values = []
+ for v in email_header_fields:
+ if not _check_parenthesis(v):
+ v = "('', '')"
+ accepted_values.append(v)
+
+ return accepted_values
+
+
+def _post_parse_validation(parsed_email_header_tuples):
+ accepted_values = []
+ # The parser would have parsed a correctly formatted domain-literal
+ # The existence of an [ after parsing indicates a parsing failure
+ for v in parsed_email_header_tuples:
+ if '[' in v[1]:
+ v = ('', '')
+ accepted_values.append(v)
+
+ return accepted_values
def _format_timetuple_and_zone(timetuple, zone):
@@ -205,16 +321,33 @@ def parsedate_to_datetime(data):
tzinfo=datetime.timezone(datetime.timedelta(seconds=tz)))
-def parseaddr(addr):
+def parseaddr(addr, *, strict=True):
"""
Parse addr into its constituent realname and email address parts.
Return a tuple of realname and email address, unless the parse fails, in
which case return a 2-tuple of ('', '').
+
+ If strict is True, use a strict parser which rejects malformed inputs.
"""
- addrs = _AddressList(addr).addresslist
- if not addrs:
- return '', ''
+ if not strict:
+ addrs = _AddressList(addr).addresslist
+ if not addrs:
+ return ('', '')
+ return addrs[0]
+
+ if isinstance(addr, list):
+ addr = addr[0]
+
+ if not isinstance(addr, str):
+ return ('', '')
+
+ addr = _pre_parse_validation([addr])[0]
+ addrs = _post_parse_validation(_AddressList(addr).addresslist)
+
+ if not addrs or len(addrs) > 1:
+ return ('', '')
+
return addrs[0]
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -16,6 +16,7 @@ from unittest.mock import patch
import email
import email.policy
+import email.utils
from email.charset import Charset
from email.generator import Generator, DecodedGenerator, BytesGenerator
@@ -3288,15 +3289,137 @@ Foo
[('Al Person', 'aperson@dom.ain'),
('Bud Person', 'bperson@dom.ain')])
+ def test_parsing_errors(self):
+ """Test for parsing errors from CVE-2023-27043 and CVE-2019-16056"""
+ alice = 'alice@example.org'
+ bob = 'bob@example.com'
+ empty = ('', '')
+
+ # Test utils.getaddresses() and utils.parseaddr() on malformed email
+ # addresses: default behavior (strict=True) rejects malformed address,
+ # and strict=False which tolerates malformed address.
+ for invalid_separator, expected_non_strict in (
+ ('(', [(f'<{bob}>', alice)]),
+ (')', [('', alice), empty, ('', bob)]),
+ ('<', [('', alice), empty, ('', bob), empty]),
+ ('>', [('', alice), empty, ('', bob)]),
+ ('[', [('', f'{alice}[<{bob}>]')]),
+ (']', [('', alice), empty, ('', bob)]),
+ ('@', [empty, empty, ('', bob)]),
+ (';', [('', alice), empty, ('', bob)]),
+ (':', [('', alice), ('', bob)]),
+ ('.', [('', alice + '.'), ('', bob)]),
+ ('"', [('', alice), ('', f'<{bob}>')]),
+ ):
+ address = f'{alice}{invalid_separator}<{bob}>'
+ with self.subTest(address=address):
+ self.assertEqual(utils.getaddresses([address]),
+ [empty])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ expected_non_strict)
+
+ self.assertEqual(utils.parseaddr([address]),
+ empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Comma (',') is treated differently depending on strict parameter.
+ # Comma without quotes.
+ address = f'{alice},<{bob}>'
+ self.assertEqual(utils.getaddresses([address]),
+ [('', alice), ('', bob)])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ [('', alice), ('', bob)])
+ self.assertEqual(utils.parseaddr([address]),
+ empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Real name between quotes containing comma.
+ address = '"Alice, alice@example.org" <bob@example.com>'
+ expected_strict = ('Alice, alice@example.org', 'bob@example.com')
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Valid parenthesis in comments.
+ address = 'alice@example.org (Alice)'
+ expected_strict = ('Alice', 'alice@example.org')
+ self.assertEqual(utils.getaddresses([address]), [expected_strict])
+ self.assertEqual(utils.getaddresses([address], strict=False), [expected_strict])
+ self.assertEqual(utils.parseaddr([address]), expected_strict)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Invalid parenthesis in comments.
+ address = 'alice@example.org )Alice('
+ self.assertEqual(utils.getaddresses([address]), [empty])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ [('', 'alice@example.org'), ('', ''), ('', 'Alice')])
+ self.assertEqual(utils.parseaddr([address]), empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Two addresses with quotes separated by comma.
+ address = '"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>'
+ self.assertEqual(utils.getaddresses([address]),
+ [('Jane Doe', 'jane@example.net'),
+ ('John Doe', 'john@example.net')])
+ self.assertEqual(utils.getaddresses([address], strict=False),
+ [('Jane Doe', 'jane@example.net'),
+ ('John Doe', 'john@example.net')])
+ self.assertEqual(utils.parseaddr([address]), empty)
+ self.assertEqual(utils.parseaddr([address], strict=False),
+ ('', address))
+
+ # Test email.utils.supports_strict_parsing attribute
+ self.assertEqual(email.utils.supports_strict_parsing, True)
+
def test_getaddresses_nasty(self):
- eq = self.assertEqual
- eq(utils.getaddresses(['foo: ;']), [('', '')])
- eq(utils.getaddresses(
- ['[]*-- =~$']),
- [('', ''), ('', ''), ('', '*--')])
- eq(utils.getaddresses(
- ['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>']),
- [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')])
+ for addresses, expected in (
+ (['"Sürname, Firstname" <to@example.com>'],
+ [('Sürname, Firstname', 'to@example.com')]),
+
+ (['foo: ;'],
+ [('', '')]),
+
+ (['foo: ;', '"Jason R. Mastaler" <jason@dom.ain>'],
+ [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]),
+
+ ([r'Pete(A nice \) chap) <pete(his account)@silly.test(his host)>'],
+ [('Pete (A nice ) chap his account his host)', 'pete@silly.test')]),
+
+ (['(Empty list)(start)Undisclosed recipients :(nobody(I know))'],
+ [('', '')]),
+
+ (['Mary <@machine.tld:mary@example.net>, , jdoe@test . example'],
+ [('Mary', 'mary@example.net'), ('', ''), ('', 'jdoe@test.example')]),
+
+ (['John Doe <jdoe@machine(comment). example>'],
+ [('John Doe (comment)', 'jdoe@machine.example')]),
+
+ (['"Mary Smith: Personal Account" <smith@home.example>'],
+ [('Mary Smith: Personal Account', 'smith@home.example')]),
+
+ (['Undisclosed recipients:;'],
+ [('', '')]),
+
+ ([r'<boss@nil.test>, "Giant; \"Big\" Box" <bob@example.net>'],
+ [('', 'boss@nil.test'), ('Giant; "Big" Box', 'bob@example.net')]),
+ ):
+ with self.subTest(addresses=addresses):
+ self.assertEqual(utils.getaddresses(addresses),
+ expected)
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
+ expected)
+
+ addresses = ['[]*-- =~$']
+ self.assertEqual(utils.getaddresses(addresses),
+ [('', '')])
+ self.assertEqual(utils.getaddresses(addresses, strict=False),
+ [('', ''), ('', ''), ('', '*--')])
def test_getaddresses_embedded_comment(self):
"""Test proper handling of a nested comment"""
@@ -3485,6 +3608,54 @@ multipart/report
m = cls(*constructor, policy=email.policy.default)
self.assertIs(m.policy, email.policy.default)
+ def test_iter_escaped_chars(self):
+ self.assertEqual(list(utils._iter_escaped_chars(r'a\\b\"c\\"d')),
+ [(0, 'a'),
+ (2, '\\\\'),
+ (3, 'b'),
+ (5, '\\"'),
+ (6, 'c'),
+ (8, '\\\\'),
+ (9, '"'),
+ (10, 'd')])
+ self.assertEqual(list(utils._iter_escaped_chars('a\\')),
+ [(0, 'a'), (1, '\\')])
+
+ def test_strip_quoted_realnames(self):
+ def check(addr, expected):
+ self.assertEqual(utils._strip_quoted_realnames(addr), expected)
+
+ check('"Jane Doe" <jane@example.net>, "John Doe" <john@example.net>',
+ ' <jane@example.net>, <john@example.net>')
+ check(r'"Jane \"Doe\"." <jane@example.net>',
+ ' <jane@example.net>')
+
+ # special cases
+ check(r'before"name"after', 'beforeafter')
+ check(r'before"name"', 'before')
+ check(r'b"name"', 'b') # single char
+ check(r'"name"after', 'after')
+ check(r'"name"a', 'a') # single char
+ check(r'"name"', '')
+
+ # no change
+ for addr in (
+ 'Jane Doe <jane@example.net>, John Doe <john@example.net>',
+ 'lone " quote',
+ ):
+ self.assertEqual(utils._strip_quoted_realnames(addr), addr)
+
+
+ def test_check_parenthesis(self):
+ addr = 'alice@example.net'
+ self.assertTrue(utils._check_parenthesis(f'{addr} (Alice)'))
+ self.assertFalse(utils._check_parenthesis(f'{addr} )Alice('))
+ self.assertFalse(utils._check_parenthesis(f'{addr} (Alice))'))
+ self.assertFalse(utils._check_parenthesis(f'{addr} ((Alice)'))
+
+ # Ignore real name between quotes
+ self.assertTrue(utils._check_parenthesis(f'")Alice((" {addr}'))
+
# Test the iterator/generators
class TestIterators(TestEmailBase):
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-10-20-15-28-08.gh-issue-102988.dStNO7.rst
@@ -0,0 +1,8 @@
+:func:`email.utils.getaddresses` and :func:`email.utils.parseaddr` now
+return ``('', '')`` 2-tuples in more situations where invalid email
+addresses are encountered instead of potentially inaccurate values. Add
+optional *strict* parameter to these two functions: use ``strict=False`` to
+get the old behavior, accept malformed inputs.
+``getattr(email.utils, 'supports_strict_parsing', False)`` can be use to check
+if the *strict* paramater is available. Patch by Thomas Dwyer and Victor
+Stinner to improve the CVE-2023-27043 fix.

View File

@@ -4,11 +4,9 @@
Lib/test/test_xml_etree.py | 7 +++++++
3 files changed, 14 insertions(+)
Index: Python-3.10.19/Lib/test/test_pyexpat.py
===================================================================
--- Python-3.10.19.orig/Lib/test/test_pyexpat.py 2025-10-09 17:25:03.000000000 +0200
+++ Python-3.10.19/Lib/test/test_pyexpat.py 2025-12-19 23:10:22.001497953 +0100
@@ -802,6 +802,10 @@
--- a/Lib/test/test_pyexpat.py
+++ b/Lib/test/test_pyexpat.py
@@ -766,6 +766,10 @@ class ReparseDeferralTest(unittest.TestC
self.assertEqual(started, ['doc'])
def test_reparse_deferral_disabled(self):
@@ -19,11 +17,9 @@ Index: Python-3.10.19/Lib/test/test_pyexpat.py
started = []
def start_element(name, _):
Index: Python-3.10.19/Lib/test/test_sax.py
===================================================================
--- Python-3.10.19.orig/Lib/test/test_sax.py 2025-10-09 17:25:03.000000000 +0200
+++ Python-3.10.19/Lib/test/test_sax.py 2025-12-19 23:10:22.002078897 +0100
@@ -1240,6 +1240,9 @@
--- a/Lib/test/test_sax.py
+++ b/Lib/test/test_sax.py
@@ -1240,6 +1240,9 @@ class ExpatReaderTest(XmlTestBase):
self.assertEqual(result.getvalue(), start + b"<doc></doc>")
@@ -33,11 +29,9 @@ Index: Python-3.10.19/Lib/test/test_sax.py
def test_flush_reparse_deferral_disabled(self):
result = BytesIO()
xmlgen = XMLGenerator(result)
Index: Python-3.10.19/Lib/test/test_xml_etree.py
===================================================================
--- Python-3.10.19.orig/Lib/test/test_xml_etree.py 2025-10-09 17:25:03.000000000 +0200
+++ Python-3.10.19/Lib/test/test_xml_etree.py 2025-12-19 23:10:22.002413090 +0100
@@ -1420,9 +1420,13 @@
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -1420,9 +1420,13 @@ class XMLPullParserTest(unittest.TestCas
self.assert_event_tags(parser, [('end', 'root')])
self.assertIsNone(parser.close())
@@ -51,7 +45,7 @@ Index: Python-3.10.19/Lib/test/test_xml_etree.py
def test_simple_xml_chunk_5(self):
self.test_simple_xml(chunk_size=5, flush=True)
@@ -1647,6 +1651,9 @@
@@ -1648,6 +1652,9 @@ class XMLPullParserTest(unittest.TestCas
self.assert_event_tags(parser, [('end', 'doc')])

View File

@@ -0,0 +1,376 @@
From 0740166e60b8cdae9448220beb28721f0126ee03 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Wed, 24 Apr 2024 14:29:30 +0200
Subject: [PATCH 1/2] gh-113171: gh-65056: Fix "private" (non-global) IP
address ranges (GH-113179) (GH-113186) (GH-118177)
* GH-113171: Fix "private" (non-global) IP address ranges (GH-113179)
The _private_networks variables, used by various is_private
implementations, were missing some ranges and at the same time had
overly strict ranges (where there are more specific ranges considered
globally reachable by the IANA registries).
This patch updates the ranges with what was missing or otherwise
incorrect.
100.64.0.0/10 is left alone, for now, as it's been made special in [1].
The _address_exclude_many() call returns 8 networks for IPv4, 121
networks for IPv6.
[1] https://github.com/python/cpython/issues/61602
* GH-65056: Improve the IP address' is_global/is_private documentation (GH-113186)
It wasn't clear what the semantics of is_global/is_private are and, when
one gets to the bottom of it, it's not quite so simple (hence the
exceptions listed).
(cherry picked from commit 2a4cbf17af19a01d942f9579342f77c39fbd23c4)
(cherry picked from commit 40d75c2b7f5c67e254d0a025e0f2e2c7ada7f69f)
---------
(cherry picked from commit f86b17ac511e68192ba71f27e752321a3252cee3)
Co-authored-by: Jakub Stasiak <jakub@stasiak.at>
---
Doc/library/ipaddress.rst | 43 +++-
Doc/whatsnew/3.10.rst | 9
Lib/ipaddress.py | 99 +++++++---
Lib/test/test_ipaddress.py | 52 +++++
Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst | 9
5 files changed, 187 insertions(+), 25 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
--- a/Doc/library/ipaddress.rst
+++ b/Doc/library/ipaddress.rst
@@ -188,18 +188,53 @@ write code that handles both IP versions
.. attribute:: is_private
- ``True`` if the address is allocated for private networks. See
+ ``True`` if the address is defined as not globally reachable by
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
- (for IPv6).
+ (for IPv6) with the following exceptions:
+
+ * ``is_private`` is ``False`` for the shared address space (``100.64.0.0/10``)
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_private == address.ipv4_mapped.is_private
+
+ ``is_private`` has value opposite to :attr:`is_global`, except for the shared address space
+ (``100.64.0.0/10`` range) where they are both ``False``.
+
+ .. versionchanged:: 3.10.15
+
+ Fixed some false positives and false negatives.
+
+ * ``192.0.0.0/24`` is considered private with the exception of ``192.0.0.9/32`` and
+ ``192.0.0.10/32`` (previously: only the ``192.0.0.0/29`` sub-range was considered private).
+ * ``64:ff9b:1::/48`` is considered private.
+ * ``2002::/16`` is considered private.
+ * There are exceptions within ``2001::/23`` (otherwise considered private): ``2001:1::1/128``,
+ ``2001:1::2/128``, ``2001:3::/32``, ``2001:4:112::/48``, ``2001:20::/28``, ``2001:30::/28``.
+ The exceptions are not considered private.
.. attribute:: is_global
- ``True`` if the address is allocated for public networks. See
+ ``True`` if the address is defined as globally reachable by
iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
- (for IPv6).
+ (for IPv6) with the following exception:
+
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_global == address.ipv4_mapped.is_global
+
+ ``is_global`` has value opposite to :attr:`is_private`, except for the shared address space
+ (``100.64.0.0/10`` range) where they are both ``False``.
.. versionadded:: 3.4
+ .. versionchanged:: 3.10.15
+
+ Fixed some false positives and false negatives, see :attr:`is_private` for details.
+
.. attribute:: is_unspecified
``True`` if the address is unspecified. See :RFC:`5735` (for IPv4)
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -2348,3 +2348,12 @@ tarfile
:exc:`DeprecationWarning`.
In Python 3.14, the default will switch to ``'data'``.
(Contributed by Petr Viktorin in :pep:`706`.)
+
+Notable changes in 3.10.15
+==========================
+
+ipaddress
+---------
+
+* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
+ ``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.
--- a/Lib/ipaddress.py
+++ b/Lib/ipaddress.py
@@ -1323,18 +1323,41 @@ class IPv4Address(_BaseV4, _BaseAddress)
@property
@functools.lru_cache()
def is_private(self):
- """Test if this address is allocated for private networks.
-
- Returns:
- A boolean, True if the address is reserved per
- iana-ipv4-special-registry.
-
- """
- return any(self in net for net in self._constants._private_networks)
+ """``True`` if the address is defined as not globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exceptions:
+
+ * ``is_private`` is ``False`` for ``100.64.0.0/10``
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_private == address.ipv4_mapped.is_private
+
+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
+ """
+ return (
+ any(self in net for net in self._constants._private_networks)
+ and all(self not in net for net in self._constants._private_networks_exceptions)
+ )
@property
@functools.lru_cache()
def is_global(self):
+ """``True`` if the address is defined as globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exception:
+
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
+
+ address.is_global == address.ipv4_mapped.is_global
+
+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
+ """
return self not in self._constants._public_network and not self.is_private
@property
@@ -1538,13 +1561,15 @@ class _IPv4Constants:
_public_network = IPv4Network('100.64.0.0/10')
+ # Not globally reachable address blocks listed on
+ # https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
_private_networks = [
IPv4Network('0.0.0.0/8'),
IPv4Network('10.0.0.0/8'),
IPv4Network('127.0.0.0/8'),
IPv4Network('169.254.0.0/16'),
IPv4Network('172.16.0.0/12'),
- IPv4Network('192.0.0.0/29'),
+ IPv4Network('192.0.0.0/24'),
IPv4Network('192.0.0.170/31'),
IPv4Network('192.0.2.0/24'),
IPv4Network('192.168.0.0/16'),
@@ -1555,6 +1580,11 @@ class _IPv4Constants:
IPv4Network('255.255.255.255/32'),
]
+ _private_networks_exceptions = [
+ IPv4Network('192.0.0.9/32'),
+ IPv4Network('192.0.0.10/32'),
+ ]
+
_reserved_network = IPv4Network('240.0.0.0/4')
_unspecified_address = IPv4Address('0.0.0.0')
@@ -1996,27 +2026,42 @@ class IPv6Address(_BaseV6, _BaseAddress)
@property
@functools.lru_cache()
def is_private(self):
- """Test if this address is allocated for private networks.
+ """``True`` if the address is defined as not globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exceptions:
+
+ * ``is_private`` is ``False`` for ``100.64.0.0/10``
+ * For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
- Returns:
- A boolean, True if the address is reserved per
- iana-ipv6-special-registry, or is ipv4_mapped and is
- reserved in the iana-ipv4-special-registry.
+ address.is_private == address.ipv4_mapped.is_private
+ ``is_private`` has value opposite to :attr:`is_global`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
"""
ipv4_mapped = self.ipv4_mapped
if ipv4_mapped is not None:
return ipv4_mapped.is_private
- return any(self in net for net in self._constants._private_networks)
+ return (
+ any(self in net for net in self._constants._private_networks)
+ and all(self not in net for net in self._constants._private_networks_exceptions)
+ )
@property
def is_global(self):
- """Test if this address is allocated for public networks.
+ """``True`` if the address is defined as globally reachable by
+ iana-ipv4-special-registry_ (for IPv4) or iana-ipv6-special-registry_
+ (for IPv6) with the following exception:
+
+ For IPv4-mapped IPv6-addresses the ``is_private`` value is determined by the
+ semantics of the underlying IPv4 addresses and the following condition holds
+ (see :attr:`IPv6Address.ipv4_mapped`)::
- Returns:
- A boolean, true if the address is not reserved per
- iana-ipv6-special-registry.
+ address.is_global == address.ipv4_mapped.is_global
+ ``is_global`` has value opposite to :attr:`is_private`, except for the ``100.64.0.0/10``
+ IPv4 range where they are both ``False``.
"""
return not self.is_private
@@ -2257,19 +2302,31 @@ class _IPv6Constants:
_multicast_network = IPv6Network('ff00::/8')
+ # Not globally reachable address blocks listed on
+ # https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
_private_networks = [
IPv6Network('::1/128'),
IPv6Network('::/128'),
IPv6Network('::ffff:0:0/96'),
+ IPv6Network('64:ff9b:1::/48'),
IPv6Network('100::/64'),
IPv6Network('2001::/23'),
- IPv6Network('2001:2::/48'),
IPv6Network('2001:db8::/32'),
- IPv6Network('2001:10::/28'),
+ # IANA says N/A, let's consider it not globally reachable to be safe
+ IPv6Network('2002::/16'),
IPv6Network('fc00::/7'),
IPv6Network('fe80::/10'),
]
+ _private_networks_exceptions = [
+ IPv6Network('2001:1::1/128'),
+ IPv6Network('2001:1::2/128'),
+ IPv6Network('2001:3::/32'),
+ IPv6Network('2001:4:112::/48'),
+ IPv6Network('2001:20::/28'),
+ IPv6Network('2001:30::/28'),
+ ]
+
_reserved_networks = [
IPv6Network('::/8'), IPv6Network('100::/8'),
IPv6Network('200::/7'), IPv6Network('400::/6'),
--- a/Lib/test/test_ipaddress.py
+++ b/Lib/test/test_ipaddress.py
@@ -2263,6 +2263,10 @@ class IpaddrUnitTest(unittest.TestCase):
self.assertEqual(True, ipaddress.ip_address(
'172.31.255.255').is_private)
self.assertEqual(False, ipaddress.ip_address('172.32.0.0').is_private)
+ self.assertFalse(ipaddress.ip_address('192.0.0.0').is_global)
+ self.assertTrue(ipaddress.ip_address('192.0.0.9').is_global)
+ self.assertTrue(ipaddress.ip_address('192.0.0.10').is_global)
+ self.assertFalse(ipaddress.ip_address('192.0.0.255').is_global)
self.assertEqual(True,
ipaddress.ip_address('169.254.100.200').is_link_local)
@@ -2278,6 +2282,40 @@ class IpaddrUnitTest(unittest.TestCase):
self.assertEqual(False, ipaddress.ip_address('128.0.0.0').is_loopback)
self.assertEqual(True, ipaddress.ip_network('0.0.0.0').is_unspecified)
+ def testPrivateNetworks(self):
+ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/0").is_private)
+ self.assertEqual(False, ipaddress.ip_network("1.0.0.0/8").is_private)
+
+ self.assertEqual(True, ipaddress.ip_network("0.0.0.0/8").is_private)
+ self.assertEqual(True, ipaddress.ip_network("10.0.0.0/8").is_private)
+ self.assertEqual(True, ipaddress.ip_network("127.0.0.0/8").is_private)
+ self.assertEqual(True, ipaddress.ip_network("169.254.0.0/16").is_private)
+ self.assertEqual(True, ipaddress.ip_network("172.16.0.0/12").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.0.0.0/29").is_private)
+ self.assertEqual(False, ipaddress.ip_network("192.0.0.9/32").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.0.0.170/31").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.0.2.0/24").is_private)
+ self.assertEqual(True, ipaddress.ip_network("192.168.0.0/16").is_private)
+ self.assertEqual(True, ipaddress.ip_network("198.18.0.0/15").is_private)
+ self.assertEqual(True, ipaddress.ip_network("198.51.100.0/24").is_private)
+ self.assertEqual(True, ipaddress.ip_network("203.0.113.0/24").is_private)
+ self.assertEqual(True, ipaddress.ip_network("240.0.0.0/4").is_private)
+ self.assertEqual(True, ipaddress.ip_network("255.255.255.255/32").is_private)
+
+ self.assertEqual(False, ipaddress.ip_network("::/0").is_private)
+ self.assertEqual(False, ipaddress.ip_network("::ff/128").is_private)
+
+ self.assertEqual(True, ipaddress.ip_network("::1/128").is_private)
+ self.assertEqual(True, ipaddress.ip_network("::/128").is_private)
+ self.assertEqual(True, ipaddress.ip_network("::ffff:0:0/96").is_private)
+ self.assertEqual(True, ipaddress.ip_network("100::/64").is_private)
+ self.assertEqual(True, ipaddress.ip_network("2001:2::/48").is_private)
+ self.assertEqual(False, ipaddress.ip_network("2001:3::/48").is_private)
+ self.assertEqual(True, ipaddress.ip_network("2001:db8::/32").is_private)
+ self.assertEqual(True, ipaddress.ip_network("2001:10::/28").is_private)
+ self.assertEqual(True, ipaddress.ip_network("fc00::/7").is_private)
+ self.assertEqual(True, ipaddress.ip_network("fe80::/10").is_private)
+
def testReservedIpv6(self):
self.assertEqual(True, ipaddress.ip_network('ffff::').is_multicast)
@@ -2351,6 +2389,20 @@ class IpaddrUnitTest(unittest.TestCase):
self.assertEqual(True, ipaddress.ip_address('0::0').is_unspecified)
self.assertEqual(False, ipaddress.ip_address('::1').is_unspecified)
+ self.assertFalse(ipaddress.ip_address('64:ff9b:1::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:1::1').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:1::2').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:2::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:3::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:4::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:4:112::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:10::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:20::').is_global)
+ self.assertTrue(ipaddress.ip_address('2001:30::').is_global)
+ self.assertFalse(ipaddress.ip_address('2001:40::').is_global)
+ self.assertFalse(ipaddress.ip_address('2002::').is_global)
+
# some generic IETF reserved addresses
self.assertEqual(True, ipaddress.ip_address('100::').is_reserved)
self.assertEqual(True, ipaddress.ip_network('4000::1/128').is_reserved)
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-03-14-01-38-44.gh-issue-113171.VFnObz.rst
@@ -0,0 +1,9 @@
+Fixed various false positives and false negatives in
+
+* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
+* :attr:`ipaddress.IPv4Address.is_global`
+* :attr:`ipaddress.IPv6Address.is_private`
+* :attr:`ipaddress.IPv6Address.is_global`
+
+Also in the corresponding :class:`ipaddress.IPv4Network` and :class:`ipaddress.IPv6Network`
+attributes.

View File

@@ -0,0 +1,335 @@
From ff3629b3cedf5e50a7a5f567283778fe5539a11e Mon Sep 17 00:00:00 2001
From: Petr Viktorin <encukou@gmail.com>
Date: Wed, 31 Jul 2024 00:19:48 +0200
Subject: [PATCH] [3.10] gh-121650: Encode newlines in headers, and verify
headers are sound (GH-122233)
Per RFC 2047:
> [...] these encoding schemes allow the
> encoding of arbitrary octet values, mail readers that implement this
> decoding should also ensure that display of the decoded data on the
> recipient's terminal will not cause unwanted side-effects
It seems that the "quoted-word" scheme is a valid way to include
a newline character in a header value, just like we already allow
undecodable bytes or control characters.
They do need to be properly quoted when serialized to text, though.
This should fail for custom fold() implementations that aren't careful
about newlines.
(cherry picked from commit 097633981879b3c9de9a1dd120d3aa585ecc2384)
Co-authored-by: Petr Viktorin <encukou@gmail.com>
Co-authored-by: Bas Bloemsaat <bas@bloemsaat.org>
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
---
Doc/library/email.errors.rst | 6
Doc/library/email.policy.rst | 18 ++
Doc/whatsnew/3.10.rst | 12 +
Lib/email/_header_value_parser.py | 12 +
Lib/email/_policybase.py | 8 +
Lib/email/errors.py | 4
Lib/email/generator.py | 13 +-
Lib/test/test_email/test_generator.py | 62 ++++++++++
Lib/test/test_email/test_policy.py | 26 ++++
Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst | 5
10 files changed, 162 insertions(+), 4 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
--- a/Doc/library/email.errors.rst
+++ b/Doc/library/email.errors.rst
@@ -59,6 +59,12 @@ The following exception classes are defi
:class:`~email.mime.image.MIMEImage`).
+.. exception:: HeaderWriteError()
+
+ Raised when an error occurs when the :mod:`~email.generator` outputs
+ headers.
+
+
Here is the list of the defects that the :class:`~email.parser.FeedParser`
can find while parsing messages. Note that the defects are added to the message
where the problem was found, so for example, if a message nested inside a
--- a/Doc/library/email.policy.rst
+++ b/Doc/library/email.policy.rst
@@ -229,6 +229,24 @@ added matters. To illustrate::
.. versionadded:: 3.6
+
+ .. attribute:: verify_generated_headers
+
+ If ``True`` (the default), the generator will raise
+ :exc:`~email.errors.HeaderWriteError` instead of writing a header
+ that is improperly folded or delimited, such that it would
+ be parsed as multiple headers or joined with adjacent data.
+ Such headers can be generated by custom header classes or bugs
+ in the ``email`` module.
+
+ As it's a security feature, this defaults to ``True`` even in the
+ :class:`~email.policy.Compat32` policy.
+ For backwards compatible, but unsafe, behavior, it must be set to
+ ``False`` explicitly.
+
+ .. versionadded:: 3.10.15
+
+
The following :class:`Policy` method is intended to be called by code using
the email library to create policy instances with custom settings:
--- a/Doc/whatsnew/3.10.rst
+++ b/Doc/whatsnew/3.10.rst
@@ -2357,3 +2357,15 @@ ipaddress
* Fixed ``is_global`` and ``is_private`` behavior in ``IPv4Address``,
``IPv6Address``, ``IPv4Network`` and ``IPv6Network``.
+
+email
+-----
+
+* Headers with embedded newlines are now quoted on output.
+
+ The :mod:`~email.generator` will now refuse to serialize (write) headers
+ that are improperly folded or delimited, such that they would be parsed as
+ multiple headers or joined with adjacent data.
+ If you need to turn this safety feature off,
+ set :attr:`~email.policy.Policy.verify_generated_headers`.
+ (Contributed by Bas Bloemsaat and Petr Viktorin in :gh:`121650`.)
--- a/Lib/email/_header_value_parser.py
+++ b/Lib/email/_header_value_parser.py
@@ -92,6 +92,8 @@ TOKEN_ENDS = TSPECIALS | WSP
ASPECIALS = TSPECIALS | set("*'%")
ATTRIBUTE_ENDS = ASPECIALS | WSP
EXTENDED_ATTRIBUTE_ENDS = ATTRIBUTE_ENDS - set('%')
+NLSET = {'\n', '\r'}
+SPECIALSNL = SPECIALS | NLSET
def quote_string(value):
return '"'+str(value).replace('\\', '\\\\').replace('"', r'\"')+'"'
@@ -2778,9 +2780,13 @@ def _refold_parse_tree(parse_tree, *, po
wrap_as_ew_blocked -= 1
continue
tstr = str(part)
- if part.token_type == 'ptext' and set(tstr) & SPECIALS:
- # Encode if tstr contains special characters.
- want_encoding = True
+ if not want_encoding:
+ if part.token_type == 'ptext':
+ # Encode if tstr contains special characters.
+ want_encoding = not SPECIALSNL.isdisjoint(tstr)
+ else:
+ # Encode if tstr contains newlines.
+ want_encoding = not NLSET.isdisjoint(tstr)
try:
tstr.encode(encoding)
charset = encoding
--- a/Lib/email/_policybase.py
+++ b/Lib/email/_policybase.py
@@ -157,6 +157,13 @@ class Policy(_PolicyBase, metaclass=abc.
message_factory -- the class to use to create new message objects.
If the value is None, the default is Message.
+ verify_generated_headers
+ -- if true, the generator verifies that each header
+ they are properly folded, so that a parser won't
+ treat it as multiple headers, start-of-body, or
+ part of another header.
+ This is a check against custom Header & fold()
+ implementations.
"""
raise_on_defect = False
@@ -165,6 +172,7 @@ class Policy(_PolicyBase, metaclass=abc.
max_line_length = 78
mangle_from_ = False
message_factory = None
+ verify_generated_headers = True
def handle_defect(self, obj, defect):
"""Based on policy, either raise defect or call register_defect.
--- a/Lib/email/errors.py
+++ b/Lib/email/errors.py
@@ -29,6 +29,10 @@ class CharsetError(MessageError):
"""An illegal charset was given."""
+class HeaderWriteError(MessageError):
+ """Error while writing headers."""
+
+
# These are parsing defects which the parser was able to work around.
class MessageDefect(ValueError):
"""Base class for a message defect."""
--- a/Lib/email/generator.py
+++ b/Lib/email/generator.py
@@ -14,12 +14,14 @@ import random
from copy import deepcopy
from io import StringIO, BytesIO
from email.utils import _has_surrogates
+from email.errors import HeaderWriteError
UNDERSCORE = '_'
NL = '\n' # XXX: no longer used by the code below.
NLCRE = re.compile(r'\r\n|\r|\n')
fcre = re.compile(r'^From ', re.MULTILINE)
+NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]')
@@ -223,7 +225,16 @@ class Generator:
def _write_headers(self, msg):
for h, v in msg.raw_items():
- self.write(self.policy.fold(h, v))
+ folded = self.policy.fold(h, v)
+ if self.policy.verify_generated_headers:
+ linesep = self.policy.linesep
+ if not folded.endswith(self.policy.linesep):
+ raise HeaderWriteError(
+ f'folded header does not end with {linesep!r}: {folded!r}')
+ if NEWLINE_WITHOUT_FWSP.search(folded.removesuffix(linesep)):
+ raise HeaderWriteError(
+ f'folded header contains newline: {folded!r}')
+ self.write(folded)
# A blank line always separates headers from body
self.write(self._NL)
--- a/Lib/test/test_email/test_generator.py
+++ b/Lib/test/test_email/test_generator.py
@@ -6,6 +6,7 @@ from email.message import EmailMessage
from email.generator import Generator, BytesGenerator
from email.headerregistry import Address
from email import policy
+import email.errors
from test.test_email import TestEmailBase, parameterize
@@ -216,6 +217,44 @@ class TestGeneratorBase:
g.flatten(msg)
self.assertEqual(s.getvalue(), self.typ(expected))
+ def test_keep_encoded_newlines(self):
+ msg = self.msgmaker(self.typ(textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
+
+ None
+ """)))
+ expected = textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
+
+ None
+ """)
+ s = self.ioclass()
+ g = self.genclass(s, policy=self.policy.clone(max_line_length=80))
+ g.flatten(msg)
+ self.assertEqual(s.getvalue(), self.typ(expected))
+
+ def test_keep_long_encoded_newlines(self):
+ msg = self.msgmaker(self.typ(textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject=?UTF-8?Q?=0A?=Bcc: injection@example.com
+
+ None
+ """)))
+ expected = textwrap.dedent("""\
+ To: nobody
+ Subject: Bad subject
+ =?utf-8?q?=0A?=Bcc:
+ injection@example.com
+
+ None
+ """)
+ s = self.ioclass()
+ g = self.genclass(s, policy=self.policy.clone(max_line_length=30))
+ g.flatten(msg)
+ self.assertEqual(s.getvalue(), self.typ(expected))
+
class TestGenerator(TestGeneratorBase, TestEmailBase):
@@ -224,6 +263,29 @@ class TestGenerator(TestGeneratorBase, T
ioclass = io.StringIO
typ = str
+ def test_verify_generated_headers(self):
+ """gh-121650: by default the generator prevents header injection"""
+ class LiteralHeader(str):
+ name = 'Header'
+ def fold(self, **kwargs):
+ return self
+
+ for text in (
+ 'Value\r\nBad Injection\r\n',
+ 'NoNewLine'
+ ):
+ with self.subTest(text=text):
+ message = message_from_string(
+ "Header: Value\r\n\r\nBody",
+ policy=self.policy,
+ )
+
+ del message['Header']
+ message['Header'] = LiteralHeader(text)
+
+ with self.assertRaises(email.errors.HeaderWriteError):
+ message.as_string()
+
class TestBytesGenerator(TestGeneratorBase, TestEmailBase):
--- a/Lib/test/test_email/test_policy.py
+++ b/Lib/test/test_email/test_policy.py
@@ -26,6 +26,7 @@ class PolicyAPITests(unittest.TestCase):
'raise_on_defect': False,
'mangle_from_': True,
'message_factory': None,
+ 'verify_generated_headers': True,
}
# These default values are the ones set on email.policy.default.
# If any of these defaults change, the docs must be updated.
@@ -277,6 +278,31 @@ class PolicyAPITests(unittest.TestCase):
with self.assertRaises(email.errors.HeaderParseError):
policy.fold("Subject", subject)
+ def test_verify_generated_headers(self):
+ """Turning protection off allows header injection"""
+ policy = email.policy.default.clone(verify_generated_headers=False)
+ for text in (
+ 'Header: Value\r\nBad: Injection\r\n',
+ 'Header: NoNewLine'
+ ):
+ with self.subTest(text=text):
+ message = email.message_from_string(
+ "Header: Value\r\n\r\nBody",
+ policy=policy,
+ )
+ class LiteralHeader(str):
+ name = 'Header'
+ def fold(self, **kwargs):
+ return self
+
+ del message['Header']
+ message['Header'] = LiteralHeader(text)
+
+ self.assertEqual(
+ message.as_string(),
+ f"{text}\nBody",
+ )
+
# XXX: Need subclassing tests.
# For adding subclassed objects, make sure the usual rules apply (subclass
# wins), but that the order still works (right overrides left).
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-07-27-16-10-41.gh-issue-121650.nf6oc9.rst
@@ -0,0 +1,5 @@
+:mod:`email` headers with embedded newlines are now quoted on output. The
+:mod:`~email.generator` will now refuse to serialize (write) headers that
+are unsafely folded or delimited; see
+:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas
+Bloemsaat and Petr Viktorin in :gh:`121650`.)

View File

@@ -0,0 +1,136 @@
---
Lib/test/test_zipfile.py | 75 ++++++++++
Lib/zipfile.py | 10 +
Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst | 1
Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst | 3
4 files changed, 87 insertions(+), 2 deletions(-)
--- a/Lib/test/test_zipfile.py
+++ b/Lib/test/test_zipfile.py
@@ -3280,6 +3280,81 @@ with zipfile.ZipFile(io.BytesIO(), "w")
zipfile.Path(zf)
zf.extractall(source_path.parent)
+ def test_malformed_paths(self):
+ """
+ Path should handle malformed paths gracefully.
+
+ Paths with leading slashes are not visible.
+
+ Paths with dots are treated like regular files.
+ """
+ data = io.BytesIO()
+ zf = zipfile.ZipFile(data, "w")
+ zf.writestr("../parent.txt", b"content")
+ zf.filename = ''
+ root = zipfile.Path(zf)
+ assert list(map(str, root.iterdir())) == ['../']
+ assert root.joinpath('..').joinpath('parent.txt').read_bytes() == b'content'
+
+ def test_unsupported_names(self):
+ """
+ Path segments with special characters are readable.
+
+ On some platforms or file systems, characters like
+ ``:`` and ``?`` are not allowed, but they are valid
+ in the zip file.
+ """
+ data = io.BytesIO()
+ zf = zipfile.ZipFile(data, "w")
+ zf.writestr("path?", b"content")
+ zf.writestr("V: NMS.flac", b"fLaC...")
+ zf.filename = ''
+ root = zipfile.Path(zf)
+ contents = root.iterdir()
+ assert next(contents).name == 'path?'
+ assert next(contents).name == 'V: NMS.flac'
+ assert root.joinpath('V: NMS.flac').read_bytes() == b"fLaC..."
+
+ def test_backslash_not_separator(self):
+ """
+ In a zip file, backslashes are not separators.
+ """
+ data = io.BytesIO()
+ zf = zipfile.ZipFile(data, "w")
+ zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
+ zf.filename = ''
+ root = zipfile.Path(zf)
+ (first,) = root.iterdir()
+ assert not first.is_dir()
+ assert first.name == 'foo\\bar'
+
+
+class DirtyZipInfo(zipfile.ZipInfo):
+ """
+ Bypass name sanitization.
+ """
+
+ def __init__(self, filename, *args, **kwargs):
+ super().__init__(filename, *args, **kwargs)
+ self.filename = filename
+
+ @classmethod
+ def for_name(cls, name, archive):
+ """
+ Construct the same way that ZipFile.writestr does.
+
+ TODO: extract this functionality and re-use
+ """
+ self = cls(filename=name, date_time=time.localtime(time.time())[:6])
+ self.compress_type = archive.compression
+ self.compress_level = archive.compresslevel
+ if self.filename.endswith('/'): # pragma: no cover
+ self.external_attr = 0o40775 << 16 # drwxrwxr-x
+ self.external_attr |= 0x10 # MS-DOS directory flag
+ else:
+ self.external_attr = 0o600 << 16 # ?rw-------
+ return self
+
class StripExtraTests(unittest.TestCase):
# Note: all of the "z" characters are technically invalid, but up
--- a/Lib/zipfile.py
+++ b/Lib/zipfile.py
@@ -9,6 +9,7 @@ import io
import itertools
import os
import posixpath
+import re
import shutil
import stat
import struct
@@ -2151,7 +2152,7 @@ def _parents(path):
def _ancestry(path):
"""
Given a path with elements separated by
- posixpath.sep, generate all elements of that path
+ posixpath.sep, generate all elements of that path.
>>> list(_ancestry('b/d'))
['b/d', 'b']
@@ -2163,9 +2164,14 @@ def _ancestry(path):
['b']
>>> list(_ancestry(''))
[]
+
+ Multiple separators are treated like a single.
+
+ >>> list(_ancestry('//b//d///f//'))
+ ['//b//d///f', '//b//d', '//b']
"""
path = path.rstrip(posixpath.sep)
- while path and path != posixpath.sep:
+ while path.rstrip(posixpath.sep):
yield path
path, tail = posixpath.split(path)
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-08-11-14-08-04.gh-issue-122905.7tDsxA.rst
@@ -0,0 +1 @@
+:class:`zipfile.Path` objects now sanitize names from the zipfile.
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-08-26-13-45-20.gh-issue-123270.gXHvNJ.rst
@@ -0,0 +1,3 @@
+Applied a more surgical fix for malformed payloads in :class:`zipfile.Path`
+causing infinite loops (gh-122905) without breaking contents using
+legitimate characters.

View File

@@ -1,116 +0,0 @@
From 2065aa5b8f2bcdea2f628686c57974793a62c42b Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Mon, 19 Jan 2026 06:38:22 -0600
Subject: [PATCH] [3.10] gh-143935: Email preserve parens when folding comments
(GH-143936)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix a bug in the folding of comments when flattening an email message
using a modern email policy. Comments consisting of a very long sequence of
non-foldable characters could trigger a forced line wrap that omitted the
required leading space on the continuation line, causing the remainder of
the comment to be interpreted as a new header field. This enabled header
injection with carefully crafted inputs.
(cherry picked from commit 17d1490)
Co-authored-by: Seth Michael Larson seth@python.org
Co-authored-by: Denis Ledoux dle@odoo.com
- Issue: Fix folding of long comments of unfoldable characters in email headers #143935
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
---
Lib/email/_header_value_parser.py | 15 +++++++++++-
.../test_email/test__header_value_parser.py | 23 +++++++++++++++++++
...-01-16-14-40-31.gh-issue-143935.U2YtKl.rst | 6 +++++
3 files changed, 43 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py
index dbc0bd8196af52..2c05abeadea22b 100644
--- a/Lib/email/_header_value_parser.py
+++ b/Lib/email/_header_value_parser.py
@@ -101,6 +101,12 @@ def make_quoted_pairs(value):
return str(value).replace('\\', '\\\\').replace('"', '\\"')
+def make_parenthesis_pairs(value):
+ """Escape parenthesis and backslash for use within a comment."""
+ return str(value).replace('\\', '\\\\') \
+ .replace('(', '\\(').replace(')', '\\)')
+
+
def quote_string(value):
escaped = make_quoted_pairs(value)
return f'"{escaped}"'
@@ -927,7 +933,7 @@ def value(self):
return ' '
def startswith_fws(self):
- return True
+ return self and self[0] in WSP
class ValueTerminal(Terminal):
@@ -2865,6 +2871,13 @@ def _refold_parse_tree(parse_tree, *, policy):
[ValueTerminal(make_quoted_pairs(p), 'ptext')
for p in newparts] +
[ValueTerminal('"', 'ptext')])
+ if part.token_type == 'comment':
+ newparts = (
+ [ValueTerminal('(', 'ptext')] +
+ [ValueTerminal(make_parenthesis_pairs(p), 'ptext')
+ if p.token_type == 'ptext' else p
+ for p in newparts] +
+ [ValueTerminal(')', 'ptext')])
if not part.as_ew_allowed:
wrap_as_ew_blocked += 1
newparts.append(end_ew_not_allowed)
diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py
index 6a4ecafd68b4ab..2eaaaaef675284 100644
--- a/Lib/test/test_email/test__header_value_parser.py
+++ b/Lib/test/test_email/test__header_value_parser.py
@@ -2973,6 +2973,29 @@ def test_address_list_with_specials_in_long_quoted_string(self):
with self.subTest(to=to):
self._test(parser.get_address_list(to)[0], folded, policy=policy)
+ def test_address_list_with_long_unwrapable_comment(self):
+ policy = self.policy.clone(max_line_length=40)
+ cases = [
+ # (to, folded)
+ ('(loremipsumdolorsitametconsecteturadipi)<spy@example.org>',
+ '(loremipsumdolorsitametconsecteturadipi)<spy@example.org>\n'),
+ ('<spy@example.org>(loremipsumdolorsitametconsecteturadipi)',
+ '<spy@example.org>(loremipsumdolorsitametconsecteturadipi)\n'),
+ ('(loremipsum dolorsitametconsecteturadipi)<spy@example.org>',
+ '(loremipsum dolorsitametconsecteturadipi)<spy@example.org>\n'),
+ ('<spy@example.org>(loremipsum dolorsitametconsecteturadipi)',
+ '<spy@example.org>(loremipsum\n dolorsitametconsecteturadipi)\n'),
+ ('(Escaped \\( \\) chars \\\\ in comments stay escaped)<spy@example.org>',
+ '(Escaped \\( \\) chars \\\\ in comments stay\n escaped)<spy@example.org>\n'),
+ ('((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>',
+ '((loremipsum)(loremipsum)(loremipsum)(loremipsum))<spy@example.org>\n'),
+ ('((loremipsum)(loremipsum)(loremipsum) (loremipsum))<spy@example.org>',
+ '((loremipsum)(loremipsum)(loremipsum)\n (loremipsum))<spy@example.org>\n'),
+ ]
+ for (to, folded) in cases:
+ with self.subTest(to=to):
+ self._test(parser.get_address_list(to)[0], folded, policy=policy)
+
def test_address_list_with_specials_in_encoded_word(self):
# An encoded-word parsed from a structured header must remain
# encoded when it contains specials. Regression for gh-121284.
diff --git a/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst b/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
new file mode 100644
index 00000000000000..c3d864936884ac
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-01-16-14-40-31.gh-issue-143935.U2YtKl.rst
@@ -0,0 +1,6 @@
+Fixed a bug in the folding of comments when flattening an email message
+using a modern email policy. Comments consisting of a very long sequence of
+non-foldable characters could trigger a forced line wrap that omitted the
+required leading space on the continuation line, causing the remainder of
+the comment to be interpreted as a new header field. This enabled header
+injection with carefully crafted inputs.

View File

@@ -1,93 +0,0 @@
From f4eb9ab014545b521fb261b80adfa6d138e7e092 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Wed, 3 Dec 2025 01:16:37 -0600
Subject: [PATCH] gh-142145: Remove quadratic behavior in node ID cache
clearing (GH-142146)
* Remove quadratic behavior in node ID cache clearing
Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com>
* Add news fragment
---------
(cherry picked from commit 08d8e18ad81cd45bc4a27d6da478b51ea49486e4)
Co-authored-by: Seth Michael Larson <seth@python.org>
Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com>
---
Lib/test/test_minidom.py | 18 ++++++++++
Lib/xml/dom/minidom.py | 9 -----
Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 1
3 files changed, 20 insertions(+), 8 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
Index: Python-3.10.19/Lib/test/test_minidom.py
===================================================================
--- Python-3.10.19.orig/Lib/test/test_minidom.py 2025-12-19 23:10:45.263295780 +0100
+++ Python-3.10.19/Lib/test/test_minidom.py 2025-12-19 23:10:50.342493590 +0100
@@ -2,6 +2,7 @@
import copy
import pickle
+import time
import io
from test import support
import unittest
@@ -176,6 +177,23 @@
self.confirm(dom.documentElement.childNodes[-1].data == "Hello")
dom.unlink()
+ def testAppendChildNoQuadraticComplexity(self):
+ impl = getDOMImplementation()
+
+ newdoc = impl.createDocument(None, "some_tag", None)
+ top_element = newdoc.documentElement
+ children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)]
+ element = top_element
+
+ start = time.time()
+ for child in children:
+ element.appendChild(child)
+ element = child
+ end = time.time()
+
+ # This example used to take at least 30 seconds.
+ self.assertLess(end - start, 1)
+
def testAppendChildFragment(self):
dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes()
dom.documentElement.appendChild(frag)
Index: Python-3.10.19/Lib/xml/dom/minidom.py
===================================================================
--- Python-3.10.19.orig/Lib/xml/dom/minidom.py 2025-12-19 23:10:45.263295780 +0100
+++ Python-3.10.19/Lib/xml/dom/minidom.py 2025-12-19 23:10:50.342898393 +0100
@@ -292,13 +292,6 @@
childNodes.append(node)
node.parentNode = self
-def _in_document(node):
- # return True iff node is part of a document tree
- while node is not None:
- if node.nodeType == Node.DOCUMENT_NODE:
- return True
- node = node.parentNode
- return False
def _write_data(writer, data):
"Writes datachars to writer."
@@ -1539,7 +1532,7 @@
if node.nodeType == Node.DOCUMENT_NODE:
node._id_cache.clear()
node._id_search_stack = None
- elif _in_document(node):
+ elif node.ownerDocument:
node.ownerDocument._id_cache.clear()
node.ownerDocument._id_search_stack= None
Index: Python-3.10.19/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.10.19/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst 2025-12-19 23:10:50.343161277 +0100
@@ -0,0 +1 @@
+Remove quadratic behavior in ``xml.minidom`` node ID cache clearing.

View File

@@ -1,155 +0,0 @@
From b3a7998115e195c40e00cfa662bcaa899d937c05 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
Date: Mon, 1 Dec 2025 17:26:07 +0200
Subject: [PATCH] gh-119451: Fix a potential denial of service in http.client
(GH-119454)
Reading the whole body of the HTTP response could cause OOM if
the Content-Length value is too large even if the server does not send
a large amount of data. Now the HTTP client reads large data by chunks,
therefore the amount of consumed memory is proportional to the amount
of sent data.
(cherry picked from commit 5a4c4a033a4a54481be6870aa1896fad732555b5)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
---
Lib/http/client.py | 28 ++++++--
Lib/test/test_httplib.py | 66 +++++++++++++++++++
...-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5 ++
3 files changed, 95 insertions(+), 4 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst
diff --git a/Lib/http/client.py b/Lib/http/client.py
index d1b7b1048c9171..c8ab5b7662c334 100644
--- a/Lib/http/client.py
+++ b/Lib/http/client.py
@@ -111,6 +111,11 @@
_MAXLINE = 65536
_MAXHEADERS = 100
+# Data larger than this will be read in chunks, to prevent extreme
+# overallocation.
+_MIN_READ_BUF_SIZE = 1 << 20
+
+
# Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2)
#
# VCHAR = %x21-7E
@@ -628,10 +633,25 @@ def _safe_read(self, amt):
reading. If the bytes are truly not available (due to EOF), then the
IncompleteRead exception can be used to detect the problem.
"""
- data = self.fp.read(amt)
- if len(data) < amt:
- raise IncompleteRead(data, amt-len(data))
- return data
+ cursize = min(amt, _MIN_READ_BUF_SIZE)
+ data = self.fp.read(cursize)
+ if len(data) >= amt:
+ return data
+ if len(data) < cursize:
+ raise IncompleteRead(data, amt - len(data))
+
+ data = io.BytesIO(data)
+ data.seek(0, 2)
+ while True:
+ # This is a geometric increase in read size (never more than
+ # doubling out the current length of data per loop iteration).
+ delta = min(cursize, amt - cursize)
+ data.write(self.fp.read(delta))
+ if data.tell() >= amt:
+ return data.getvalue()
+ cursize += delta
+ if data.tell() < cursize:
+ raise IncompleteRead(data.getvalue(), amt - data.tell())
def _safe_readinto(self, b):
"""Same as _safe_read, but for reading into a buffer."""
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
index 77152cf64565e0..89ec5f6f1c5383 100644
--- a/Lib/test/test_httplib.py
+++ b/Lib/test/test_httplib.py
@@ -1226,6 +1226,72 @@ def run_server():
thread.join()
self.assertEqual(result, b"proxied data\n")
+ def test_large_content_length(self):
+ serv = socket.create_server((HOST, 0))
+ self.addCleanup(serv.close)
+
+ def run_server():
+ [conn, address] = serv.accept()
+ with conn:
+ while conn.recv(1024):
+ conn.sendall(
+ b"HTTP/1.1 200 Ok\r\n"
+ b"Content-Length: %d\r\n"
+ b"\r\n" % size)
+ conn.sendall(b'A' * (size//3))
+ conn.sendall(b'B' * (size - size//3))
+
+ thread = threading.Thread(target=run_server)
+ thread.start()
+ self.addCleanup(thread.join, 1.0)
+
+ conn = client.HTTPConnection(*serv.getsockname())
+ try:
+ for w in range(15, 27):
+ size = 1 << w
+ conn.request("GET", "/")
+ with conn.getresponse() as response:
+ self.assertEqual(len(response.read()), size)
+ finally:
+ conn.close()
+ thread.join(1.0)
+
+ def test_large_content_length_truncated(self):
+ serv = socket.create_server((HOST, 0))
+ self.addCleanup(serv.close)
+
+ def run_server():
+ while True:
+ [conn, address] = serv.accept()
+ with conn:
+ conn.recv(1024)
+ if not size:
+ break
+ conn.sendall(
+ b"HTTP/1.1 200 Ok\r\n"
+ b"Content-Length: %d\r\n"
+ b"\r\n"
+ b"Text" % size)
+
+ thread = threading.Thread(target=run_server)
+ thread.start()
+ self.addCleanup(thread.join, 1.0)
+
+ conn = client.HTTPConnection(*serv.getsockname())
+ try:
+ for w in range(18, 65):
+ size = 1 << w
+ conn.request("GET", "/")
+ with conn.getresponse() as response:
+ self.assertRaises(client.IncompleteRead, response.read)
+ conn.close()
+ finally:
+ conn.close()
+ size = 0
+ conn.request("GET", "/")
+ conn.close()
+ thread.join(1.0)
+
def test_putrequest_override_domain_validation(self):
"""
It should be possible to override the default validation
diff --git a/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst b/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst
new file mode 100644
index 00000000000000..6d6f25cd2f8bf7
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst
@@ -0,0 +1,5 @@
+Fix a potential memory denial of service in the :mod:`http.client` module.
+When connecting to a malicious server, it could cause
+an arbitrary amount of memory to be allocated.
+This could have led to symptoms including a :exc:`MemoryError`, swapping, out
+of memory (OOM) killed processes or containers, or even system crashes.

View File

@@ -1,160 +0,0 @@
From e99059d800b741504ef18693803927a0dc062be4 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
Date: Mon, 1 Dec 2025 17:28:15 +0200
Subject: [PATCH] [3.10] gh-119342: Fix a potential denial of service in
plistlib (GH-119343)
Reading a specially prepared small Plist file could cause OOM because file's
read(n) preallocates a bytes object for reading the specified amount of
data. Now plistlib reads large data by chunks, therefore the upper limit of
consumed memory is proportional to the size of the input file.
(cherry picked from commit 694922cf40aa3a28f898b5f5ee08b71b4922df70)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
---
Lib/plistlib.py | 31 ++++++++++------
Lib/test/test_plistlib.py | 37 +++++++++++++++++--
...-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst | 5 +++
3 files changed, 59 insertions(+), 14 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst
diff --git a/Lib/plistlib.py b/Lib/plistlib.py
index d6c997efe9c5f5..c80dfee02a3335 100644
--- a/Lib/plistlib.py
+++ b/Lib/plistlib.py
@@ -73,6 +73,9 @@
PlistFormat = enum.Enum('PlistFormat', 'FMT_XML FMT_BINARY', module=__name__)
globals().update(PlistFormat.__members__)
+# Data larger than this will be read in chunks, to prevent extreme
+# overallocation.
+_MIN_READ_BUF_SIZE = 1 << 20
class UID:
def __init__(self, data):
@@ -499,12 +502,24 @@ def _get_size(self, tokenL):
return tokenL
+ def _read(self, size):
+ cursize = min(size, _MIN_READ_BUF_SIZE)
+ data = self._fp.read(cursize)
+ while True:
+ if len(data) != cursize:
+ raise InvalidFileException
+ if cursize == size:
+ return data
+ delta = min(cursize, size - cursize)
+ data += self._fp.read(delta)
+ cursize += delta
+
def _read_ints(self, n, size):
- data = self._fp.read(size * n)
+ data = self._read(size * n)
if size in _BINARY_FORMAT:
return struct.unpack(f'>{n}{_BINARY_FORMAT[size]}', data)
else:
- if not size or len(data) != size * n:
+ if not size:
raise InvalidFileException()
return tuple(int.from_bytes(data[i: i + size], 'big')
for i in range(0, size * n, size))
@@ -561,22 +576,16 @@ def _read_object(self, ref):
elif tokenH == 0x40: # data
s = self._get_size(tokenL)
- result = self._fp.read(s)
- if len(result) != s:
- raise InvalidFileException()
+ result = self._read(s)
elif tokenH == 0x50: # ascii string
s = self._get_size(tokenL)
- data = self._fp.read(s)
- if len(data) != s:
- raise InvalidFileException()
+ data = self._read(s)
result = data.decode('ascii')
elif tokenH == 0x60: # unicode string
s = self._get_size(tokenL) * 2
- data = self._fp.read(s)
- if len(data) != s:
- raise InvalidFileException()
+ data = self._read(s)
result = data.decode('utf-16be')
elif tokenH == 0x80: # UID
diff --git a/Lib/test/test_plistlib.py b/Lib/test/test_plistlib.py
index ef96c6ceda21a2..d3836991d212cd 100644
--- a/Lib/test/test_plistlib.py
+++ b/Lib/test/test_plistlib.py
@@ -838,8 +838,7 @@ def test_xml_plist_with_entity_decl(self):
class TestBinaryPlistlib(unittest.TestCase):
- @staticmethod
- def decode(*objects, offset_size=1, ref_size=1):
+ def build(self, *objects, offset_size=1, ref_size=1):
data = [b'bplist00']
offset = 8
offsets = []
@@ -851,7 +850,11 @@ def decode(*objects, offset_size=1, ref_size=1):
len(objects), 0, offset)
data.extend(offsets)
data.append(tail)
- return plistlib.loads(b''.join(data), fmt=plistlib.FMT_BINARY)
+ return b''.join(data)
+
+ def decode(self, *objects, offset_size=1, ref_size=1):
+ data = self.build(*objects, offset_size=offset_size, ref_size=ref_size)
+ return plistlib.loads(data, fmt=plistlib.FMT_BINARY)
def test_nonstandard_refs_size(self):
# Issue #21538: Refs and offsets are 24-bit integers
@@ -959,6 +962,34 @@ def test_invalid_binary(self):
with self.assertRaises(plistlib.InvalidFileException):
plistlib.loads(b'bplist00' + data, fmt=plistlib.FMT_BINARY)
+ def test_truncated_large_data(self):
+ self.addCleanup(os_helper.unlink, os_helper.TESTFN)
+ def check(data):
+ with open(os_helper.TESTFN, 'wb') as f:
+ f.write(data)
+ # buffered file
+ with open(os_helper.TESTFN, 'rb') as f:
+ with self.assertRaises(plistlib.InvalidFileException):
+ plistlib.load(f, fmt=plistlib.FMT_BINARY)
+ # unbuffered file
+ with open(os_helper.TESTFN, 'rb', buffering=0) as f:
+ with self.assertRaises(plistlib.InvalidFileException):
+ plistlib.load(f, fmt=plistlib.FMT_BINARY)
+ for w in range(20, 64):
+ s = 1 << w
+ # data
+ check(self.build(b'\x4f\x13' + s.to_bytes(8, 'big')))
+ # ascii string
+ check(self.build(b'\x5f\x13' + s.to_bytes(8, 'big')))
+ # unicode string
+ check(self.build(b'\x6f\x13' + s.to_bytes(8, 'big')))
+ # array
+ check(self.build(b'\xaf\x13' + s.to_bytes(8, 'big')))
+ # dict
+ check(self.build(b'\xdf\x13' + s.to_bytes(8, 'big')))
+ # number of objects
+ check(b'bplist00' + struct.pack('>6xBBQQQ', 1, 1, s, 0, 8))
+
class TestKeyedArchive(unittest.TestCase):
def test_keyed_archive_data(self):
diff --git a/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst b/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst
new file mode 100644
index 00000000000000..04fd8faca4cf7e
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2024-05-21-22-11-31.gh-issue-119342.BTFj4Z.rst
@@ -0,0 +1,5 @@
+Fix a potential memory denial of service in the :mod:`plistlib` module.
+When reading a Plist file received from untrusted source, it could cause
+an arbitrary amount of memory to be allocated.
+This could have led to symptoms including a :exc:`MemoryError`, swapping, out
+of memory (OOM) killed processes or containers, or even system crashes.

View File

@@ -1,64 +0,0 @@
From d6c6f0880dbc6ffd770b859087f4cd749a1d0dbb Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Tue, 20 Jan 2026 14:45:58 -0600
Subject: [PATCH] [3.10] gh-143925: Reject control characters in data: URL
mediatypes (cherry picked from commit
f25509e78e8be6ea73c811ac2b8c928c28841b9f) (cherry picked from commit
2c9c746077d8119b5bcf5142316992e464594946)
Co-authored-by: Seth Michael Larson <seth@python.org>
---
Lib/test/test_urllib.py | 8 ++++++++
Lib/urllib/request.py | 5 +++++
Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst | 1 +
3 files changed, 14 insertions(+)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst
Index: Python-3.10.19/Lib/test/test_urllib.py
===================================================================
--- Python-3.10.19.orig/Lib/test/test_urllib.py 2026-02-12 01:05:56.127447144 +0100
+++ Python-3.10.19/Lib/test/test_urllib.py 2026-02-12 01:08:02.226352573 +0100
@@ -11,6 +11,7 @@
from test import support
from test.support import os_helper
from test.support import warnings_helper
+from test.support import control_characters_c0
import os
try:
import ssl
@@ -683,6 +684,13 @@
# missing padding character
self.assertRaises(ValueError,urllib.request.urlopen,'data:;base64,Cg=')
+ def test_invalid_mediatype(self):
+ for c0 in control_characters_c0():
+ self.assertRaises(ValueError,urllib.request.urlopen,
+ f'data:text/html;{c0},data')
+ for c0 in control_characters_c0():
+ self.assertRaises(ValueError,urllib.request.urlopen,
+ f'data:text/html{c0};base64,ZGF0YQ==')
class urlretrieve_FileTests(unittest.TestCase):
"""Test urllib.urlretrieve() on local files"""
Index: Python-3.10.19/Lib/urllib/request.py
===================================================================
--- Python-3.10.19.orig/Lib/urllib/request.py 2026-02-12 01:05:56.627830069 +0100
+++ Python-3.10.19/Lib/urllib/request.py 2026-02-12 01:08:02.226810828 +0100
@@ -1654,6 +1654,11 @@
scheme, data = url.split(":",1)
mediatype, data = data.split(",",1)
+ # Disallow control characters within mediatype.
+ if re.search(r"[\x00-\x1F\x7F]", mediatype):
+ raise ValueError(
+ "Control characters not allowed in data: mediatype")
+
# even base64 encoded data URLs might be quoted so unquote in any case:
data = unquote_to_bytes(data)
if mediatype.endswith(";base64"):
Index: Python-3.10.19/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.10.19/Misc/NEWS.d/next/Security/2026-01-16-11-51-19.gh-issue-143925.mrtcHW.rst 2026-02-12 01:08:02.227192287 +0100
@@ -0,0 +1 @@
+Reject control characters in ``data:`` URL media types.

View File

@@ -1,56 +0,0 @@
From 7485ee5e2cf81d3e5ad0d9c3be73cecd2ab4eec7 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Fri, 16 Jan 2026 10:54:09 -0600
Subject: [PATCH 1/2] Add 'test.support' fixture for C0 control characters
---
Lib/imaplib.py | 4 +++-
Lib/test/test_imaplib.py | 6 ++++++
Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst | 1 +
3 files changed, 10 insertions(+), 1 deletion(-)
Index: Python-3.10.19/Lib/imaplib.py
===================================================================
--- Python-3.10.19.orig/Lib/imaplib.py 2026-02-12 01:05:53.821319313 +0100
+++ Python-3.10.19/Lib/imaplib.py 2026-02-12 01:06:28.558652908 +0100
@@ -132,7 +132,7 @@
# We compile these in _mode_xxx.
_Literal = br'.*{(?P<size>\d+)}$'
_Untagged_status = br'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?'
-
+_control_chars = re.compile(b'[\x00-\x1F\x7F]')
class IMAP4:
@@ -994,6 +994,8 @@
if arg is None: continue
if isinstance(arg, str):
arg = bytes(arg, self._encoding)
+ if _control_chars.search(arg):
+ raise ValueError("Control characters not allowed in commands")
data = data + b' ' + arg
literal = self.literal
Index: Python-3.10.19/Lib/test/test_imaplib.py
===================================================================
--- Python-3.10.19.orig/Lib/test/test_imaplib.py 2026-02-12 01:05:55.293033311 +0100
+++ Python-3.10.19/Lib/test/test_imaplib.py 2026-02-12 01:07:45.387053336 +0100
@@ -538,6 +538,12 @@
self.assertEqual(data[0], b'Returned to authenticated state. (Success)')
self.assertEqual(client.state, 'AUTH')
+ def test_control_characters(self):
+ client, _ = self._setup(SimpleIMAPHandler)
+ for c0 in support.control_characters_c0():
+ with self.assertRaises(ValueError):
+ client.login(f'user{c0}', 'pass')
+
class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase):
imap_class = imaplib.IMAP4
Index: Python-3.10.19/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.10.19/Misc/NEWS.d/next/Security/2026-01-16-11-41-06.gh-issue-143921.AeCOor.rst 2026-02-12 01:06:28.559224837 +0100
@@ -0,0 +1 @@
+Reject control characters in IMAP commands.

View File

@@ -1,56 +0,0 @@
From b6f733b285b1c4f27dacb5c2e1f292c914e8b933 Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Fri, 16 Jan 2026 10:54:09 -0600
Subject: [PATCH 1/2] Add 'test.support' fixture for C0 control characters
---
Lib/poplib.py | 2 ++
Lib/test/test_poplib.py | 8 ++++++++
Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst | 1 +
3 files changed, 11 insertions(+)
Index: Python-3.10.19/Lib/poplib.py
===================================================================
--- Python-3.10.19.orig/Lib/poplib.py 2026-02-12 01:05:54.262197434 +0100
+++ Python-3.10.19/Lib/poplib.py 2026-02-12 01:12:02.945762019 +0100
@@ -122,6 +122,8 @@
def _putcmd(self, line):
if self._debugging: print('*cmd*', repr(line))
line = bytes(line, self.encoding)
+ if re.search(b'[\x00-\x1F\x7F]', line):
+ raise ValueError('Control characters not allowed in commands')
self._putline(line)
Index: Python-3.10.19/Lib/test/test_poplib.py
===================================================================
--- Python-3.10.19.orig/Lib/test/test_poplib.py 2026-02-12 01:05:55.796995175 +0100
+++ Python-3.10.19/Lib/test/test_poplib.py 2026-02-12 01:12:32.837694637 +0100
@@ -15,6 +15,7 @@
from test.support import hashlib_helper
from test.support import socket_helper
from test.support import threading_helper
+from test.support import control_characters_c0
import warnings
with warnings.catch_warnings():
@@ -365,6 +366,13 @@
self.assertIsNone(self.client.sock)
self.assertIsNone(self.client.file)
+ def test_control_characters(self):
+ for c0 in control_characters_c0():
+ with self.assertRaises(ValueError):
+ self.client.user(f'user{c0}')
+ with self.assertRaises(ValueError):
+ self.client.pass_(f'{c0}pass')
+
@requires_ssl
def test_stls_capa(self):
capa = self.client.capa()
Index: Python-3.10.19/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst
===================================================================
--- /dev/null 1970-01-01 00:00:00.000000000 +0000
+++ Python-3.10.19/Misc/NEWS.d/next/Security/2026-01-16-11-43-47.gh-issue-143923.DuytMe.rst 2026-02-12 01:12:02.946199975 +0100
@@ -0,0 +1 @@
+Reject control characters in POP3 commands.

View File

@@ -1,359 +0,0 @@
From 20044636f0d9a802c42f907934c69d46e4019a0c Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka <storchaka@gmail.com>
Date: Fri, 31 Oct 2025 15:49:51 +0200
Subject: [PATCH] [3.10] gh-136065: Fix quadratic complexity in
os.path.expandvars() (GH-134952) (cherry picked from commit
f029e8db626ddc6e3a3beea4eff511a71aaceb5c)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
---
Lib/ntpath.py | 126 ++++++------------
Lib/posixpath.py | 43 +++---
Lib/test/test_genericpath.py | 14 ++
Lib/test/test_ntpath.py | 20 ++-
...-05-30-22-33-27.gh-issue-136065.bu337o.rst | 1 +
5 files changed, 93 insertions(+), 111 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
index 9b0cca44727fd5..bd2b4e289c17b9 100644
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -374,17 +374,23 @@ def expanduser(path):
# XXX With COMMAND.COM you can use any characters in a variable name,
# XXX except '^|<>='.
+_varpattern = r"'[^']*'?|%(%|[^%]*%?)|\$(\$|[-\w]+|\{[^}]*\}?)"
+_varsub = None
+_varsubb = None
+
def expandvars(path):
"""Expand shell variables of the forms $var, ${var} and %var%.
Unknown variables are left unchanged."""
path = os.fspath(path)
+ global _varsub, _varsubb
if isinstance(path, bytes):
if b'$' not in path and b'%' not in path:
return path
- import string
- varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii')
- quote = b'\''
+ if not _varsubb:
+ import re
+ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
+ sub = _varsubb
percent = b'%'
brace = b'{'
rbrace = b'}'
@@ -393,94 +399,44 @@ def expandvars(path):
else:
if '$' not in path and '%' not in path:
return path
- import string
- varchars = string.ascii_letters + string.digits + '_-'
- quote = '\''
+ if not _varsub:
+ import re
+ _varsub = re.compile(_varpattern, re.ASCII).sub
+ sub = _varsub
percent = '%'
brace = '{'
rbrace = '}'
dollar = '$'
environ = os.environ
- res = path[:0]
- index = 0
- pathlen = len(path)
- while index < pathlen:
- c = path[index:index+1]
- if c == quote: # no expansion within single quotes
- path = path[index + 1:]
- pathlen = len(path)
- try:
- index = path.index(c)
- res += c + path[:index + 1]
- except ValueError:
- res += c + path
- index = pathlen - 1
- elif c == percent: # variable or '%'
- if path[index + 1:index + 2] == percent:
- res += c
- index += 1
- else:
- path = path[index+1:]
- pathlen = len(path)
- try:
- index = path.index(percent)
- except ValueError:
- res += percent + path
- index = pathlen - 1
- else:
- var = path[:index]
- try:
- if environ is None:
- value = os.fsencode(os.environ[os.fsdecode(var)])
- else:
- value = environ[var]
- except KeyError:
- value = percent + var + percent
- res += value
- elif c == dollar: # variable or '$$'
- if path[index + 1:index + 2] == dollar:
- res += c
- index += 1
- elif path[index + 1:index + 2] == brace:
- path = path[index+2:]
- pathlen = len(path)
- try:
- index = path.index(rbrace)
- except ValueError:
- res += dollar + brace + path
- index = pathlen - 1
- else:
- var = path[:index]
- try:
- if environ is None:
- value = os.fsencode(os.environ[os.fsdecode(var)])
- else:
- value = environ[var]
- except KeyError:
- value = dollar + brace + var + rbrace
- res += value
- else:
- var = path[:0]
- index += 1
- c = path[index:index + 1]
- while c and c in varchars:
- var += c
- index += 1
- c = path[index:index + 1]
- try:
- if environ is None:
- value = os.fsencode(os.environ[os.fsdecode(var)])
- else:
- value = environ[var]
- except KeyError:
- value = dollar + var
- res += value
- if c:
- index -= 1
+
+ def repl(m):
+ lastindex = m.lastindex
+ if lastindex is None:
+ return m[0]
+ name = m[lastindex]
+ if lastindex == 1:
+ if name == percent:
+ return name
+ if not name.endswith(percent):
+ return m[0]
+ name = name[:-1]
else:
- res += c
- index += 1
- return res
+ if name == dollar:
+ return name
+ if name.startswith(brace):
+ if not name.endswith(rbrace):
+ return m[0]
+ name = name[1:-1]
+
+ try:
+ if environ is None:
+ return os.fsencode(os.environ[os.fsdecode(name)])
+ else:
+ return environ[name]
+ except KeyError:
+ return m[0]
+
+ return sub(repl, path)
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B.
diff --git a/Lib/posixpath.py b/Lib/posixpath.py
index b8dd563ada3880..75020eef477b5e 100644
--- a/Lib/posixpath.py
+++ b/Lib/posixpath.py
@@ -279,42 +279,41 @@ def expanduser(path):
# This expands the forms $variable and ${variable} only.
# Non-existent variables are left unchanged.
-_varprog = None
-_varprogb = None
+_varpattern = r'\$(\w+|\{[^}]*\}?)'
+_varsub = None
+_varsubb = None
def expandvars(path):
"""Expand shell variables of form $var and ${var}. Unknown variables
are left unchanged."""
path = os.fspath(path)
- global _varprog, _varprogb
+ global _varsub, _varsubb
if isinstance(path, bytes):
if b'$' not in path:
return path
- if not _varprogb:
+ if not _varsubb:
import re
- _varprogb = re.compile(br'\$(\w+|\{[^}]*\})', re.ASCII)
- search = _varprogb.search
+ _varsubb = re.compile(_varpattern.encode(), re.ASCII).sub
+ sub = _varsubb
start = b'{'
end = b'}'
environ = getattr(os, 'environb', None)
else:
if '$' not in path:
return path
- if not _varprog:
+ if not _varsub:
import re
- _varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII)
- search = _varprog.search
+ _varsub = re.compile(_varpattern, re.ASCII).sub
+ sub = _varsub
start = '{'
end = '}'
environ = os.environ
- i = 0
- while True:
- m = search(path, i)
- if not m:
- break
- i, j = m.span(0)
- name = m.group(1)
- if name.startswith(start) and name.endswith(end):
+
+ def repl(m):
+ name = m[1]
+ if name.startswith(start):
+ if not name.endswith(end):
+ return m[0]
name = name[1:-1]
try:
if environ is None:
@@ -322,13 +321,11 @@ def expandvars(path):
else:
value = environ[name]
except KeyError:
- i = j
+ return m[0]
else:
- tail = path[j:]
- path = path[:i] + value
- i = len(path)
- path += tail
- return path
+ return value
+
+ return sub(repl, path)
# Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B.
diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py
index 1ff7f75ad3e614..b0a13265c98b4e 100644
--- a/Lib/test/test_genericpath.py
+++ b/Lib/test/test_genericpath.py
@@ -7,6 +7,7 @@
import sys
import unittest
import warnings
+from test import support
from test.support import os_helper
from test.support import warnings_helper
from test.support.script_helper import assert_python_ok
@@ -430,6 +431,19 @@ def check(value, expected):
os.fsencode('$bar%s bar' % nonascii))
check(b'$spam}bar', os.fsencode('%s}bar' % nonascii))
+ @support.requires_resource('cpu')
+ def test_expandvars_large(self):
+ expandvars = self.pathmodule.expandvars
+ with os_helper.EnvironmentVarGuard() as env:
+ env.clear()
+ env["A"] = "B"
+ n = 100_000
+ self.assertEqual(expandvars('$A'*n), 'B'*n)
+ self.assertEqual(expandvars('${A}'*n), 'B'*n)
+ self.assertEqual(expandvars('$A!'*n), 'B!'*n)
+ self.assertEqual(expandvars('${A}A'*n), 'BA'*n)
+ self.assertEqual(expandvars('${'*10*n), '${'*10*n)
+
def test_abspath(self):
self.assertIn("foo", self.pathmodule.abspath("foo"))
with warnings.catch_warnings():
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index f790f771f2fb6d..161e57d62a06d3 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -5,8 +5,8 @@
import unittest
import warnings
from ntpath import ALLOW_MISSING
+from test import support
from test.support import os_helper
-from test.support import TestFailed
from test.support.os_helper import FakePath
from test import test_genericpath
from tempfile import TemporaryFile
@@ -56,7 +56,7 @@ def tester(fn, wantResult):
fn = fn.replace("\\", "\\\\")
gotResult = eval(fn)
if wantResult != gotResult and _norm(wantResult) != _norm(gotResult):
- raise TestFailed("%s should return: %s but returned: %s" \
+ raise support.TestFailed("%s should return: %s but returned: %s" \
%(str(fn), str(wantResult), str(gotResult)))
# then with bytes
@@ -72,7 +72,7 @@ def tester(fn, wantResult):
warnings.simplefilter("ignore", DeprecationWarning)
gotResult = eval(fn)
if _norm(wantResult) != _norm(gotResult):
- raise TestFailed("%s should return: %s but returned: %s" \
+ raise support.TestFailed("%s should return: %s but returned: %s" \
%(str(fn), str(wantResult), repr(gotResult)))
@@ -689,6 +689,19 @@ def check(value, expected):
check('%spam%bar', '%sbar' % nonascii)
check('%{}%bar'.format(nonascii), 'ham%sbar' % nonascii)
+ @support.requires_resource('cpu')
+ def test_expandvars_large(self):
+ expandvars = ntpath.expandvars
+ with os_helper.EnvironmentVarGuard() as env:
+ env.clear()
+ env["A"] = "B"
+ n = 100_000
+ self.assertEqual(expandvars('%A%'*n), 'B'*n)
+ self.assertEqual(expandvars('%A%A'*n), 'BA'*n)
+ self.assertEqual(expandvars("''"*n + '%%'), "''"*n + '%')
+ self.assertEqual(expandvars("%%"*n), "%"*n)
+ self.assertEqual(expandvars("$$"*n), "$"*n)
+
def test_expanduser(self):
tester('ntpath.expanduser("test")', 'test')
@@ -923,6 +936,7 @@ def test_nt_helpers(self):
self.assertIsInstance(b_final_path, bytes)
self.assertGreater(len(b_final_path), 0)
+
class NtCommonTest(test_genericpath.CommonTest, unittest.TestCase):
pathmodule = ntpath
attributes = ['relpath']
diff --git a/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
new file mode 100644
index 00000000000000..1d152bb5318380
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2025-05-30-22-33-27.gh-issue-136065.bu337o.rst
@@ -0,0 +1 @@
+Fix quadratic complexity in :func:`os.path.expandvars`.

View File

@@ -1,184 +0,0 @@
From 57c5ecd7e61fbb24e7de76eafd95332bd0ae4dea Mon Sep 17 00:00:00 2001
From: Seth Michael Larson <seth@python.org>
Date: Tue, 20 Jan 2026 15:23:42 -0600
Subject: [PATCH] [3.10] gh-143919: Reject control characters in http cookies
(cherry picked from commit 95746b3a13a985787ef53b977129041971ed7f70)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Seth Michael Larson <seth@python.org>
Co-authored-by: Bartosz Sławecki <bartosz@ilikepython.com>
Co-authored-by: sobolevn <mail@sobolevn.me>
---
Doc/library/http.cookies.rst | 4 +-
Lib/http/cookies.py | 25 +++++++--
Lib/test/test_http_cookies.py | 52 +++++++++++++++++--
...-01-16-11-13-15.gh-issue-143919.kchwZV.rst | 1 +
4 files changed, 73 insertions(+), 9 deletions(-)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
diff --git a/Doc/library/http.cookies.rst b/Doc/library/http.cookies.rst
index a2c1eb00d8b33d..4cb563c230ea5e 100644
--- a/Doc/library/http.cookies.rst
+++ b/Doc/library/http.cookies.rst
@@ -270,9 +270,9 @@ The following example demonstrates how to use the :mod:`http.cookies` module.
Set-Cookie: chips=ahoy
Set-Cookie: vienna=finger
>>> C = cookies.SimpleCookie()
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
>>> print(C)
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
>>> C = cookies.SimpleCookie()
>>> C["oreo"] = "doublestuff"
>>> C["oreo"]["path"] = "/"
diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py
index 2c1f021d0abede..5cfa7a8072c7f7 100644
--- a/Lib/http/cookies.py
+++ b/Lib/http/cookies.py
@@ -87,9 +87,9 @@
such trickeries do not confuse it.
>>> C = cookies.SimpleCookie()
- >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";')
+ >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=;";')
>>> print(C)
- Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;"
+ Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=;"
Each element of the Cookie also supports all of the RFC 2109
Cookie attributes. Here's an example which sets the Path
@@ -170,6 +170,15 @@ class CookieError(Exception):
})
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
+_control_character_re = re.compile(r'[\x00-\x1F\x7F]')
+
+
+def _has_control_character(*val):
+ """Detects control characters within a value.
+ Supports any type, as header values can be any type.
+ """
+ return any(_control_character_re.search(str(v)) for v in val)
+
def _quote(str):
r"""Quote a string for use in a cookie header.
@@ -292,12 +301,16 @@ def __setitem__(self, K, V):
K = K.lower()
if not K in self._reserved:
raise CookieError("Invalid attribute %r" % (K,))
+ if _has_control_character(K, V):
+ raise CookieError(f"Control characters are not allowed in cookies {K!r} {V!r}")
dict.__setitem__(self, K, V)
def setdefault(self, key, val=None):
key = key.lower()
if key not in self._reserved:
raise CookieError("Invalid attribute %r" % (key,))
+ if _has_control_character(key, val):
+ raise CookieError("Control characters are not allowed in cookies %r %r" % (key, val,))
return dict.setdefault(self, key, val)
def __eq__(self, morsel):
@@ -333,6 +346,9 @@ def set(self, key, val, coded_val):
raise CookieError('Attempt to set a reserved key %r' % (key,))
if not _is_legal_key(key):
raise CookieError('Illegal key %r' % (key,))
+ if _has_control_character(key, val, coded_val):
+ raise CookieError(
+ "Control characters are not allowed in cookies %r %r %r" % (key, val, coded_val,))
# It's a good key, so save it.
self._key = key
@@ -484,7 +500,10 @@ def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"):
result = []
items = sorted(self.items())
for key, value in items:
- result.append(value.output(attrs, header))
+ value_output = value.output(attrs, header)
+ if _has_control_character(value_output):
+ raise CookieError("Control characters are not allowed in cookies")
+ result.append(value_output)
return sep.join(result)
__str__ = output
diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py
index 644e75cd5b742e..1f2c049fa811fa 100644
--- a/Lib/test/test_http_cookies.py
+++ b/Lib/test/test_http_cookies.py
@@ -17,10 +17,10 @@ def test_basic(self):
'repr': "<SimpleCookie: chips='ahoy' vienna='finger'>",
'output': 'Set-Cookie: chips=ahoy\nSet-Cookie: vienna=finger'},
- {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
- 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=\012;'},
- 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=\\n;'>''',
- 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"'},
+ {'data': 'keebler="E=mc2; L=\\"Loves\\"; fudge=;"',
+ 'dict': {'keebler' : 'E=mc2; L="Loves"; fudge=;'},
+ 'repr': '''<SimpleCookie: keebler='E=mc2; L="Loves"; fudge=;'>''',
+ 'output': 'Set-Cookie: keebler="E=mc2; L=\\"Loves\\"; fudge=;"'},
# Check illegal cookies that have an '=' char in an unquoted value
{'data': 'keebler=E=mc2',
@@ -517,6 +517,50 @@ def test_repr(self):
r'Set-Cookie: key=coded_val; '
r'expires=\w+, \d+ \w+ \d+ \d+:\d+:\d+ \w+')
+ def test_control_characters(self):
+ for c0 in support.control_characters_c0():
+ morsel = cookies.Morsel()
+
+ # .__setitem__()
+ with self.assertRaises(cookies.CookieError):
+ morsel[c0] = "val"
+ with self.assertRaises(cookies.CookieError):
+ morsel["path"] = c0
+
+ # .setdefault()
+ with self.assertRaises(cookies.CookieError):
+ morsel.setdefault("path", c0)
+ with self.assertRaises(cookies.CookieError):
+ morsel.setdefault(c0, "val")
+
+ # .set()
+ with self.assertRaises(cookies.CookieError):
+ morsel.set(c0, "val", "coded-value")
+ with self.assertRaises(cookies.CookieError):
+ morsel.set("path", c0, "coded-value")
+ with self.assertRaises(cookies.CookieError):
+ morsel.set("path", "val", c0)
+
+ def test_control_characters_output(self):
+ # Tests that even if the internals of Morsel are modified
+ # that a call to .output() has control character safeguards.
+ for c0 in support.control_characters_c0():
+ morsel = cookies.Morsel()
+ morsel.set("key", "value", "coded-value")
+ morsel._key = c0 # Override private variable.
+ cookie = cookies.SimpleCookie()
+ cookie["cookie"] = morsel
+ with self.assertRaises(cookies.CookieError):
+ cookie.output()
+
+ morsel = cookies.Morsel()
+ morsel.set("key", "value", "coded-value")
+ morsel._coded_value = c0 # Override private variable.
+ cookie = cookies.SimpleCookie()
+ cookie["cookie"] = morsel
+ with self.assertRaises(cookies.CookieError):
+ cookie.output()
+
def test_main():
run_unittest(CookieTests, MorselTests)
run_doctest(cookies)
diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
new file mode 100644
index 00000000000000..788c3e4ac2ebf7
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-13-15.gh-issue-143919.kchwZV.rst
@@ -0,0 +1 @@
+Reject control characters in :class:`http.cookies.Morsel` fields and values.

View File

@@ -1,97 +0,0 @@
From 123bfbbe9074ef7fa28e1e7b25575665296560fa Mon Sep 17 00:00:00 2001
From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com>
Date: Sat, 17 Jan 2026 10:23:57 -0800
Subject: [PATCH] [3.10] gh-143916: Reject control characters in
wsgiref.headers.Headers (GH-143917) (GH-143973)
gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917)
* Add 'test.support' fixture for C0 control characters
* gh-143916: Reject control characters in wsgiref.headers.Headers
(cherry picked from commit f7fceed79ca1bceae8dbe5ba5bc8928564da7211)
(cherry picked from commit 22e4d55285cee52bc4dbe061324e5f30bd4dee58)
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Co-authored-by: Seth Michael Larson <seth@python.org>
---
Lib/test/support/__init__.py | 7 +++++++
Lib/test/test_wsgiref.py | 12 +++++++++++-
Lib/wsgiref/headers.py | 3 +++
.../2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2 ++
4 files changed, 23 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 0d3b9634f10248..d0492fe1914343 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -2157,3 +2157,10 @@ def adjust_int_max_str_digits(max_digits):
yield
finally:
sys.set_int_max_str_digits(current)
+
+
+def control_characters_c0() -> list[str]:
+ """Returns a list of C0 control characters as strings.
+ C0 control characters defined as the byte range 0x00-0x1F, and 0x7F.
+ """
+ return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"]
diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py
index 42094f467731d4..01ca51ba458587 100644
--- a/Lib/test/test_wsgiref.py
+++ b/Lib/test/test_wsgiref.py
@@ -1,6 +1,6 @@
from unittest import mock
from test import support
-from test.support import socket_helper
+from test.support import socket_helper, control_characters_c0
from test.support import warnings_helper
from test.test_httpservers import NoLogRequestHandler
from unittest import TestCase
@@ -527,6 +527,16 @@ def testExtras(self):
'\r\n'
)
+ def testRaisesControlCharacters(self):
+ headers = Headers()
+ for c0 in control_characters_c0():
+ self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val")
+ self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}")
+ self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param")
+ self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param")
+ self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}")
+
+
class ErrorHandler(BaseCGIHandler):
"""Simple handler subclass for testing BaseHandler"""
diff --git a/Lib/wsgiref/headers.py b/Lib/wsgiref/headers.py
index fab851c5a44430..fd98e85d75492b 100644
--- a/Lib/wsgiref/headers.py
+++ b/Lib/wsgiref/headers.py
@@ -9,6 +9,7 @@
# existence of which force quoting of the parameter value.
import re
tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
+_control_chars_re = re.compile(r'[\x00-\x1F\x7F]')
def _formatparam(param, value=None, quote=1):
"""Convenience function to format and return a key=value pair.
@@ -41,6 +42,8 @@ def __init__(self, headers=None):
def _convert_string_type(self, value):
"""Convert/check value type."""
if type(value) is str:
+ if _control_chars_re.search(value):
+ raise ValueError("Control characters not allowed in headers")
return value
raise AssertionError("Header names/values must be"
" of type str (got {0})".format(repr(value)))
diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst
new file mode 100644
index 00000000000000..44bd0b27059f94
--- /dev/null
+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst
@@ -0,0 +1,2 @@
+Reject C0 control characters within wsgiref.headers.Headers fields, values,
+and parameters.

BIN
Python-3.10.14.tar.xz LFS Normal file

Binary file not shown.

16
Python-3.10.14.tar.xz.asc Normal file
View File

@@ -0,0 +1,16 @@
-----BEGIN PGP SIGNATURE-----
iQIzBAABCAAdFiEEz9yiRbEEPPKl+Xhl/+h0BBaL2EcFAmX76AkACgkQ/+h0BBaL
2EeKvhAAuN+7X3iFv8tYwUTbKJT9x9fLsADI5wOn5j8xuDiXQCOMzsqqyB1RSdEd
tbCQXg9XJj1bVHc4DY337vUix9jvFcTqbQqlzUm/peX4buY2bKkZu2quti1iWSJf
IN26jDYO2TobPvGdvNiH2Hceqe1dc5tU7iYEfaLR5ImgO4aGgK6x4DiLdmFqo2bk
ZZWZLkXbwenrSdLVmUZLP5Gg2dsfMkbfFpydau9Zk3RVl6mVYATwzJaY9K5otC0K
7kc+nKPwkTxKAjndbznjsVrWK0Xcr4hrlMHs4Re2Nrdqa2mVd1jAAFO5xETJJtd7
YqL6mQuJ9wQfEEq2QWz1hEi67l8g8VeEgzYQOjZ6pTxwYYt0YDfKBjRtRCWuJ11c
w6Q+pniGcgIHAMkQGjZds88CwAdIiyG7IAIT2ovW+xVxH/JqLPHeRsHMKYx4DPqL
2y23Tchw+gBUvmbwCdObXWL1eq5R3Xz3ikkdX/I6zknmEvgPTi5N59C1IQqh0W/6
8uMrHOdELz9I5Fd+zGTJ8iyh/wrecMiIx+HOsBTYv/FYbMVnQUshUBOiD70geUb5
uSeHyxl/P7VK/0phbxOznU4oDot2fHPmZRK3q+K67J9L16q7pEou1AJAw8E7ed5C
Ywf+y2tdxsuqChQK/OA6uuqW6rXjZPuCoG5Bn6YIEuU769LsHcY=
=1PBR
-----END PGP SIGNATURE-----

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,9 @@
Misc/NEWS | 2 +-
2 files changed, 1 insertion(+), 4 deletions(-)
Index: Python-3.10.19/Doc/using/configure.rst
===================================================================
--- Python-3.10.19.orig/Doc/using/configure.rst 2025-10-09 17:25:03.000000000 +0200
+++ Python-3.10.19/Doc/using/configure.rst 2025-12-19 23:10:08.779794344 +0100
@@ -42,7 +42,6 @@
--- a/Doc/using/configure.rst
+++ b/Doc/using/configure.rst
@@ -42,7 +42,6 @@ General Options
See :data:`sys.int_info.bits_per_digit <sys.int_info>`.
@@ -15,7 +13,7 @@ Index: Python-3.10.19/Doc/using/configure.rst
.. cmdoption:: --with-cxx-main=COMPILER
Compile the Python ``main()`` function and link Python executable with C++
@@ -473,13 +472,11 @@
@@ -473,13 +472,11 @@ macOS Options
See ``Mac/README.rst``.
@@ -29,11 +27,9 @@ Index: Python-3.10.19/Doc/using/configure.rst
.. cmdoption:: --enable-framework=INSTALLDIR
Create a Python.framework rather than a traditional Unix install. Optional
Index: Python-3.10.19/Misc/NEWS
===================================================================
--- Python-3.10.19.orig/Misc/NEWS 2025-10-09 17:25:03.000000000 +0200
+++ Python-3.10.19/Misc/NEWS 2025-12-19 23:10:08.784479751 +0100
@@ -4018,7 +4018,7 @@
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -3731,7 +3731,7 @@ C API
-----
- bpo-43795: The list in :ref:`stable-abi-list` now shows the public name

View File

@@ -1,35 +0,0 @@
From 1b3f6523a5c83323cdc44031b33a1c062e5dc698 Mon Sep 17 00:00:00 2001
From: Xi Ruoyao <xry111@xry111.site>
Date: Fri, 7 Jun 2024 23:51:32 +0800
Subject: [PATCH] gh-120226: Fix
test_sendfile_close_peer_in_the_middle_of_receiving on Linux >= 6.10
(GH-120227)
The worst case is that the kernel buffers 17 pages with a page size of 64k.
(cherry picked from commit a7584245661102a5768c643fbd7db8395fd3c90e)
Co-authored-by: Xi Ruoyao <xry111@xry111.site>
---
Lib/test/test_asyncio/test_sendfile.py | 11 ++++-------
1 file changed, 4 insertions(+), 7 deletions(-)
--- a/Lib/test/test_asyncio/test_sendfile.py
+++ b/Lib/test/test_asyncio/test_sendfile.py
@@ -93,13 +93,10 @@ class MyProto(asyncio.Protocol):
class SendfileBase:
- # 256 KiB plus small unaligned to buffer chunk
- # Newer versions of Windows seems to have increased its internal
- # buffer and tries to send as much of the data as it can as it
- # has some form of buffering for this which is less than 256KiB
- # on newer server versions and Windows 11.
- # So DATA should be larger than 256 KiB to make this test reliable.
- DATA = b"x" * (1024 * 256 + 1)
+ # Linux >= 6.10 seems buffering up to 17 pages of data.
+ # So DATA should be large enough to make this test reliable even with a
+ # 64 KiB page configuration.
+ DATA = b"x" * (1024 * 17 * 64 + 1)
# Reduce socket buffer size to test on relative small data sets.
BUF_SIZE = 4 * 1024 # 4 KiB

View File

@@ -1,36 +0,0 @@
From 19b61747df3d62c822285c488753d6fbdf91e3ac Mon Sep 17 00:00:00 2001
From: Daniel Garcia Moreno <daniel.garcia@suse.com>
Date: Tue, 23 Sep 2025 10:20:16 +0200
Subject: [PATCH 1/2] gh-139257: Support docutils >= 0.22
---
Doc/tools/extensions/pyspecific.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
Index: Python-3.10.18/Doc/tools/extensions/pyspecific.py
===================================================================
--- Python-3.10.18.orig/Doc/tools/extensions/pyspecific.py 2025-09-30 18:24:01.155883150 +0200
+++ Python-3.10.18/Doc/tools/extensions/pyspecific.py 2025-09-30 18:24:01.192999890 +0200
@@ -55,11 +55,21 @@
SOURCE_URI = 'https://github.com/python/cpython/tree/3.10/%s'
# monkey-patch reST parser to disable alphabetic and roman enumerated lists
+def _disable_alphabetic_and_roman(text):
+ try:
+ # docutils >= 0.22
+ from docutils.parsers.rst.states import InvalidRomanNumeralError
+ raise InvalidRomanNumeralError(text)
+ except ImportError:
+ # docutils < 0.22
+ return None
+
+
from docutils.parsers.rst.states import Body
Body.enum.converters['loweralpha'] = \
Body.enum.converters['upperalpha'] = \
Body.enum.converters['lowerroman'] = \
- Body.enum.converters['upperroman'] = lambda x: None
+ Body.enum.converters['upperroman'] = _disable_alphabetic_and_roman
# Support for marking up and linking to bugs.python.org issues

View File

@@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>org.python.IDLE3</id>
<launchable type="desktop-id">idle3.desktop</launchable>
<!-- Copyright 2017 Zbigniew Jędrzejewski-Szmek -->
<application>
<id type="desktop">idle3.desktop</id>
<name>IDLE3</name>
<metadata_licence>CC0</metadata_licence>
<project_license>Python-2.0</project_license>
<summary>Python 3 Integrated Development and Learning Environment</summary>
<description>
<p>
IDLE is Pythons Integrated Development and Learning Environment.
The GUI is uniform between Windows, Unix, and macOS.
The GUI is uniform between Windows, Unix, and Mac OS X.
IDLE provides an easy way to start writing, running, and debugging
Python code.
</p>
@@ -19,33 +19,17 @@
It provides:
</p>
<ul>
<li>a Python shell window (interactive interpreter) with colorizing of code input, output, and error messages,</li>
<li>a multi-window text editor with multiple undo, Python colorizing, smart indent, call tips, auto completion, and other features,</li>
<li>search within any window, replace within editor windows, and search through multiple files (grep),</li>
<li>a debugger with persistent breakpoints, stepping, and viewing of global and local namespaces.</li>
<li>a Python shell window (interactive interpreter) with colorizing of code input, output, and error messages,</li>
<li>a multi-window text editor with multiple undo, Python colorizing, smart indent, call tips, auto completion, and other features,</li>
<li>search within any window, replace within editor windows, and search through multiple files (grep),</li>
<li>a debugger with persistent breakpoints, stepping, and viewing of global and local namespaces.</li>
</ul>
</description>
<developer id="org.python">
<name>Python Software Foundation</name>
</developer>
<url type="homepage">https://docs.python.org/3/library/idle.html</url>
<screenshots>
<screenshot type="default">
<image>https://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-main-window.png</image>
</screenshot>
<screenshot>
<image>https://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-class-browser.png</image>
</screenshot>
<screenshot>
<image>https://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-code-viewer.png</image>
</screenshot>
<screenshot type="default">http://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-main-window.png</screenshot>
<screenshot>http://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-class-browser.png</screenshot>
<screenshot>http://in.waw.pl/~zbyszek/fedora/idle3-appdata/idle3-code-viewer.png</screenshot>
</screenshots>
<project_license>Python-2.0</project_license>
<metadata_license>CC0-1.0</metadata_license>
<update_contact>zbyszek@in.waw.pl</update_contact>
</component>
</application>

View File

@@ -4,11 +4,9 @@ unchanged:
Doc/library/turtle.rst | 82 -------------------------------------------------
1 file changed, 82 deletions(-)
Index: Python-3.10.19/Doc/library/turtle.rst
===================================================================
--- Python-3.10.19.orig/Doc/library/turtle.rst 2025-10-09 17:25:03.000000000 +0200
+++ Python-3.10.19/Doc/library/turtle.rst 2025-12-19 23:10:03.998503888 +0100
@@ -250,7 +250,6 @@
--- a/Doc/library/turtle.rst
+++ b/Doc/library/turtle.rst
@@ -250,7 +250,6 @@ Turtle motion
turtle is headed.
.. doctest::
@@ -16,7 +14,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.position()
(0.00,0.00)
@@ -277,7 +276,6 @@
@@ -277,7 +276,6 @@ Turtle motion
>>> turtle.goto(0, 0)
.. doctest::
@@ -24,7 +22,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.position()
(0.00,0.00)
@@ -296,13 +294,11 @@
@@ -296,13 +294,11 @@ Turtle motion
orientation depends on the turtle mode, see :func:`mode`.
.. doctest::
@@ -38,7 +36,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.heading()
22.0
@@ -321,13 +317,11 @@
@@ -321,13 +317,11 @@ Turtle motion
orientation depends on the turtle mode, see :func:`mode`.
.. doctest::
@@ -52,7 +50,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.heading()
22.0
@@ -350,13 +344,11 @@
@@ -350,13 +344,11 @@ Turtle motion
not change the turtle's orientation.
.. doctest::
@@ -66,7 +64,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> tp = turtle.pos()
>>> tp
@@ -380,13 +372,11 @@
@@ -380,13 +372,11 @@ Turtle motion
unchanged.
.. doctest::
@@ -80,7 +78,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.position()
(0.00,240.00)
@@ -402,13 +392,11 @@
@@ -402,13 +392,11 @@ Turtle motion
Set the turtle's second coordinate to *y*, leave first coordinate unchanged.
.. doctest::
@@ -94,7 +92,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.position()
(0.00,40.00)
@@ -435,7 +423,6 @@
@@ -435,7 +423,6 @@ Turtle motion
=================== ====================
.. doctest::
@@ -102,7 +100,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.setheading(90)
>>> turtle.heading()
@@ -448,14 +435,12 @@
@@ -448,14 +435,12 @@ Turtle motion
its start-orientation (which depends on the mode, see :func:`mode`).
.. doctest::
@@ -117,7 +115,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.heading()
90.0
@@ -487,7 +472,6 @@
@@ -487,7 +472,6 @@ Turtle motion
calculated automatically. May be used to draw regular polygons.
.. doctest::
@@ -125,7 +123,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.position()
@@ -516,7 +500,6 @@
@@ -516,7 +500,6 @@ Turtle motion
.. doctest::
@@ -133,7 +131,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.dot()
@@ -534,7 +517,6 @@
@@ -534,7 +517,6 @@ Turtle motion
it by calling ``clearstamp(stamp_id)``.
.. doctest::
@@ -141,7 +139,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.color("blue")
>>> turtle.stamp()
@@ -550,7 +532,6 @@
@@ -550,7 +532,6 @@ Turtle motion
Delete stamp with given *stampid*.
.. doctest::
@@ -149,7 +147,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.position()
(150.00,-0.00)
@@ -595,7 +576,6 @@
@@ -595,7 +576,6 @@ Turtle motion
undo actions is determined by the size of the undobuffer.
.. doctest::
@@ -157,7 +155,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> for i in range(4):
... turtle.fd(50); turtle.lt(80)
@@ -628,7 +608,6 @@
@@ -628,7 +608,6 @@ Turtle motion
turtle turn instantly.
.. doctest::
@@ -165,7 +163,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.speed()
3
@@ -649,7 +628,6 @@
@@ -649,7 +628,6 @@ Tell Turtle's state
Return the turtle's current location (x,y) (as a :class:`Vec2D` vector).
.. doctest::
@@ -173,7 +171,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.pos()
(440.00,-0.00)
@@ -665,7 +643,6 @@
@@ -665,7 +643,6 @@ Tell Turtle's state
orientation which depends on the mode - "standard"/"world" or "logo".
.. doctest::
@@ -181,7 +179,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.goto(10, 10)
>>> turtle.towards(0,0)
@@ -677,7 +654,6 @@
@@ -677,7 +654,6 @@ Tell Turtle's state
Return the turtle's x coordinate.
.. doctest::
@@ -189,7 +187,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.left(50)
@@ -693,7 +669,6 @@
@@ -693,7 +669,6 @@ Tell Turtle's state
Return the turtle's y coordinate.
.. doctest::
@@ -197,7 +195,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.left(60)
@@ -710,7 +685,6 @@
@@ -710,7 +685,6 @@ Tell Turtle's state
:func:`mode`).
.. doctest::
@@ -205,7 +203,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.left(67)
@@ -727,7 +701,6 @@
@@ -727,7 +701,6 @@ Tell Turtle's state
other turtle, in turtle step units.
.. doctest::
@@ -213,7 +211,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.distance(30,40)
@@ -751,7 +724,6 @@
@@ -751,7 +724,6 @@ Settings for measurement
Default value is 360 degrees.
.. doctest::
@@ -221,7 +219,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.left(90)
@@ -774,7 +746,6 @@
@@ -774,7 +746,6 @@ Settings for measurement
``degrees(2*math.pi)``.
.. doctest::
@@ -229,7 +227,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.left(90)
@@ -785,7 +756,6 @@
@@ -785,7 +756,6 @@ Settings for measurement
1.5707963267948966
.. doctest::
@@ -237,7 +235,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
:hide:
>>> turtle.degrees(360)
@@ -821,7 +791,6 @@
@@ -821,7 +791,6 @@ Drawing state
thickness. If no argument is given, the current pensize is returned.
.. doctest::
@@ -245,7 +243,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.pensize()
1
@@ -853,7 +822,6 @@
@@ -853,7 +822,6 @@ Drawing state
attributes in one statement.
.. doctest::
@@ -253,7 +251,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
:options: +NORMALIZE_WHITESPACE
>>> turtle.pen(fillcolor="black", pencolor="red", pensize=10)
@@ -876,7 +844,6 @@
@@ -876,7 +844,6 @@ Drawing state
Return ``True`` if pen is down, ``False`` if it's up.
.. doctest::
@@ -261,7 +259,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.penup()
>>> turtle.isdown()
@@ -917,7 +884,6 @@
@@ -917,7 +884,6 @@ Color control
newly set pencolor.
.. doctest::
@@ -269,7 +267,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> colormode()
1.0
@@ -966,7 +932,6 @@
@@ -966,7 +932,6 @@ Color control
with the newly set fillcolor.
.. doctest::
@@ -277,7 +275,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.fillcolor("violet")
>>> turtle.fillcolor()
@@ -1005,7 +970,6 @@
@@ -1005,7 +970,6 @@ Color control
with the newly set colors.
.. doctest::
@@ -285,7 +283,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.color("red", "green")
>>> turtle.color()
@@ -1022,7 +986,6 @@
@@ -1022,7 +986,6 @@ Filling
~~~~~~~
.. doctest::
@@ -293,7 +291,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
:hide:
>>> turtle.home()
@@ -1032,7 +995,6 @@
@@ -1032,7 +995,6 @@ Filling
Return fillstate (``True`` if filling, ``False`` else).
.. doctest::
@@ -301,7 +299,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.begin_fill()
>>> if turtle.filling():
@@ -1057,7 +1019,6 @@
@@ -1057,7 +1019,6 @@ Filling
above may be either all yellow or have some white regions.
.. doctest::
@@ -309,7 +307,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.color("black", "red")
>>> turtle.begin_fill()
@@ -1074,7 +1035,6 @@
@@ -1074,7 +1035,6 @@ More drawing control
variables to the default values.
.. doctest::
@@ -317,7 +315,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.goto(0,-22)
>>> turtle.left(100)
@@ -1125,7 +1085,6 @@
@@ -1125,7 +1085,6 @@ Visibility
drawing observably.
.. doctest::
@@ -325,7 +323,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.hideturtle()
@@ -1136,7 +1095,6 @@
@@ -1136,7 +1095,6 @@ Visibility
Make the turtle visible.
.. doctest::
@@ -333,7 +331,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.showturtle()
@@ -1167,7 +1125,6 @@
@@ -1167,7 +1125,6 @@ Appearance
deal with shapes see Screen method :func:`register_shape`.
.. doctest::
@@ -341,7 +339,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.shape()
'classic'
@@ -1193,7 +1150,6 @@
@@ -1193,7 +1150,6 @@ Appearance
``resizemode("user")`` is called by :func:`shapesize` when used with arguments.
.. doctest::
@@ -349,15 +347,15 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.resizemode()
'noresize'
@@ -1217,7 +1173,6 @@
of the shape's outline.
@@ -1217,7 +1173,6 @@ Appearance
of the shapes's outline.
.. doctest::
- :skipif: _tkinter is None
>>> turtle.shapesize()
(1.0, 1.0, 1)
@@ -1242,7 +1197,6 @@
@@ -1242,7 +1197,6 @@ Appearance
heading of the turtle are sheared.
.. doctest::
@@ -365,7 +363,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.shape("circle")
>>> turtle.shapesize(5,2)
@@ -1259,7 +1213,6 @@
@@ -1259,7 +1213,6 @@ Appearance
change the turtle's heading (direction of movement).
.. doctest::
@@ -373,7 +371,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.reset()
>>> turtle.shape("circle")
@@ -1279,7 +1232,6 @@
@@ -1279,7 +1232,6 @@ Appearance
(direction of movement).
.. doctest::
@@ -381,7 +379,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.reset()
>>> turtle.shape("circle")
@@ -1305,7 +1257,6 @@
@@ -1305,7 +1257,6 @@ Appearance
turtle (its direction of movement).
.. doctest::
@@ -389,7 +387,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.reset()
>>> turtle.shape("circle")
@@ -1334,7 +1285,6 @@
@@ -1334,7 +1285,6 @@ Appearance
given matrix.
.. doctest::
@@ -397,7 +395,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle = Turtle()
>>> turtle.shape("square")
@@ -1350,7 +1300,6 @@
@@ -1350,7 +1300,6 @@ Appearance
can be used to define a new shape or components of a compound shape.
.. doctest::
@@ -405,7 +403,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.shape("square")
>>> turtle.shapetransform(4, -1, 0, 2)
@@ -1375,7 +1324,6 @@
@@ -1375,7 +1324,6 @@ Using events
procedural way:
.. doctest::
@@ -413,7 +411,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> def turn(x, y):
... left(180)
@@ -1396,7 +1344,6 @@
@@ -1396,7 +1344,6 @@ Using events
``None``, existing bindings are removed.
.. doctest::
@@ -421,7 +419,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> class MyTurtle(Turtle):
... def glow(self,x,y):
@@ -1424,7 +1371,6 @@
@@ -1424,7 +1371,6 @@ Using events
mouse-click event on that turtle.
.. doctest::
@@ -429,7 +427,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.ondrag(turtle.goto)
@@ -1452,7 +1398,6 @@
@@ -1452,7 +1398,6 @@ Special Turtle methods
Return the last recorded polygon.
.. doctest::
@@ -437,7 +435,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.home()
>>> turtle.begin_poly()
@@ -1472,7 +1417,6 @@
@@ -1472,7 +1417,6 @@ Special Turtle methods
turtle properties.
.. doctest::
@@ -445,7 +443,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> mick = Turtle()
>>> joe = mick.clone()
@@ -1485,7 +1429,6 @@
@@ -1485,7 +1429,6 @@ Special Turtle methods
return the "anonymous turtle":
.. doctest::
@@ -453,7 +451,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> pet = getturtle()
>>> pet.fd(50)
@@ -1499,7 +1442,6 @@
@@ -1499,7 +1442,6 @@ Special Turtle methods
TurtleScreen methods can then be called for that object.
.. doctest::
@@ -461,7 +459,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> ts = turtle.getscreen()
>>> ts
@@ -1517,7 +1459,6 @@
@@ -1517,7 +1459,6 @@ Special Turtle methods
``None``, the undobuffer is disabled.
.. doctest::
@@ -469,7 +467,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> turtle.setundobuffer(42)
@@ -1527,7 +1468,6 @@
@@ -1527,7 +1468,6 @@ Special Turtle methods
Return number of entries in the undobuffer.
.. doctest::
@@ -477,7 +475,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> while undobufferentries():
... undo()
@@ -1550,7 +1490,6 @@
@@ -1550,7 +1490,6 @@ below:
For example:
.. doctest::
@@ -485,7 +483,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> s = Shape("compound")
>>> poly1 = ((0,0),(10,-5),(0,10),(-10,-5))
@@ -1561,7 +1500,6 @@
@@ -1561,7 +1500,6 @@ below:
3. Now add the Shape to the Screen's shapelist and use it:
.. doctest::
@@ -493,7 +491,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> register_shape("myshape", s)
>>> shape("myshape")
@@ -1581,7 +1519,6 @@
@@ -1581,7 +1519,6 @@ Most of the examples in this section ref
``screen``.
.. doctest::
@@ -501,7 +499,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
:hide:
>>> screen = Screen()
@@ -1598,7 +1535,6 @@
@@ -1598,7 +1535,6 @@ Window control
Set or return background color of the TurtleScreen.
.. doctest::
@@ -509,7 +507,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.bgcolor("orange")
>>> screen.bgcolor()
@@ -1690,7 +1626,6 @@
@@ -1690,7 +1626,6 @@ Window control
distorted.
.. doctest::
@@ -517,7 +515,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.reset()
>>> screen.setworldcoordinates(-50,-7.5,50,7.5)
@@ -1701,7 +1636,6 @@
@@ -1701,7 +1636,6 @@ Window control
... left(45); fd(2) # a regular octagon
.. doctest::
@@ -525,7 +523,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
:hide:
>>> screen.reset()
@@ -1723,7 +1657,6 @@
@@ -1723,7 +1657,6 @@ Animation control
Optional argument:
.. doctest::
@@ -533,7 +531,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.delay()
10
@@ -1745,7 +1678,6 @@
@@ -1745,7 +1678,6 @@ Animation control
:func:`delay`).
.. doctest::
@@ -541,7 +539,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.tracer(8, 25)
>>> dist = 2
@@ -1782,7 +1714,6 @@
@@ -1782,7 +1714,6 @@ Using screen events
must have the focus. (See method :func:`listen`.)
.. doctest::
@@ -549,7 +547,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> def f():
... fd(50)
@@ -1803,7 +1734,6 @@
@@ -1803,7 +1734,6 @@ Using screen events
must have focus. (See method :func:`listen`.)
.. doctest::
@@ -557,7 +555,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> def f():
... fd(50)
@@ -1828,7 +1758,6 @@
@@ -1828,7 +1758,6 @@ Using screen events
named ``turtle``:
.. doctest::
@@ -565,7 +563,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.onclick(turtle.goto) # Subsequently clicking into the TurtleScreen will
>>> # make the turtle move to the clicked point.
@@ -1848,7 +1777,6 @@
@@ -1848,7 +1777,6 @@ Using screen events
Install a timer that calls *fun* after *t* milliseconds.
.. doctest::
@@ -573,7 +571,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> running = True
>>> def f():
@@ -1930,7 +1858,6 @@
@@ -1930,7 +1858,6 @@ Settings and special methods
============ ========================= ===================
.. doctest::
@@ -581,7 +579,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> mode("logo") # resets turtle heading to north
>>> mode()
@@ -1945,7 +1872,6 @@
@@ -1945,7 +1872,6 @@ Settings and special methods
values of color triples have to be in the range 0..\ *cmode*.
.. doctest::
@@ -589,7 +587,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.colormode(1)
>>> turtle.pencolor(240, 160, 80)
@@ -1966,7 +1892,6 @@
@@ -1966,7 +1892,6 @@ Settings and special methods
do with a Tkinter Canvas.
.. doctest::
@@ -597,7 +595,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> cv = screen.getcanvas()
>>> cv
@@ -1978,7 +1903,6 @@
@@ -1978,7 +1903,6 @@ Settings and special methods
Return a list of names of all currently available turtle shapes.
.. doctest::
@@ -605,7 +603,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.getshapes()
['arrow', 'blank', 'circle', ..., 'turtle']
@@ -2002,7 +1926,6 @@
@@ -2002,7 +1926,6 @@ Settings and special methods
coordinates: Install the corresponding polygon shape.
.. doctest::
@@ -613,7 +611,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.register_shape("triangle", ((5,-3), (0,5), (-5,-3)))
@@ -2018,7 +1941,6 @@
@@ -2018,7 +1941,6 @@ Settings and special methods
Return the list of turtles on the screen.
.. doctest::
@@ -621,7 +619,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> for turtle in screen.turtles():
... turtle.color("red")
@@ -2080,7 +2002,6 @@
@@ -2080,7 +2002,6 @@ Methods specific to Screen, not inherite
center window vertically
.. doctest::
@@ -629,7 +627,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.setup (width=200, height=200, startx=0, starty=0)
>>> # sets window to 200x200 pixels, in upper left of screen
@@ -2096,7 +2017,6 @@
@@ -2096,7 +2017,6 @@ Methods specific to Screen, not inherite
Set title of turtle window to *titlestring*.
.. doctest::
@@ -637,7 +635,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> screen.title("Welcome to the turtle zoo!")
@@ -2167,7 +2087,6 @@
@@ -2167,7 +2087,6 @@ Public classes
Example:
.. doctest::
@@ -645,7 +643,7 @@ Index: Python-3.10.19/Doc/library/turtle.rst
>>> poly = ((0,0),(10,-5),(0,10),(-10,-5))
>>> s = Shape("compound")
@@ -2518,7 +2437,6 @@
@@ -2514,7 +2433,6 @@ Changes since Python 3.0
.. doctest::

View File

@@ -0,0 +1,15 @@
---
Lib/test/test_posix.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
--- a/Lib/test/test_posix.py
+++ b/Lib/test/test_posix.py
@@ -425,7 +425,7 @@ class PosixTester(unittest.TestCase):
def test_posix_fadvise(self):
fd = os.open(os_helper.TESTFN, os.O_RDONLY)
try:
- posix.posix_fadvise(fd, 0, 0, posix.POSIX_FADV_WILLNEED)
+ posix.posix_fadvise(fd, 0, 0, posix.POSIX_FADV_RANDOM)
finally:
os.close(fd)

View File

@@ -1,413 +1,3 @@
-------------------------------------------------------------------
Wed Feb 11 23:49:49 CET 2026 - Matej Cepl <mcepl@suse.com>
- CVE-2025-11468: preserving parens when folding comments in
email headers (bsc#1257029, gh#python/cpython#143935).
CVE-2025-11468-email-hdr-fold-comment.patch
- CVE-2026-0672: rejects control characters in http cookies.
(bsc#1257031, gh#python/cpython#143919)
CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
- CVE-2026-0865: rejecting control characters in
wsgiref.headers.Headers, which could be abused for injecting
false HTTP headers. (bsc#1257042, gh#python/cpython#143916)
CVE-2026-0865-wsgiref-ctrl-chars.patch
- CVE-2025-15366: basically the same as the previous patch for
IMAP protocol. (bsc#1257044, gh#python/cpython#143921)
CVE-2025-15366-imap-ctrl-chars.patch
- CVE-2025-15282: basically the same as the previous patch for
urllib library. (bsc#1257046, gh#python/cpython#143925)
CVE-2025-15282-urllib-ctrl-chars.patch
- CVE-2025-15367: basically the same as the previous patch for
poplib library. (bsc#1257041, gh#python/cpython#143923)
CVE-2025-15367-poplib-ctrl-chars.patch
-------------------------------------------------------------------
Thu Dec 18 10:33:44 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2025-13836-http-resp-cont-len.patch (bsc#1254400,
CVE-2025-13836) to prevent reading an HTTP response from
a server, if no read amount is specified, with using
Content-Length per default as the length.
- Add CVE-2025-12084-minidom-quad-search.patch prevent quadratic
behavior in node ID cache clearing (CVE-2025-12084,
bsc#1254997).
- Add CVE-2025-13837-plistlib-mailicious-length.patch protect
against OOM when loading malicious content (CVE-2025-13837,
bsc#1254401).
-------------------------------------------------------------------
Thu Dec 18 10:33:44 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2025-13836-http-resp-cont-len.patch (bsc#1254400,
CVE-2025-13836) to prevent reading an HTTP response from
a server, if no read amount is specified, with using
Content-Length per default as the length.
-------------------------------------------------------------------
Thu Nov 13 17:13:03 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2025-6075-expandvars-perf-degrad.patch avoid simple
quadratic complexity vulnerabilities of os.path.expandvars()
(CVE-2025-6075, bsc#1252974).
-------------------------------------------------------------------
Wed Oct 15 08:43:33 UTC 2025 - Daniel Garcia <daniel.garcia@suse.com>
- Update to 3.10.19:
- Security
- gh-139700: Check consistency of the zip64 end of central
directory record. Support records with “zip64 extensible data”
if there are no bytes prepended to the ZIP file
(CVE-2025-8291, bsc#1251305).
- gh-139400: xml.parsers.expat: Make sure that parent Expat
parsers are only garbage-collected once they are no longer
referenced by subparsers created by
ExternalEntityParserCreate(). Patch by Sebastian Pipping.
- gh-135661: Fix parsing start and end tags in
html.parser.HTMLParser according to the HTML5 standard.
* Whitespaces no longer accepted between </ and the tag name.
E.g. </ script> does not end the script section.
* Vertical tabulation (\v) and non-ASCII whitespaces no longer
recognized as whitespaces. The only whitespaces are \t\n\r\f
and space.
* Null character (U+0000) no longer ends the tag name.
* Attributes and slashes after the tag name in end tags are now
ignored, instead of terminating after the first > in quoted
attribute value. E.g. </script/foo=">"/>.
* Multiple slashes and whitespaces between the last attribute
and closing > are now ignored in both start and end tags. E.g.
<a foo=bar/ //>.
* Multiple = between attribute name and value are no longer
collapsed. E.g. <a foo==bar> produces attribute “foo” with
value “=bar”.
- gh-135661: Fix CDATA section parsing in html.parser.HTMLParser
according to the HTML5 standard: ] ]> and ]] > no longer end the
CDATA section. Add private method _set_support_cdata() which can
be used to specify how to parse <[CDATA[ — as a CDATA section in
foreign content (SVG or MathML) or as a bogus comment in the
HTML namespace.
- gh-102555: Fix comment parsing in html.parser.HTMLParser
according to the HTML5 standard. --!> now ends the comment. -- >
no longer ends the comment. Support abnormally ended empty
comments <--> and <--->.
- gh-135462: Fix quadratic complexity in processing specially
crafted input in html.parser.HTMLParser. End-of-file errors are
now handled according to the HTML5 specs comments and
declarations are automatically closed, tags are ignored.
- gh-118350: Fix support of escapable raw text mode (elements
“textarea” and “title”) in html.parser.HTMLParser.
- gh-86155: html.parser.HTMLParser.close() no longer loses data
when the <script> tag is not closed. Patch by Waylan Limberg.
- Library
- gh-139312: Upgrade bundled libexpat to 2.7.3
- gh-138998: Update bundled libexpat to 2.7.2
- gh-130577: tarfile now validates archives to ensure member
offsets are non-negative. (Contributed by Alexander Enrique
Urieles Nieto in gh-130577.)
- gh-135374: Update the bundled copy of setuptools to 79.0.1.
- Drop upstreamed patches:
- CVE-2025-8194-tarfile-no-neg-offsets.patch
- CVE-2025-6069-quad-complex-HTMLParser.patch
-------------------------------------------------------------------
Mon Sep 29 06:52:07 UTC 2025 - Daniel Garcia <daniel.garcia@suse.com>
- Add gh139257-Support-docutils-0.22.patch to fix build with latest
docutils (>=0.22) gh#python/cpython#139257
-------------------------------------------------------------------
Thu Sep 18 08:15:31 UTC 2025 - Dominique Leuenberger <dimstar@opensuse.org>
- Require AppStream to validate appdata file instead of deprecated
appstream-glib.
- Update idle3.appdata.xml to pass the more pedantic appstreamcli.
-------------------------------------------------------------------
Fri Aug 1 20:09:24 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2025-8194-tarfile-no-neg-offsets.patch which now
validates archives to ensure member offsets are non-negative
(gh#python/cpython#130577, CVE-2025-8194, bsc#1247249).
-------------------------------------------------------------------
Wed Jul 2 14:47:20 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2025-6069-quad-complex-HTMLParser.patch to avoid worst
case quadratic complexity when processing certain crafted
malformed inputs with HTMLParser (CVE-2025-6069, bsc#1244705).
-------------------------------------------------------------------
Mon Jun 9 16:53:24 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Update to 3.10.18:
- Security
- gh-135034: Fixes multiple issues that allowed tarfile
extraction filters (filter="data" and filter="tar")
to be bypassed using crafted symlinks and hard links.
Addresses CVE-2024-12718 (bsc#1244056), CVE-2025-4138
(bsc#1244059), CVE-2025-4330 (bsc#1244060), and
CVE-2025-4517 (bsc#1244032). Also addresses CVE-2025-4435
(gh#135034, bsc#1244061).
- gh-133767: Fix use-after-free in the “unicode-escape”
decoder with a non-“strict” error handler (CVE-2025-4516,
bsc#1243273).
- gh-128840: Short-circuit the processing of long IPv6
addresses early in ipaddress to prevent excessive memory
consumption and a minor denial-of-service.
- Library
- gh-128840: Fix parsing long IPv6 addresses with embedded
IPv4 address.
- gh-134062: ipaddress: fix collisions in __hash__() for
IPv4Network and IPv6Network objects.
- gh-123409: Fix ipaddress.IPv6Address.reverse_pointer output
according to RFC 3596, §2.5. Patch by Bénédikt Tran.
- bpo-43633: Improve the textual representation of
IPv4-mapped IPv6 addresses (RFC 4291 Sections 2.2, 2.5.5.2)
in ipaddress. Patch by Oleksandr Pavliuk.
- Remove upstreamed patches:
- gh-126572-test_ssl-no-stop-ThreadedEchoServer-OSError.patch
- CVE-2025-4516-DecodeError-handler.patch
-------------------------------------------------------------------
Thu May 22 13:01:17 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2025-4516-DecodeError-handler.patch fixing
CVE-2025-4516 (bsc#1243273) blocking DecodeError handling
vulnerability, which could lead to DoS.
-------------------------------------------------------------------
Sat May 17 10:02:27 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Use extended %autopatch.
-------------------------------------------------------------------
Sat May 10 11:38:22 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Remove python-3.3.0b1-test-posix_fadvise.patch (not needed
since kernel 3.6-rc1)
-------------------------------------------------------------------
Fri Apr 11 08:12:14 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Update to 3.10.17:
- gh-131809: Update bundled libexpat to 2.7.1
- gh-131261: Upgrade to libexpat 2.7.0
- gh-105704: When using urllib.parse.urlsplit() and
urllib.parse.urlparse() host parsing would not reject domain
names containing square brackets ([ and ]). Square brackets
are only valid for IPv6 and IPvFuture hosts according to RFC
3986 Section 3.2.2 (bsc#1236705, CVE-2025-0938,
gh#python/cpython#105704).
- gh-121284: Fix bug in the folding of rfc2047 encoded-words
when flattening an email message using a modern email
policy. Previously when an encoded-word was too long for
a line, it would be decoded, split across lines, and
re-encoded. But commas and other special characters in the
original text could be left unencoded and unquoted. This
could theoretically be used to spoof header lines using a
carefully constructed encoded-word if the resulting rendered
email was transmitted or re-parsed.
- gh-80222: Fix bug in the folding of quoted strings
when flattening an email message using a modern email
policy. Previously when a quoted string was folded so that
it spanned more than one line, the surrounding quotes and
internal escapes would be omitted. This could theoretically
be used to spoof header lines using a carefully constructed
quoted string if the resulting rendered email was transmitted
or re-parsed.
- gh-119511: Fix a potential denial of service in the imaplib
module. When connecting to a malicious server, it could
cause an arbitrary amount of memory to be allocated. On many
systems this is harmless as unused virtual memory is only
a mapping, but if this hit a virtual address size limit
it could lead to a MemoryError or other process crash. On
unusual systems or builds where all allocated memory is
touched and backed by actual ram or storage it couldve
consumed resources doing so until similarly crashing.
- gh-127257: In ssl, system call failures that OpenSSL reports
using ERR_LIB_SYS are now raised as OSError.
- gh-121277: Writers of CPythons documentation can now use
next as the version for the versionchanged, versionadded,
deprecated directives.
- Add gh-126572-test_ssl-no-stop-ThreadedEchoServer-OSError.patch
which makes test_ssl not to stop ThreadedEchoServer on OSError,
which makes test_ssl pass with OpenSSL 3.5 (bsc#1241067,
gh#python/cpython!126572)
- Remote upstreamed patch:
- CVE-2025-0938-sq-brackets-domain-names.patch
-------------------------------------------------------------------
Mon Mar 10 15:44:31 UTC 2025 - Bernhard Wiedemann <bwiedemann@suse.com>
- Skip PGO with %want_reproducible_builds (bsc#1239210)
-------------------------------------------------------------------
Tue Feb 4 14:43:13 UTC 2025 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2025-0938-sq-brackets-domain-names.patch which
disallows square brackets ([ and ]) in domain names for parsed
URLs (bsc#1236705, CVE-2025-0938, gh#python/cpython#105704)
-------------------------------------------------------------------
Wed Dec 4 21:23:20 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Update to 3.10.16:
- Tests
- gh-125041: Re-enable skipped tests for zlib on the
s390x architecture: only skip checks of the compressed
bytes, which can be different between zlibs software
implementation and the hardware-accelerated implementation.
- gh-109396: Fix test_socket.test_hmac_sha1() in FIPS
mode. Use a longer key: FIPS mode requires at least of at
least 112 bits. The previous key was only 32 bits. Patch by
Victor Stinner.
- Security
- gh-126623: Upgrade libexpat to 2.6.4
- gh-122792: Changed IPv4-mapped ipaddress.IPv6Address to
consistently use the mapped IPv4 address value for deciding
properties. Properties which have their behavior fixed are
is_multicast, is_reserved, is_link_local, is_global, and
is_unspecified (bsc#1233307, CVE-2024-11168).
- Library
- gh-124651: Properly quote template strings in venv
activation scripts (bsc#1232241, CVE-2024-9287).
- gh-103848: Add checks to ensure that [ bracketed ] hosts
found by urllib.parse.urlsplit() are of IPv6 or IPvFuture
format.
- Removed upstreamed patches:
- CVE-2024-9287-venv_path_unquoted.patch
- CVE-2024-11168-validation-IPv6-addrs.patch
-------------------------------------------------------------------
Thu Nov 14 07:06:20 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Remove -IVendor/ from python-config boo#1231795
- Apply sphinx-72.patch only conditionally for non-SLE-15 builds.
-------------------------------------------------------------------
Wed Nov 13 13:25:01 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2024-11168-validation-IPv6-addrs.patch
fixing bsc#1233307 (CVE-2024-11168,
gh#python/cpython#103848): Improper validation of IPv6 and
IPvFuture addresses.
-------------------------------------------------------------------
Mon Nov 4 21:49:20 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Update sphinx-72.patch to include renaming :noindex: option to
:no-index: in Sphinx 7.2 (bsc#1232750).
- While renaming drop fix-sphinx-72.patch.
-------------------------------------------------------------------
Fri Nov 1 21:38:45 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Update CVE-2024-9287-venv_path_unquoted.patch according to the
upstream PR gh#python/cpython!126301.
-------------------------------------------------------------------
Thu Oct 24 16:09:00 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Add CVE-2024-9287-venv_path_unquoted.patch to properly quote
path names provided when creating a virtual environment
(bsc#1232241, CVE-2024-9287)
-------------------------------------------------------------------
Wed Oct 2 16:18:29 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Drop .pyc files from docdir for reproducible builds
(bsc#1230906).
-------------------------------------------------------------------
Mon Sep 9 13:41:07 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Update to 3.10.15:
- Tests
- gh-112769: The tests now correctly compare zlib version
when :const:`zlib.ZLIB_RUNTIME_VERSION` contains
non-integer suffixes. For example zlib-ng defines the
version as ``1.3.0.zlib-ng``.
- gh-117187: Fix XML tests for vanilla Expat <2.6.0.
- gh-100454: Fix SSL tests CI for OpenSSL 3.1+
- Security
- gh-123678: Upgrade libexpat to 2.6.3
- gh-121957: Fixed missing audit events around interactive
use of Python, now also properly firing for ``python -i``,
as well as for ``python -m asyncio``. The event in question
is ``cpython.run_stdin``.
- gh-122133: Authenticate the socket connection for the
``socket.socketpair()`` fallback on platforms where
``AF_UNIX`` is not available like Windows. Patch by
Gregory P. Smith <greg@krypto.org> and Seth Larson
<seth@python.org>. Reported by Ellie <el@horse64.org>
- gh-121285: Remove backtracking from tarfile header
parsing for ``hdrcharset``, PAX, and GNU sparse headers
(bsc#1230227, CVE-2024-6232).
- gh-118486: :func:`os.mkdir` on Windows now accepts
*mode* of ``0o700`` to restrict the new directory to
the current user. This fixes CVE-2024-4030 affecting
:func:`tempfile.mkdtemp` in scenarios where the base
temporary directory is more permissive than the default.
- gh-116741: Update bundled libexpat to 2.6.2
- Library
- gh-123693: Use platform-agnostic behavior when computing
``zipfile.Path.name``.
- gh-123270: Applied a more surgical fix for malformed
payloads in :class:`zipfile.Path` causing infinite loops
(gh-122905) without breaking contents using legitimate
characters (bsc#1229704, CVE-2024-8088).
- gh-123067: Fix quadratic complexity in parsing ``"``-quoted
cookie values with backslashes by :mod:`http.cookies`
(bsc#1229596, CVE-2024-7592).
- gh-122905: :class:`zipfile.Path` objects now sanitize names
from the zipfile.
- gh-121650: :mod:`email` headers with embedded newlines are
now quoted on output. The :mod:`~email.generator` will now
refuse to serialize (write) headers that are unsafely folded
or delimited; see :attr:`~email.policy.Policy.verify_generated_headers`.
(Contributed by Bas Bloemsaat and Petr Viktorin in
gh-121650.; CVE-2024-6923, bsc#1228780).
- gh-113171: Fixed various false positives and false negatives in
* :attr:`ipaddress.IPv4Address.is_private` (see these docs for details)
* :attr:`ipaddress.IPv4Address.is_global`
* :attr:`ipaddress.IPv6Address.is_private`
* :attr:`ipaddress.IPv6Address.is_global`
Also in the corresponding :class:`ipaddress.IPv4Network` and
:class:`ipaddress.IPv6Network` attributes.
Fixes bsc#1226448 (CVE-2024-4032).
- gh-102988: :func:`email.utils.getaddresses` and
:func:`email.utils.parseaddr` now return ``('', '')`` 2-tuples in more
situations where invalid email addresses are encountered instead of
potentially inaccurate values. Add optional *strict* parameter to these
two functions: use ``strict=False`` to get the old behavior, accept
malformed inputs. ``getattr(email.utils, 'supports_strict_parsing',
False)`` can be use to check if the *strict* paramater is available. Patch
by Thomas Dwyer and Victor Stinner to improve the
CVE-2023-27043 fix (bsc#1210638).
- gh-67693: Fix :func:`urllib.parse.urlunparse` and
:func:`urllib.parse.urlunsplit` for URIs with path starting with multiple
slashes and no authority. Based on patch by Ashwin Ramaswami.
- Core and Builtins
- gh-112275: A deadlock involving ``pystate.c``'s
``HEAD_LOCK`` in ``posixmodule.c`` at fork is now
fixed. Patch by ChuBoning based on previous Python 3.12 fix
by Victor Stinner.
- Remove upstreamed patches:
- CVE-2023-27043-email-parsing-errors.patch
- CVE-2024-4032-private-IP-addrs.patch
- CVE-2024-6923-email-hdr-inject.patch
- CVE-2024-8088-inf-loop-zipfile_Path.patch
- Add sphinx-802.patch to overcome working both with the most
recent and older Sphinx versions.
-------------------------------------------------------------------
Mon Sep 2 09:44:26 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Add gh120226-fix-sendfile-test-kernel-610.patch to avoid
failing test_sendfile_close_peer_in_the_middle_of_receiving
tests on Linux >= 6.10 (GH-120227).
-------------------------------------------------------------------
Wed Aug 28 16:54:34 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
@@ -532,7 +122,7 @@ Fri Feb 23 01:06:42 UTC 2024 - Matej Cepl <mcepl@suse.com>
Tue Feb 20 22:14:02 UTC 2024 - Matej Cepl <mcepl@cepl.eu>
- Remove double definition of /usr/bin/idle%%{version} in
%%files.
%%files.
-------------------------------------------------------------------
Thu Feb 15 10:29:07 UTC 2024 - Daniel Garcia <daniel.garcia@suse.com>

View File

@@ -1,7 +1,7 @@
#
# spec file for package python310
#
# Copyright (c) 2025 SUSE LLC and contributors
# Copyright (c) 2024 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -36,7 +36,7 @@
%bcond_without general
%endif
%if 0%{?do_profiling} && !0%{?want_reproducible_builds}
%if 0%{?do_profiling}
%bcond_without profileopt
%else
%bcond_with profileopt
@@ -108,13 +108,13 @@ Obsoletes: python39%{?1:-%{1}}
# _md5.cpython-38m-x86_64-linux-gnu.so
%define dynlib() %{sitedir}/lib-dynload/%{1}.cpython-%{abi_tag}-%{archname}-%{_os}%{?_gnu}%{?armsuffix}.so
Name: %{python_pkg_name}%{psuffix}
Version: 3.10.19
Version: 3.10.14
Release: 0
Summary: Python 3 Interpreter
License: Python-2.0
URL: https://www.python.org/
Source0: https://www.python.org/ftp/python/%{folderversion}/%{tarname}.tar.xz
Source1: https://www.python.org/ftp/python/%{folderversion}/%{tarname}.tar.xz.sigstore
Source1: https://www.python.org/ftp/python/%{folderversion}/%{tarname}.tar.xz.asc
Source2: baselibs.conf
Source3: README.SUSE
Source7: macros.python3
@@ -152,6 +152,8 @@ Patch02: distutils-reproducible-compile.patch
Patch03: python-3.3.0b1-localpath.patch
# replace DATE, TIME and COMPILER by fixed definitions to aid reproducible builds
Patch04: python-3.3.0b1-fix_date_time_compiler.patch
# POSIX_FADV_WILLNEED throws EINVAL. Use a different constant in test
Patch05: python-3.3.0b1-test-posix_fadvise.patch
# Raise timeout value for test_subprocess
Patch06: subprocess-raise-timeout.patch
# PATCH-FEATURE-UPSTREAM bpo-31046_ensurepip_honours_prefix.patch bpo#31046 mcepl@suse.com
@@ -160,6 +162,7 @@ Patch07: bpo-31046_ensurepip_honours_prefix.patch
# PATCH-FIX-SLE no-skipif-doctests.patch jsc#SLE-13738 mcepl@suse.com
# SLE-15 version of Sphinx doesn't know about skipif directive in doctests.
Patch11: no-skipif-doctests.patch
# PATCH-FIX-SLE skip-test_pyobject_freed_is_freed.patch mcepl@suse.com
# skip a test failing on SLE-15
Patch15: skip-test_pyobject_freed_is_freed.patch
@@ -175,6 +178,11 @@ Patch18: bpo-37596-make-set-marshalling.patch
# PATCH-FIX-UPSTREAM gh-78214-marshal_stabilize_FLAG_REF.patch bsc#1213463 mcepl@suse.com
# marshal: Stabilize FLAG_REF usage
Patch19: gh-78214-marshal_stabilize_FLAG_REF.patch
# PATCH-FIX-UPSTREAM CVE-2023-27043-email-parsing-errors.patch bsc#1210638 mcepl@suse.com
# Detect email address parsing errors and return empty tuple to
# indicate the parsing error (old API), from gh#python/cpython!105127
# Patch carries a REGRESSION (gh#python/cpython#106669), so it has been also partially REVERTED
Patch20: CVE-2023-27043-email-parsing-errors.patch
# PATCH-FIX-UPSTREAM fix-sphinx-72.patch gh#python/cpython#97950
# This is a patch with a lot of PR combined to make the doc work with
# sphinx 7.2
@@ -188,52 +196,22 @@ Patch19: gh-78214-marshal_stabilize_FLAG_REF.patch
# * gh#python/cpython#104163
# * gh#python/cpython#104221
# * gh#python/cpython#107246
Patch21: sphinx-72.patch
Patch21: fix-sphinx-72.patch
# PATCH-FIX-UPSTREAM CVE-2023-52425-libexpat-2.6.0-backport.patch gh#python/cpython#117187 mcepl@suse.com
# Make the test suite work with libexpat < 2.6.0
Patch22: CVE-2023-52425-libexpat-2.6.0-backport.patch
# PATCH-FIX-UPSTREAM CVE-2024-4032-private-IP-addrs.patch bsc#1226448 mcepl@suse.com
# rearrange definition of private v global IP addresses
Patch23: CVE-2024-4032-private-IP-addrs.patch
# PATCH-FIX-UPSTREAM bso1227999-reproducible-builds.patch bsc#1227999 mcepl@suse.com
# reproducibility patches
Patch24: bso1227999-reproducible-builds.patch
# PATCH-FIX-UPSTREAM gh120226-fix-sendfile-test-kernel-610.patch gh#python/cpython#120226 mcepl@suse.com
# Fix test_sendfile_close_peer_in_the_middle_of_receiving on Linux >= 6.10 (GH-120227)
Patch27: gh120226-fix-sendfile-test-kernel-610.patch
# PATCH-FIX-UPSTREAM sphinx-802.patch mcepl@suse.com
# status_iterator method moved between the Sphinx versions
Patch28: sphinx-802.patch
# PATCH-FIX-OPENSUSE gh139257-Support-docutils-0.22.patch gh#python/cpython#139257 daniel.garcia@suse.com
Patch29: gh139257-Support-docutils-0.22.patch
# PATCH-FIX-UPSTREAM CVE-2025-6075-expandvars-perf-degrad.patch bsc#1252974 mcepl@suse.com
# Avoid potential quadratic complexity vulnerabilities in path modules
Patch30: CVE-2025-6075-expandvars-perf-degrad.patch
# PATCH-FIX-UPSTREAM CVE-2025-13836-http-resp-cont-len.patch bsc#1254400 mcepl@suse.com
# Avoid loading possibly compromised length of HTTP response
Patch31: CVE-2025-13836-http-resp-cont-len.patch
# PATCH-FIX-UPSTREAM CVE-2025-12084-minidom-quad-search.patch bsc#1254997 mcepl@suse.com
# prevent quadratic behavior in node ID cache clearing
Patch32: CVE-2025-12084-minidom-quad-search.patch
# PATCH-FIX-UPSTREAM CVE-2025-13837-plistlib-mailicious-length.patch bsc#1254401 mcepl@suse.com
# protect against OOM when loading malicious content
Patch33: CVE-2025-13837-plistlib-mailicious-length.patch
# PATCH-FIX-UPSTREAM CVE-2025-11468-email-hdr-fold-comment.patch bsc#1257029 mcepl@suse.com
# this patch makes things totally awesome
Patch34: CVE-2025-11468-email-hdr-fold-comment.patch
# PATCH-FIX-UPSTREAM CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch bsc#1257031 mcepl@suse.com
# rejects control characters in http cookies.
Patch35: CVE-2026-0672-http-hdr-inject-cookie-Morsel.patch
# PATCH-FIX-UPSTREAM CVE-2026-0865-wsgiref-ctrl-chars.patch bsc#1257042 mcepl@suse.com
# Reject control characters in wsgiref.headers.Headers
Patch37: CVE-2026-0865-wsgiref-ctrl-chars.patch
# PATCH-FIX-UPSTREAM CVE-2025-15366-imap-ctrl-chars.patch bsc#1257044 mcepl@suse.com
# Reject control characters in wsgiref.headers.Headers
Patch38: CVE-2025-15366-imap-ctrl-chars.patch
# PATCH-FIX-UPSTREAM CVE-2025-15282-urllib-ctrl-chars.patch bsc#1257046 mcepl@suse.com
# Reject control characters in urllib
Patch39: CVE-2025-15282-urllib-ctrl-chars.patch
# PATCH-FIX-UPSTREAM CVE-2025-15367-poplib-ctrl-chars.patch bsc#1257041 mcepl@suse.com
# Reject control characters in poplib
Patch40: CVE-2025-15367-poplib-ctrl-chars.patch
### END OF PATCHES
# PATCH-FIX-UPSTREAM CVE-2024-6923-email-hdr-inject.patch bsc#1228780 mcepl@suse.com
# prevent email header injection, patch from gh#python/cpython!122608
Patch25: CVE-2024-6923-email-hdr-inject.patch
# PATCH-FIX-UPSTREAM CVE-2024-8088-inf-loop-zipfile_Path.patch bsc#1229704 mcepl@suse.com
# avoid denial of service in zipfile
Patch26: CVE-2024-8088-inf-loop-zipfile_Path.patch
BuildRequires: autoconf-archive
BuildRequires: automake
BuildRequires: fdupes
@@ -272,7 +250,7 @@ BuildRequires: python3-python-docs-theme >= 2022.1
%endif
%if %{with general}
# required for idle3 (.desktop and .appdata.xml files)
BuildRequires: AppStream
BuildRequires: appstream-glib
BuildRequires: gcc-c++
BuildRequires: gdbm-devel
BuildRequires: gettext
@@ -491,15 +469,30 @@ other applications.
%prep
%setup -q -n %{tarname}
%autopatch -p1 -M 07
%patch -p1 -P 01
%patch -p1 -P 02
%patch -p1 -P 03
%patch -p1 -P 04
%patch -p1 -P 05
%patch -p1 -P 06
%patch -p1 -P 07
%if 0%{?sle_version} && 0%{?sle_version} <= 150300
%patch -p1 -P 11
%patch -P 11 -p1
%endif
%autopatch -p1 -m 12 -M 20
%if ! 0%{?sle_version} || 0%{?sle_version} >= 160000
%patch -p1 -P 15
%patch -p1 -P 16
%patch -p1 -P 17
%patch -p1 -P 18
%patch -p1 -P 19
%patch -p1 -P 20
%patch -p1 -P 21
%endif
%autopatch -p1 -m 22
%patch -p1 -P 22
%patch -p1 -P 23
%patch -p1 -P 24
%patch -p1 -P 25
%patch -p1 -P 26
# drop Autoconf version requirement
sed -i 's/^AC_PREREQ/dnl AC_PREREQ/' configure.ac
@@ -533,9 +526,6 @@ rm Lib/site-packages/README.txt
# Add vendored bluez-devel files
tar xvf %{SOURCE21}
# Don't fail on warnings when building documentation
sed -i -e '/^SPHINXERRORHANDLING/s/-W//' Doc/Makefile
%build
%if %{with doc}
TODAY_DATE=`date -r %{SOURCE0} "+%%B %%d, %%Y"`
@@ -738,7 +728,7 @@ install -m 644 -D -t %{buildroot}%{_datadir}/applications idle%{python_version}.
cp %{SOURCE20} idle%{python_version}.appdata.xml
sed -i -e 's:idle3.desktop:idle%{python_version}.desktop:g' idle%{python_version}.appdata.xml
install -m 644 -D -t %{buildroot}%{_datadir}/metainfo idle%{python_version}.appdata.xml
appstreamcli validate --no-net %{buildroot}%{_datadir}/metainfo/idle%{python_version}.appdata.xml
appstream-util validate-relax --nonet %{buildroot}%{_datadir}/metainfo/idle%{python_version}.appdata.xml
%fdupes %{buildroot}/%{_libdir}/python%{python_version}
%endif
@@ -811,9 +801,6 @@ install -m 755 -D Tools/gdb/libpython.py %{buildroot}%{_datadir}/gdb/auto-load/%
# install devel files to /config
#cp Makefile Makefile.pre.in Makefile.pre $RPM_BUILD_ROOT%{sitedir}/config-%{python_abi}/
# Remove -IVendor/ from python-config boo#1231795
sed -i 's/-IVendor\///' %{buildroot}%{_bindir}/python%{python_abi}-config
# RPM macros
%if %{primary_interpreter}
mkdir -p %{buildroot}%{_rpmconfigdir}/macros.d/
@@ -842,11 +829,6 @@ LD_LIBRARY_PATH=. ./python -O -c "from py_compile import compile; compile('$FAIL
echo %{sitedir}/_import_failed > %{buildroot}/%{sitedir}/site-packages/zzzz-import-failed-hooks.pth
%endif
# For the purposes of reproducibility, it is necessary to eliminate any *.pyc files inside documentation dirs
if [ -d %{buildroot}%{_defaultdocdir} ] ; then
find %{buildroot}%{_defaultdocdir} -type f -name \*.pyc -ls -exec rm -vf '{}' \;
fi
%if %{with general}
%files -n %{python_pkg_name}-tk
%{sitedir}/tkinter

View File

@@ -1,21 +0,0 @@
---
Doc/tools/extensions/pyspecific.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
--- a/Doc/tools/extensions/pyspecific.py
+++ b/Doc/tools/extensions/pyspecific.py
@@ -28,7 +28,13 @@ try:
except ImportError:
from sphinx.environment import NoUri
from sphinx.locale import _ as sphinx_gettext
-from sphinx.util import status_iterator, logging
+try:
+ from sphinx.util.display import status_iterator
+except ImportError:
+ # This method was moved into sphinx.util.display in Sphinx 6.1.0. Before
+ # that it resided in sphinx.util.
+ from sphinx.util import status_iterator
+from sphinx.util import logging
from sphinx.util.nodes import split_explicit_title
from sphinx.writers.text import TextWriter, TextTranslator
from sphinx.writers.latex import LaTeXTranslator