From 12683e3ce818a2f0ab1b16e5710491accb00ef58 Mon Sep 17 00:00:00 2001 From: Thomas Wouters Date: Tue, 7 Apr 2026 23:28:57 +0200 Subject: [PATCH 001/242] Post 3.13.13 --- Include/patchlevel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Include/patchlevel.h b/Include/patchlevel.h index 746b9fcaa5fc1b..439793b02047fd 100644 --- a/Include/patchlevel.h +++ b/Include/patchlevel.h @@ -23,7 +23,7 @@ #define PY_RELEASE_SERIAL 0 /* Version as a string */ -#define PY_VERSION "3.13.13" +#define PY_VERSION "3.13.13+" /*--end constants--*/ /* Version as a single 4-byte hex number, e.g. 0x010502B2 == 1.5.2b2. From 4002c3a4615f2d41cf081595cb0d4e3774957ad6 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:56:10 +0200 Subject: [PATCH 002/242] [3.13] Minor edit: Four space indent in example (gh-148264) (gh-148266) --- Doc/library/itertools.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/itertools.rst b/Doc/library/itertools.rst index 8c352c939e9e54..47c5326d137c50 100644 --- a/Doc/library/itertools.rst +++ b/Doc/library/itertools.rst @@ -855,7 +855,7 @@ and :term:`generators ` which incur interpreter overhead. def running_mean(iterable): "Yield the average of all values seen so far." - # running_mean([8.5, 9.5, 7.5, 6.5]) -> 8.5 9.0 8.5 8.0 + # running_mean([8.5, 9.5, 7.5, 6.5]) → 8.5 9.0 8.5 8.0 return map(truediv, accumulate(iterable), count(1)) def repeatfunc(function, times=None, *args): @@ -934,10 +934,10 @@ and :term:`generators ` which incur interpreter overhead. yield element def unique(iterable, key=None, reverse=False): - "Yield unique elements in sorted order. Supports unhashable inputs." - # unique([[1, 2], [3, 4], [1, 2]]) → [1, 2] [3, 4] - sequenced = sorted(iterable, key=key, reverse=reverse) - return unique_justseen(sequenced, key=key) + "Yield unique elements in sorted order. Supports unhashable inputs." + # unique([[1, 2], [3, 4], [1, 2]]) → [1, 2] [3, 4] + sequenced = sorted(iterable, key=key, reverse=reverse) + return unique_justseen(sequenced, key=key) def sliding_window(iterable, n): "Collect data into overlapping fixed-length chunks or blocks." From 461ae64628185f3df1773b9c8ba5d007b955ee67 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:15:16 +0200 Subject: [PATCH 003/242] [3.13] gh-70039: smtplib: store the server name in ._host in .connect() (GH-115259) (#148272) Original patch by gigaplastik, extended with a few more tests. Addresses gh-70039 and bpo-25852: failure of starttls if connect is called explicitly. (cherry picked from commit 442f83a5ea1b4d334befd231a79c40d6ff41a0bd) Co-authored-by: nmartensen --- Lib/smtplib.py | 2 +- Lib/test/test_smtpnet.py | 53 +++++++++++++++++++ ...4-02-10-21-25-22.gh-issue-70039.6wvcAP.rst | 1 + 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2024-02-10-21-25-22.gh-issue-70039.6wvcAP.rst diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 9bedcc5ff6ce89..95ad966c3939a5 100755 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -253,7 +253,6 @@ def __init__(self, host='', port=0, local_hostname=None, will be used. """ - self._host = host self.timeout = timeout self.esmtp_features = {} self.command_encoding = 'ascii' @@ -344,6 +343,7 @@ def connect(self, host='localhost', port=0, source_address=None): port = int(port) except ValueError: raise OSError("nonnumeric port") + self._host = host if not port: port = self.default_port sys.audit("smtplib.connect", self, host, port) diff --git a/Lib/test/test_smtpnet.py b/Lib/test/test_smtpnet.py index d765746987bc4b..861e7e6a725c40 100644 --- a/Lib/test/test_smtpnet.py +++ b/Lib/test/test_smtpnet.py @@ -45,6 +45,59 @@ def test_connect_starttls(self): server.ehlo() server.quit() + def test_connect_host_port_starttls(self): + support.get_attribute(smtplib, 'SMTP_SSL') + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket_helper.transient_internet(self.testServer): + server = smtplib.SMTP(f'{self.testServer}:{self.remotePort}') + try: + server.starttls(context=context) + except smtplib.SMTPException as e: + if e.args[0] == 'STARTTLS extension not supported by server.': + unittest.skip(e.args[0]) + else: + raise + server.ehlo() + server.quit() + + def test_explicit_connect_starttls(self): + support.get_attribute(smtplib, 'SMTP_SSL') + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket_helper.transient_internet(self.testServer): + server = smtplib.SMTP() + server.connect(self.testServer, self.remotePort) + try: + server.starttls(context=context) + except smtplib.SMTPException as e: + if e.args[0] == 'STARTTLS extension not supported by server.': + unittest.skip(e.args[0]) + else: + raise + server.ehlo() + server.quit() + + def test_explicit_connect_host_port_starttls(self): + support.get_attribute(smtplib, 'SMTP_SSL') + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.check_hostname = False + context.verify_mode = ssl.CERT_NONE + with socket_helper.transient_internet(self.testServer): + server = smtplib.SMTP() + server.connect(f'{self.testServer}:{self.remotePort}') + try: + server.starttls(context=context) + except smtplib.SMTPException as e: + if e.args[0] == 'STARTTLS extension not supported by server.': + unittest.skip(e.args[0]) + else: + raise + server.ehlo() + server.quit() + class SmtpSSLTest(unittest.TestCase): testServer = SMTP_TEST_SERVER diff --git a/Misc/NEWS.d/next/Library/2024-02-10-21-25-22.gh-issue-70039.6wvcAP.rst b/Misc/NEWS.d/next/Library/2024-02-10-21-25-22.gh-issue-70039.6wvcAP.rst new file mode 100644 index 00000000000000..8bb2cd188e89fa --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-10-21-25-22.gh-issue-70039.6wvcAP.rst @@ -0,0 +1 @@ +Fixed bug where :meth:`smtplib.SMTP.starttls` could fail if :meth:`smtplib.SMTP.connect` is called explicitly rather than implicitly. From 0438467b2d7faa66b3c90f76e2d15c6c3b8456ff Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:51:12 +0200 Subject: [PATCH 004/242] [3.13] gh-146646: Document that glob functions suppress OSError (GH-147996) (#148289) gh-146646: Document that glob functions suppress OSError (GH-147996) (cherry picked from commit 8000a9de3c0b22f8202898a424c1008e13bd16ce) Co-authored-by: WYSIATI --- Doc/library/glob.rst | 10 ++++++++++ Doc/library/pathlib.rst | 10 ++++++++++ .../2026-04-02-07-20-00.gh-issue-146646.GlobDoc1.rst | 3 +++ 3 files changed, 23 insertions(+) create mode 100644 Misc/NEWS.d/next/Documentation/2026-04-02-07-20-00.gh-issue-146646.GlobDoc1.rst diff --git a/Doc/library/glob.rst b/Doc/library/glob.rst index 7580c564494e3a..9ce031b0133d41 100644 --- a/Doc/library/glob.rst +++ b/Doc/library/glob.rst @@ -83,6 +83,11 @@ The :mod:`!glob` module defines the following functions: This function may return duplicate path names if *pathname* contains multiple "``**``" patterns and *recursive* is true. + .. note:: + Any :exc:`OSError` exceptions raised from scanning the filesystem are + suppressed. This includes :exc:`PermissionError` when accessing + directories without read permission. + .. versionchanged:: 3.5 Support for recursive globs using "``**``". @@ -106,6 +111,11 @@ The :mod:`!glob` module defines the following functions: This function may return duplicate path names if *pathname* contains multiple "``**``" patterns and *recursive* is true. + .. note:: + Any :exc:`OSError` exceptions raised from scanning the filesystem are + suppressed. This includes :exc:`PermissionError` when accessing + directories without read permission. + .. versionchanged:: 3.5 Support for recursive globs using "``**``". diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index cfb9971cec8c3d..11ff4c4bbd4435 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1303,6 +1303,11 @@ Reading directories ``False``, this method follows symlinks except when expanding "``**``" wildcards. Set *recurse_symlinks* to ``True`` to always follow symlinks. + .. note:: + Any :exc:`OSError` exceptions raised from scanning the filesystem are + suppressed. This includes :exc:`PermissionError` when accessing + directories without read permission. + .. audit-event:: pathlib.Path.glob self,pattern pathlib.Path.glob .. versionchanged:: 3.12 @@ -1329,6 +1334,11 @@ Reading directories The paths are returned in no particular order. If you need a specific order, sort the results. + .. note:: + Any :exc:`OSError` exceptions raised from scanning the filesystem are + suppressed. This includes :exc:`PermissionError` when accessing + directories without read permission. + .. seealso:: :ref:`pathlib-pattern-language` and :meth:`Path.glob` documentation. diff --git a/Misc/NEWS.d/next/Documentation/2026-04-02-07-20-00.gh-issue-146646.GlobDoc1.rst b/Misc/NEWS.d/next/Documentation/2026-04-02-07-20-00.gh-issue-146646.GlobDoc1.rst new file mode 100644 index 00000000000000..4e89270442a33b --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2026-04-02-07-20-00.gh-issue-146646.GlobDoc1.rst @@ -0,0 +1,3 @@ +Document that :func:`glob.glob`, :func:`glob.iglob`, +:meth:`pathlib.Path.glob`, and :meth:`pathlib.Path.rglob` silently suppress +:exc:`OSError` exceptions raised from scanning the filesystem. From 8337805ad172c39eac4acc37527a01c35ea8361c Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:40:28 +0200 Subject: [PATCH 005/242] [3.13] gh-106318: Add example for str.swapcase() method (GH-144575) (#148297) Co-authored-by: Adorilson Bezerra --- Doc/library/stdtypes.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 95d281811bedc8..13275dc760f815 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -2620,8 +2620,22 @@ expression support in the :mod:`re` module). .. method:: str.swapcase() Return a copy of the string with uppercase characters converted to lowercase and - vice versa. Note that it is not necessarily true that - ``s.swapcase().swapcase() == s``. + vice versa. For example: + + .. doctest:: + + >>> 'Hello World'.swapcase() + 'hELLO wORLD' + + Note that it is not necessarily true that ``s.swapcase().swapcase() == s``. + For example: + + .. doctest:: + + >>> 'straße'.swapcase().swapcase() + 'strasse' + + See also :meth:`str.lower` and :meth:`str.upper`. .. method:: str.title() From 88b4c2305e83cfcf9b23c37091b2d03ed8c8f2a8 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:07:54 +0200 Subject: [PATCH 006/242] [3.13] gh-148067: Fix typo in asyncio event loop docs: 'signals' -> 'signal' (GH-148073) (#148245) Co-authored-by: TT <70463940+Herrtian@users.noreply.github.com> --- Doc/library/asyncio-eventloop.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 602e2faf38d0c7..6fdbfce54d0d89 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -2033,7 +2033,7 @@ Wait until a file descriptor received some data using the Set signal handlers for SIGINT and SIGTERM ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -(This ``signals`` example only works on Unix.) +(This ``signal`` example only works on Unix.) Register handlers for signals :const:`~signal.SIGINT` and :const:`~signal.SIGTERM` using the :meth:`loop.add_signal_handler` method:: From 83cca73a0da6e66bc81482196185748065d1f9e5 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:52:59 +0200 Subject: [PATCH 007/242] [3.13] gh-148091: clarify asyncio.Future.cancel(msg) behaviour (GH-148248) (#148300) gh-148091: clarify asyncio.Future.cancel(msg) behaviour (GH-148248) (cherry picked from commit 2acb8d9257c4f5049777d9d462092373b0b3feca) Co-authored-by: Manoj K M <136242596+manoj-k-m@users.noreply.github.com> --- Doc/library/asyncio-future.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/library/asyncio-future.rst b/Doc/library/asyncio-future.rst index 1a08afc02222d7..5c32bd3eaad828 100644 --- a/Doc/library/asyncio-future.rst +++ b/Doc/library/asyncio-future.rst @@ -195,6 +195,10 @@ Future Object Otherwise, change the Future's state to *cancelled*, schedule the callbacks, and return ``True``. + The optional string argument *msg* is passed as the argument to the + :exc:`CancelledError` exception raised when a cancelled Future + is awaited. + .. versionchanged:: 3.9 Added the *msg* parameter. From fe08867347f5136b05bb8d3b3ef132b2987435f8 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 9 Apr 2026 18:06:27 +0300 Subject: [PATCH 008/242] [3.13] gh-148254: Use singular "sec" in timeit verbose output (GH-148290) (#148304) Co-authored-by: gaweng <38250674+gaweng@users.noreply.github.com> --- Lib/test/test_timeit.py | 28 +++++++++---------- Lib/timeit.py | 2 +- ...-04-09-12-42-42.gh-issue-148254.Xt7vKs.rst | 2 ++ 3 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-09-12-42-42.gh-issue-148254.Xt7vKs.rst diff --git a/Lib/test/test_timeit.py b/Lib/test/test_timeit.py index 72a104fc1a6790..0ff3170d8f8814 100644 --- a/Lib/test/test_timeit.py +++ b/Lib/test/test_timeit.py @@ -304,7 +304,7 @@ def test_main_help(self): def test_main_verbose(self): s = self.run_main(switches=['-v']) self.assertEqual(s, dedent("""\ - 1 loop -> 1 secs + 1 loop -> 1 sec raw times: 1 sec, 1 sec, 1 sec, 1 sec, 1 sec @@ -314,19 +314,19 @@ def test_main_verbose(self): def test_main_very_verbose(self): s = self.run_main(seconds_per_increment=0.000_030, switches=['-vv']) self.assertEqual(s, dedent("""\ - 1 loop -> 3e-05 secs - 2 loops -> 6e-05 secs - 5 loops -> 0.00015 secs - 10 loops -> 0.0003 secs - 20 loops -> 0.0006 secs - 50 loops -> 0.0015 secs - 100 loops -> 0.003 secs - 200 loops -> 0.006 secs - 500 loops -> 0.015 secs - 1000 loops -> 0.03 secs - 2000 loops -> 0.06 secs - 5000 loops -> 0.15 secs - 10000 loops -> 0.3 secs + 1 loop -> 3e-05 sec + 2 loops -> 6e-05 sec + 5 loops -> 0.00015 sec + 10 loops -> 0.0003 sec + 20 loops -> 0.0006 sec + 50 loops -> 0.0015 sec + 100 loops -> 0.003 sec + 200 loops -> 0.006 sec + 500 loops -> 0.015 sec + 1000 loops -> 0.03 sec + 2000 loops -> 0.06 sec + 5000 loops -> 0.15 sec + 10000 loops -> 0.3 sec raw times: 300 msec, 300 msec, 300 msec, 300 msec, 300 msec diff --git a/Lib/timeit.py b/Lib/timeit.py index 02cfafaf36e5d1..f5f75048be18df 100755 --- a/Lib/timeit.py +++ b/Lib/timeit.py @@ -322,7 +322,7 @@ def main(args=None, *, _wrap_timer=None): callback = None if verbose: def callback(number, time_taken): - msg = "{num} loop{s} -> {secs:.{prec}g} secs" + msg = "{num} loop{s} -> {secs:.{prec}g} sec" plural = (number != 1) print(msg.format(num=number, s='s' if plural else '', secs=time_taken, prec=precision)) diff --git a/Misc/NEWS.d/next/Library/2026-04-09-12-42-42.gh-issue-148254.Xt7vKs.rst b/Misc/NEWS.d/next/Library/2026-04-09-12-42-42.gh-issue-148254.Xt7vKs.rst new file mode 100644 index 00000000000000..818310c31b9de6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-09-12-42-42.gh-issue-148254.Xt7vKs.rst @@ -0,0 +1,2 @@ +Use singular "sec" instead of "secs" in :mod:`timeit` verbose output for +consistency with other time units. From 0530f105ce7e2a60b92006a40e5f426bd80d1b7b Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:51:34 +0200 Subject: [PATCH 009/242] [3.13] gh-145831: email.quoprimime: `decode()` leaves stray `\r` when `eol='\r\n'` (GH-145832) (#148311) decoded[:-1] only strips one character, leaving a stray \r when eol is two characters. Fix: decoded[:-len(eol)]. (cherry picked from commit 1a0edb1fa899c067f19b09598b45cdb6e733c4ee) Co-authored-by: Stefan Zetzsche <120379523+stefanzetzsche@users.noreply.github.com> --- Lib/email/quoprimime.py | 2 +- Lib/test/test_email/test_email.py | 9 +++++++++ .../2026-03-11-15-09-52.gh-issue-145831._sW94w.rst | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-11-15-09-52.gh-issue-145831._sW94w.rst diff --git a/Lib/email/quoprimime.py b/Lib/email/quoprimime.py index 27fcbb5a26e3ae..a5a6eef0b6d20b 100644 --- a/Lib/email/quoprimime.py +++ b/Lib/email/quoprimime.py @@ -272,7 +272,7 @@ def decode(encoded, eol=NL): decoded += eol # Special case if original string did not end with eol if encoded[-1] not in '\r\n' and decoded.endswith(eol): - decoded = decoded[:-1] + decoded = decoded[:-len(eol)] return decoded diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py index e40904f709057e..9d968018cba451 100644 --- a/Lib/test/test_email/test_email.py +++ b/Lib/test/test_email/test_email.py @@ -4802,6 +4802,15 @@ def test_decode_soft_line_break(self): def test_decode_false_quoting(self): self._test_decode('A=1,B=A ==> A+B==2', 'A=1,B=A ==> A+B==2') + def test_decode_crlf_eol_no_trailing_newline(self): + self._test_decode('abc', 'abc', eol='\r\n') + + def test_decode_crlf_eol_multiline_no_trailing_newline(self): + self._test_decode('a\r\nb', 'a\r\nb', eol='\r\n') + + def test_decode_crlf_eol_with_trailing_newline(self): + self._test_decode('abc\r\n', 'abc\r\n', eol='\r\n') + def _test_encode(self, body, expected_encoded_body, maxlinelen=None, eol=None): kwargs = {} if maxlinelen is None: diff --git a/Misc/NEWS.d/next/Library/2026-03-11-15-09-52.gh-issue-145831._sW94w.rst b/Misc/NEWS.d/next/Library/2026-03-11-15-09-52.gh-issue-145831._sW94w.rst new file mode 100644 index 00000000000000..454b62bc0db95f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-11-15-09-52.gh-issue-145831._sW94w.rst @@ -0,0 +1,2 @@ +Fix :func:`!email.quoprimime.decode` leaving a stray ``\r`` when +``eol='\r\n'`` by stripping the full *eol* string instead of one character. From a14e4e3a1afb00b0a4f0213ae6d4f994c500b377 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:36:16 +0300 Subject: [PATCH 010/242] [3.13] Fix mixed line endings with pre-commit (GH-148336) (#148339) Co-authored-by: Zachary Ware --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 055f8854a970f8..ff08c7583fc4de 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,6 +78,9 @@ repos: exclude: Lib/test/tokenizedata/coding20731.py - id: end-of-file-fixer files: '^\.github/CODEOWNERS$' + - id: mixed-line-ending + args: [--fix=auto] + exclude: '^Lib/test/.*data/' - id: trailing-whitespace types_or: [c, inc, python, rst, yaml] - id: trailing-whitespace From 8e369d364518d668898f2142f8daf402e2cd3ddc Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:44:45 +0200 Subject: [PATCH 011/242] [3.13] gh-145105: Fix crash in csv.reader with re-entrant iterator (GH-145106) (#148405) gh-145105: Fix crash in csv.reader with re-entrant iterator (GH-145106) When a custom iterator calls next() on the same csv.reader from within __next__, the inner iteration sets self->fields to NULL. The outer iteration then crashes in parse_save_field() by passing NULL to PyList_Append. Add a guard after PyIter_Next() to detect that fields was set to NULL by a re-entrant call, and raise csv.Error instead of crashing. (cherry picked from commit 20994b1809f9c162e4cae01a5af08bd492ede9f9) Co-authored-by: Ramin Farajpour Cami --- Lib/test/test_csv.py | 27 +++++++++++++++++++ ...0.gh-issue-145105.csv-reader-reentrant.rst | 2 ++ Modules/_csv.c | 6 +++++ 3 files changed, 35 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-02-22-00-00-00.gh-issue-145105.csv-reader-reentrant.rst diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py index ce5c03659f1979..6ea388313bbfd4 100644 --- a/Lib/test/test_csv.py +++ b/Lib/test/test_csv.py @@ -552,6 +552,33 @@ def test_roundtrip_escaped_unquoted_newlines(self): self.assertEqual(row, rows[i]) + def test_reader_reentrant_iterator(self): + # gh-145105: re-entering the reader from the iterator must not crash. + class ReentrantIter: + def __init__(self): + self.reader = None + self.n = 0 + def __iter__(self): + return self + def __next__(self): + self.n += 1 + if self.n == 1: + try: + next(self.reader) + except StopIteration: + pass + return "a,b" + if self.n == 2: + return "x" + raise StopIteration + + it = ReentrantIter() + reader = csv.reader(it) + it.reader = reader + with self.assertRaises(csv.Error): + next(reader) + + class TestDialectRegistry(unittest.TestCase): def test_registry_badargs(self): self.assertRaises(TypeError, csv.list_dialects, None) diff --git a/Misc/NEWS.d/next/Library/2026-02-22-00-00-00.gh-issue-145105.csv-reader-reentrant.rst b/Misc/NEWS.d/next/Library/2026-02-22-00-00-00.gh-issue-145105.csv-reader-reentrant.rst new file mode 100644 index 00000000000000..bc61cc43a5aa33 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-02-22-00-00-00.gh-issue-145105.csv-reader-reentrant.rst @@ -0,0 +1,2 @@ +Fix crash in :mod:`csv` reader when iterating with a re-entrant iterator +that calls :func:`next` on the same reader from within ``__next__``. diff --git a/Modules/_csv.c b/Modules/_csv.c index 0b7ff2575fcfec..755d005f4d82d5 100644 --- a/Modules/_csv.c +++ b/Modules/_csv.c @@ -957,6 +957,12 @@ Reader_iternext(ReaderObj *self) Py_DECREF(lineobj); return NULL; } + if (self->fields == NULL) { + PyErr_SetString(module_state->error_obj, + "iterator has already advanced the reader"); + Py_DECREF(lineobj); + return NULL; + } ++self->line_num; kind = PyUnicode_KIND(lineobj); data = PyUnicode_DATA(lineobj); From fdc7f7e742113e8ad4a267c44d852154d4b15c95 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:06:41 +0300 Subject: [PATCH 012/242] [3.13] Default GHA permissions to `contents: read` (GH-148346) (#148387) (cherry picked from commit 9c9df8ac8cbb8f539b3f342d01e40b7a0a57dcbf) --- .github/workflows/add-issue-header.yml | 3 ++- .github/workflows/build.yml | 4 +++- .github/workflows/jit.yml | 3 ++- .github/workflows/lint.yml | 3 ++- .github/workflows/mypy.yml | 3 ++- .github/workflows/new-bugs-announce-notifier.yml | 3 ++- .github/workflows/require-pr-label.yml | 3 ++- .github/workflows/reusable-cifuzz.yml | 3 ++- .github/workflows/reusable-context.yml | 3 ++- .github/workflows/reusable-docs.yml | 3 ++- .github/workflows/reusable-macos.yml | 3 ++- .github/workflows/reusable-san.yml | 3 ++- .github/workflows/reusable-ubuntu.yml | 3 ++- .github/workflows/reusable-wasi.yml | 3 ++- .github/workflows/reusable-windows-msi.yml | 3 ++- .github/workflows/reusable-windows.yml | 3 ++- .github/workflows/stale.yml | 3 ++- .github/workflows/verify-ensurepip-wheels.yml | 3 ++- .github/workflows/verify-expat.yml | 3 ++- 19 files changed, 39 insertions(+), 19 deletions(-) diff --git a/.github/workflows/add-issue-header.yml b/.github/workflows/add-issue-header.yml index 00b7ae50cb9935..4c25976b9c24f7 100644 --- a/.github/workflows/add-issue-header.yml +++ b/.github/workflows/add-issue-header.yml @@ -12,7 +12,8 @@ on: # Only ever run once - opened -permissions: {} +permissions: + contents: read jobs: add-header: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a45f2c069a755d..c50d060051bc4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,8 @@ on: - 'main' - '3.*' -permissions: {} +permissions: + contents: read concurrency: # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency @@ -540,6 +541,7 @@ jobs: needs.build-context.outputs.run-ci-fuzz == 'true' || needs.build-context.outputs.run-ci-fuzz-stdlib == 'true' permissions: + contents: read security-events: write strategy: fail-fast: false diff --git a/.github/workflows/jit.yml b/.github/workflows/jit.yml index f19394227d090c..a9e9b8e3a4cd8a 100644 --- a/.github/workflows/jit.yml +++ b/.github/workflows/jit.yml @@ -18,7 +18,8 @@ on: - '!**/*.ini' workflow_dispatch: -permissions: {} +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fb2b94b7362308..e9a4eb2b0808cb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,7 +2,8 @@ name: Lint on: [push, pull_request, workflow_dispatch] -permissions: {} +permissions: + contents: read env: FORCE_COLOR: 1 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 61c8562a338a01..0c97ba4861dbc2 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -30,7 +30,8 @@ on: - "Tools/requirements-dev.txt" workflow_dispatch: -permissions: {} +permissions: + contents: read env: PIP_DISABLE_PIP_VERSION_CHECK: 1 diff --git a/.github/workflows/new-bugs-announce-notifier.yml b/.github/workflows/new-bugs-announce-notifier.yml index 14860e56600d06..e585657dde6881 100644 --- a/.github/workflows/new-bugs-announce-notifier.yml +++ b/.github/workflows/new-bugs-announce-notifier.yml @@ -5,7 +5,8 @@ on: types: - opened -permissions: {} +permissions: + contents: read jobs: notify-new-bugs-announce: diff --git a/.github/workflows/require-pr-label.yml b/.github/workflows/require-pr-label.yml index ebc5699d490841..206f24cf9d5fb3 100644 --- a/.github/workflows/require-pr-label.yml +++ b/.github/workflows/require-pr-label.yml @@ -4,7 +4,8 @@ on: pull_request: types: [opened, reopened, labeled, unlabeled, synchronize] -permissions: {} +permissions: + contents: read jobs: label: diff --git a/.github/workflows/reusable-cifuzz.yml b/.github/workflows/reusable-cifuzz.yml index f06b193d3715fb..9b49e7fd26f007 100644 --- a/.github/workflows/reusable-cifuzz.yml +++ b/.github/workflows/reusable-cifuzz.yml @@ -13,7 +13,8 @@ on: required: true type: string -permissions: {} +permissions: + contents: read jobs: cifuzz: diff --git a/.github/workflows/reusable-context.yml b/.github/workflows/reusable-context.yml index 6416115b1de058..8ed6873104db7b 100644 --- a/.github/workflows/reusable-context.yml +++ b/.github/workflows/reusable-context.yml @@ -48,7 +48,8 @@ on: # yamllint disable-line rule:truthy description: Whether to run the Windows tests value: ${{ jobs.compute-changes.outputs.run-windows-tests }} # bool -permissions: {} +permissions: + contents: read jobs: compute-changes: diff --git a/.github/workflows/reusable-docs.yml b/.github/workflows/reusable-docs.yml index e1c35021432ad0..bee44e8df27663 100644 --- a/.github/workflows/reusable-docs.yml +++ b/.github/workflows/reusable-docs.yml @@ -4,7 +4,8 @@ on: workflow_call: workflow_dispatch: -permissions: {} +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/reusable-macos.yml b/.github/workflows/reusable-macos.yml index dbc6fd3774a5ba..e7e2a0afd341f7 100644 --- a/.github/workflows/reusable-macos.yml +++ b/.github/workflows/reusable-macos.yml @@ -12,7 +12,8 @@ on: required: true type: string -permissions: {} +permissions: + contents: read env: FORCE_COLOR: 1 diff --git a/.github/workflows/reusable-san.yml b/.github/workflows/reusable-san.yml index f5e7f48b860b2f..82d6759b6347e3 100644 --- a/.github/workflows/reusable-san.yml +++ b/.github/workflows/reusable-san.yml @@ -12,7 +12,8 @@ on: type: boolean default: false -permissions: {} +permissions: + contents: read env: FORCE_COLOR: 1 diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml index 3f1abce25c9684..f03908afc14b83 100644 --- a/.github/workflows/reusable-ubuntu.yml +++ b/.github/workflows/reusable-ubuntu.yml @@ -9,7 +9,8 @@ on: type: boolean default: false -permissions: {} +permissions: + contents: read env: FORCE_COLOR: 1 diff --git a/.github/workflows/reusable-wasi.yml b/.github/workflows/reusable-wasi.yml index e9c032f93bbf2d..2e5d4b8dd56618 100644 --- a/.github/workflows/reusable-wasi.yml +++ b/.github/workflows/reusable-wasi.yml @@ -3,7 +3,8 @@ name: Reusable WASI on: workflow_call: -permissions: {} +permissions: + contents: read env: FORCE_COLOR: 1 diff --git a/.github/workflows/reusable-windows-msi.yml b/.github/workflows/reusable-windows-msi.yml index e836944f465bb3..e690224f35537b 100644 --- a/.github/workflows/reusable-windows-msi.yml +++ b/.github/workflows/reusable-windows-msi.yml @@ -8,7 +8,8 @@ on: required: true type: string -permissions: {} +permissions: + contents: read env: FORCE_COLOR: 1 diff --git a/.github/workflows/reusable-windows.yml b/.github/workflows/reusable-windows.yml index 41ba50d8665d80..919ab4a4fb33b7 100644 --- a/.github/workflows/reusable-windows.yml +++ b/.github/workflows/reusable-windows.yml @@ -17,7 +17,8 @@ on: type: boolean default: false -permissions: {} +permissions: + contents: read env: FORCE_COLOR: 1 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 42ddb713c10393..1fbc4a20dbc7dd 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -4,7 +4,8 @@ on: schedule: - cron: "0 */6 * * *" -permissions: {} +permissions: + contents: read jobs: stale: diff --git a/.github/workflows/verify-ensurepip-wheels.yml b/.github/workflows/verify-ensurepip-wheels.yml index 4ac25bc909b13f..cb40f6abc0b3b7 100644 --- a/.github/workflows/verify-ensurepip-wheels.yml +++ b/.github/workflows/verify-ensurepip-wheels.yml @@ -13,7 +13,8 @@ on: - '.github/workflows/verify-ensurepip-wheels.yml' - 'Tools/build/verify_ensurepip_wheels.py' -permissions: {} +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} diff --git a/.github/workflows/verify-expat.yml b/.github/workflows/verify-expat.yml index e193dfa4603e8a..472a11db2da5fb 100644 --- a/.github/workflows/verify-expat.yml +++ b/.github/workflows/verify-expat.yml @@ -11,7 +11,8 @@ on: - 'Modules/expat/**' - '.github/workflows/verify-expat.yml' -permissions: {} +permissions: + contents: read concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} From 2f30fcf6748ae3baace466a1052787ba2e54fe65 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:10:53 +0200 Subject: [PATCH 013/242] [3.13] gh-148337: Document `importlib.resources` security model (GH-148340) (#148355) gh-148337: Document `importlib.resources` security model (GH-148340) (cherry picked from commit 70b86e7829c42d36c80853ba9bf1da0d8464065b) Co-authored-by: Stan Ulbrych --- Doc/library/importlib.resources.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/library/importlib.resources.rst b/Doc/library/importlib.resources.rst index 7a11f4fe069004..46eab78a22b66a 100644 --- a/Doc/library/importlib.resources.rst +++ b/Doc/library/importlib.resources.rst @@ -31,6 +31,12 @@ not** have to exist as physical files and directories on the file system: for example, a package and its resources can be imported from a zip file using :py:mod:`zipimport`. +.. warning:: + + :mod:`importlib.resources` follows the same security model as the built-in + :func:`open` function. Passing untrusted inputs to the functions + in this module is unsafe. + .. note:: This module provides functionality similar to `pkg_resources From 4830d291e7480b197dbafdca36deaa0939984103 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:52:32 +0200 Subject: [PATCH 014/242] [3.13] gh-147965: Add shutdown() to multiprocessing.Queue excluded methods (GH-147970) (#148417) gh-147965: Add shutdown() to multiprocessing.Queue excluded methods (GH-147970) The multiprocessing.Queue documentation states it implements all methods of queue.Queue except task_done() and join(). Since queue.Queue.shutdown() was added in Python 3.13, multiprocessing.Queue also does not implement it. Update the docs to include shutdown() in the list of excluded methods. (cherry picked from commit 22290ed011a8ac4060390e57f53053ab932fb3f3) Co-authored-by: WYSIATI --- Doc/library/multiprocessing.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst index 0605a0c03784f1..304e580eb38713 100644 --- a/Doc/library/multiprocessing.rst +++ b/Doc/library/multiprocessing.rst @@ -899,7 +899,8 @@ For an example of the usage of queues for interprocess communication see standard library's :mod:`queue` module are raised to signal timeouts. :class:`Queue` implements all the methods of :class:`queue.Queue` except for - :meth:`~queue.Queue.task_done` and :meth:`~queue.Queue.join`. + :meth:`~queue.Queue.task_done`, :meth:`~queue.Queue.join`, and + :meth:`~queue.Queue.shutdown`. .. method:: qsize() From a268d3fcefe2c5d5aeda6b53cc0399ea59d7e16b Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Sun, 12 Apr 2026 19:08:36 +0200 Subject: [PATCH 015/242] [3.13] gh-146313: Fix multiprocessing ResourceTracker deadlock after os.fork() (GH-146316) (#148426) gh-146313: Fix multiprocessing ResourceTracker deadlock after os.fork() (GH-146316) `ResourceTracker.__del__` (added in gh-88887 circa Python 3.12) calls os.waitpid(pid, 0) which blocks indefinitely if a process created via os.fork() still holds the tracker pipe's write end. The tracker never sees EOF, never exits, and the parent hangs at interpreter shutdown. Fix with two layers: - **At-fork handler.** An os.register_at_fork(after_in_child=...) handler closes the inherited pipe fd in the child unless a preserve flag is set. popen_fork.Popen._launch() sets the flag before its fork so mp.Process(fork) children keep the fd and reuse the parent's tracker (preserving gh-80849). Raw os.fork() children close the fd, letting the parent reap promptly. - **Timeout safety-net.** _stop_locked() gains a wait_timeout parameter. When called from `__del__`, it polls with WNOHANG using exponential backoff for up to 1 second instead of blocking indefinitely. The at-fork handler makes this unreachable in well-behaved paths; it remains for abnormal shutdowns. (cherry picked from commit 3a7df632c96eb6c5de12fac08d1da42df9e25334) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> Co-authored-by: Itamar Oren --- Lib/multiprocessing/popen_fork.py | 12 +- Lib/multiprocessing/resource_tracker.py | 89 ++++++++- Lib/test/_test_multiprocessing.py | 182 +++++++++++++++++- ...-03-22-23-42-22.gh-issue-146313.RtDeAd.rst | 4 + 4 files changed, 279 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-22-23-42-22.gh-issue-146313.RtDeAd.rst diff --git a/Lib/multiprocessing/popen_fork.py b/Lib/multiprocessing/popen_fork.py index a57ef6bdad5ccc..05593737dbb9c1 100644 --- a/Lib/multiprocessing/popen_fork.py +++ b/Lib/multiprocessing/popen_fork.py @@ -64,7 +64,17 @@ def _launch(self, process_obj): code = 1 parent_r, child_w = os.pipe() child_r, parent_w = os.pipe() - self.pid = os.fork() + # gh-146313: Tell the resource tracker's at-fork handler to keep + # the inherited pipe fd so this child reuses the parent's tracker + # (gh-80849) rather than closing it and launching its own. + from .resource_tracker import _fork_intent + _fork_intent.preserve_fd = True + try: + self.pid = os.fork() + finally: + # Reset in both parent and child so the flag does not leak + # into a subsequent raw os.fork() or nested Process launch. + _fork_intent.preserve_fd = False if self.pid == 0: try: atexit._clear() diff --git a/Lib/multiprocessing/resource_tracker.py b/Lib/multiprocessing/resource_tracker.py index 22e3bbcf21b017..028c1f60e59864 100644 --- a/Lib/multiprocessing/resource_tracker.py +++ b/Lib/multiprocessing/resource_tracker.py @@ -20,6 +20,7 @@ import signal import sys import threading +import time import warnings from collections import deque @@ -79,6 +80,10 @@ def __init__(self): # The reader should understand all formats. self._use_simple_format = True + # Set to True by _stop_locked() if the waitpid polling loop ran to + # its timeout without reaping the tracker. Exposed for tests. + self._waitpid_timed_out = False + def _reentrant_call_error(self): # gh-109629: this happens if an explicit call to the ResourceTracker # gets interrupted by a garbage collection, invoking a finalizer (*) @@ -91,16 +96,51 @@ def __del__(self): # making sure child processess are cleaned before ResourceTracker # gets destructed. # see https://github.com/python/cpython/issues/88887 - self._stop(use_blocking_lock=False) + # gh-146313: use a timeout to avoid deadlocking if a forked child + # still holds the pipe's write end open. + self._stop(use_blocking_lock=False, wait_timeout=1.0) + + def _after_fork_in_child(self): + # gh-146313: Called in the child right after os.fork(). + # + # The tracker process is a child of the *parent*, not of us, so we + # could never waitpid() it anyway. Clearing _pid means our __del__ + # becomes a no-op (the early return for _pid is None). + # + # Whether we keep the inherited _fd depends on who forked us: + # + # - multiprocessing.Process with the 'fork' start method sets + # _fork_intent.preserve_fd before forking. The child keeps the + # fd and reuses the parent's tracker (gh-80849). This is safe + # because multiprocessing's atexit handler joins all children + # before the parent's __del__ runs, so by then the fd copies + # are gone and the parent can reap the tracker promptly. + # + # - A raw os.fork() leaves the flag unset. We close the fd in the child after forking so + # the parent's __del__ can reap the tracker without waiting + # for the child to exit. If we later need a tracker, ensure_running() + # will launch a fresh one. + self._lock._at_fork_reinit() + self._reentrant_messages.clear() + self._pid = None + self._exitcode = None + if (self._fd is not None and + not getattr(_fork_intent, 'preserve_fd', False)): + fd = self._fd + self._fd = None + try: + os.close(fd) + except OSError: + pass - def _stop(self, use_blocking_lock=True): + def _stop(self, use_blocking_lock=True, wait_timeout=None): if use_blocking_lock: with self._lock: - self._stop_locked() + self._stop_locked(wait_timeout=wait_timeout) else: acquired = self._lock.acquire(blocking=False) try: - self._stop_locked() + self._stop_locked(wait_timeout=wait_timeout) finally: if acquired: self._lock.release() @@ -110,6 +150,10 @@ def _stop_locked( close=os.close, waitpid=os.waitpid, waitstatus_to_exitcode=os.waitstatus_to_exitcode, + monotonic=time.monotonic, + sleep=time.sleep, + WNOHANG=getattr(os, 'WNOHANG', None), + wait_timeout=None, ): # This shouldn't happen (it might when called by a finalizer) # so we check for it anyway. @@ -126,7 +170,30 @@ def _stop_locked( self._fd = None try: - _, status = waitpid(self._pid, 0) + if wait_timeout is None: + _, status = waitpid(self._pid, 0) + else: + # gh-146313: A forked child may still hold the pipe's write + # end open, preventing the tracker from seeing EOF and + # exiting. Poll with WNOHANG to avoid blocking forever. + deadline = monotonic() + wait_timeout + delay = 0.001 + while True: + result_pid, status = waitpid(self._pid, WNOHANG) + if result_pid != 0: + break + remaining = deadline - monotonic() + if remaining <= 0: + # The tracker is still running; it will be + # reparented to PID 1 (or the nearest subreaper) + # when we exit, and reaped there once all pipe + # holders release their fd. + self._pid = None + self._exitcode = None + self._waitpid_timed_out = True + return + delay = min(delay * 2, remaining, 0.1) + sleep(delay) except ChildProcessError: self._pid = None self._exitcode = None @@ -312,12 +379,24 @@ def _send(self, cmd, name, rtype): self._ensure_running_and_write(msg) +# gh-146313: Per-thread flag set by .popen_fork.Popen._launch() just before +# os.fork(), telling _after_fork_in_child() to keep the inherited pipe fd so +# the child can reuse this tracker (gh-80849). Unset for raw os.fork() calls, +# where the child instead closes the fd so the parent's __del__ can reap the +# tracker. Using threading.local() keeps multiple threads calling +# popen_fork.Popen._launch() at once from clobbering eachothers intent. +_fork_intent = threading.local() + _resource_tracker = ResourceTracker() ensure_running = _resource_tracker.ensure_running register = _resource_tracker.register unregister = _resource_tracker.unregister getfd = _resource_tracker.getfd +# gh-146313: See _after_fork_in_child docstring. +if hasattr(os, 'register_at_fork'): + os.register_at_fork(after_in_child=_resource_tracker._after_fork_in_child) + def _decode_message(line): if line.startswith(b'{'): diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index f3fd8f0ac30c7f..d35fa12f17c0c1 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -6007,8 +6007,9 @@ def test_resource_tracker_sigkill(self): def _is_resource_tracker_reused(conn, pid): from multiprocessing.resource_tracker import _resource_tracker _resource_tracker.ensure_running() - # The pid should be None in the child process, expect for the fork - # context. It should not be a new value. + # The pid should be None in the child (the at-fork handler clears + # it for fork; spawn/forkserver children never had it set). It + # should not be a new value. reused = _resource_tracker._pid in (None, pid) reused &= _resource_tracker._check_alive() conn.send(reused) @@ -6093,6 +6094,183 @@ def test_resource_tracker_blocked_signals(self): # restore sigmask to what it was before executing test signal.pthread_sigmask(signal.SIG_SETMASK, orig_sigmask) + @only_run_in_forkserver_testsuite("avoids redundant testing.") + def test_resource_tracker_fork_deadlock(self): + # gh-146313: ResourceTracker.__del__ used to deadlock if a forked + # child still held the pipe's write end open when the parent + # exited, because the parent would block in waitpid() waiting for + # the tracker to exit, but the tracker would never see EOF. + cmd = '''if 1: + import os, signal + from multiprocessing.resource_tracker import ensure_running + ensure_running() + if os.fork() == 0: + signal.pause() + os._exit(0) + # parent falls through and exits, triggering __del__ + ''' + proc = subprocess.Popen([sys.executable, '-c', cmd], + start_new_session=True) + try: + try: + proc.wait(timeout=support.SHORT_TIMEOUT) + except subprocess.TimeoutExpired: + self.fail( + "Parent process deadlocked in ResourceTracker.__del__" + ) + self.assertEqual(proc.returncode, 0) + finally: + try: + os.killpg(proc.pid, signal.SIGKILL) + except ProcessLookupError: + pass + proc.wait() + + @only_run_in_forkserver_testsuite("avoids redundant testing.") + def test_resource_tracker_mp_fork_reuse_and_prompt_reap(self): + # gh-146313 / gh-80849: A child started via multiprocessing.Process + # with the 'fork' start method should reuse the parent's resource + # tracker (the at-fork handler preserves the inherited pipe fd), + # *and* the parent should be able to reap the tracker promptly + # after joining the child, without hitting the waitpid timeout. + cmd = textwrap.dedent(''' + import multiprocessing as mp + from multiprocessing.resource_tracker import _resource_tracker + + def child(conn): + # Prove we can talk to the parent's tracker by registering + # and unregistering a dummy resource over the inherited fd. + # If the fd were closed, ensure_running would launch a new + # tracker and _pid would be non-None. + _resource_tracker.register("x", "dummy") + _resource_tracker.unregister("x", "dummy") + conn.send((_resource_tracker._fd is not None, + _resource_tracker._pid is None, + _resource_tracker._check_alive())) + + if __name__ == "__main__": + mp.set_start_method("fork") + _resource_tracker.ensure_running() + r, w = mp.Pipe(duplex=False) + p = mp.Process(target=child, args=(w,)) + p.start() + child_has_fd, child_pid_none, child_alive = r.recv() + p.join() + w.close(); r.close() + + # Now simulate __del__: the child has exited and released + # its fd copy, so the tracker should see EOF and exit + # promptly -- no timeout. + _resource_tracker._stop(wait_timeout=5.0) + print(child_has_fd, child_pid_none, child_alive, + _resource_tracker._waitpid_timed_out, + _resource_tracker._exitcode) + ''') + rc, out, err = script_helper.assert_python_ok('-c', cmd) + parts = out.decode().split() + self.assertEqual(parts, ['True', 'True', 'True', 'False', '0'], + f"unexpected: {parts!r} stderr={err!r}") + + @only_run_in_forkserver_testsuite("avoids redundant testing.") + def test_resource_tracker_raw_fork_prompt_reap(self): + # gh-146313: After a raw os.fork() the at-fork handler closes the + # child's inherited fd, so the parent can reap the tracker + # immediately -- even while the child is still alive -- rather + # than waiting out the 1s timeout. + cmd = textwrap.dedent(''' + import os, signal + from multiprocessing.resource_tracker import _resource_tracker + + _resource_tracker.ensure_running() + r, w = os.pipe() + pid = os.fork() + if pid == 0: + os.close(r) + # Report whether our fd was closed by the at-fork handler. + os.write(w, b"1" if _resource_tracker._fd is None else b"0") + os.close(w) + signal.pause() # stay alive so parent's reap is meaningful + os._exit(0) + os.close(w) + child_fd_closed = os.read(r, 1) == b"1" + os.close(r) + + # Child is still alive and paused. Because it closed its fd + # copy, our close below is the last one and the tracker exits. + _resource_tracker._stop(wait_timeout=5.0) + + os.kill(pid, signal.SIGKILL) + os.waitpid(pid, 0) + print(child_fd_closed, + _resource_tracker._waitpid_timed_out, + _resource_tracker._exitcode) + ''') + rc, out, err = script_helper.assert_python_ok('-c', cmd) + parts = out.decode().split() + self.assertEqual(parts, ['True', 'False', '0'], + f"unexpected: {parts!r} stderr={err!r}") + + @only_run_in_forkserver_testsuite("avoids redundant testing.") + def test_resource_tracker_lock_reinit_after_fork(self): + # gh-146313: If a parent thread held the tracker's lock at fork + # time, the child would inherit the held lock and deadlock on + # its next ensure_running(). The at-fork handler reinits it. + cmd = textwrap.dedent(''' + import os, threading + from multiprocessing.resource_tracker import _resource_tracker + + held = threading.Event() + release = threading.Event() + def hold(): + with _resource_tracker._lock: + held.set() + release.wait() + t = threading.Thread(target=hold) + t.start() + held.wait() + + pid = os.fork() + if pid == 0: + ok = _resource_tracker._lock.acquire(timeout=5.0) + os._exit(0 if ok else 1) + + release.set() + t.join() + _, status = os.waitpid(pid, 0) + print(os.waitstatus_to_exitcode(status)) + ''') + rc, out, err = script_helper.assert_python_ok( + '-W', 'ignore::DeprecationWarning', '-c', cmd) + self.assertEqual(out.strip(), b'0', + f"child failed to acquire lock: stderr={err!r}") + + @only_run_in_forkserver_testsuite("avoids redundant testing.") + def test_resource_tracker_safety_net_timeout(self): + # gh-146313: When an mp.Process(fork) child holds the preserved + # fd and the parent calls _stop() without joining (simulating + # abnormal shutdown), the safety-net timeout should fire rather + # than deadlocking. + cmd = textwrap.dedent(''' + import multiprocessing as mp + import signal + from multiprocessing.resource_tracker import _resource_tracker + + if __name__ == "__main__": + mp.set_start_method("fork") + _resource_tracker.ensure_running() + p = mp.Process(target=signal.pause) + p.start() + # Stop WITHOUT joining -- child still holds preserved fd + _resource_tracker._stop(wait_timeout=0.5) + print(_resource_tracker._waitpid_timed_out) + p.terminate() + p.join() + ''') + rc, out, err = script_helper.assert_python_ok('-c', cmd) + self.assertEqual(out.strip(), b'True', + f"safety-net timeout did not fire: stderr={err!r}") + + class TestSimpleQueue(unittest.TestCase): @classmethod diff --git a/Misc/NEWS.d/next/Library/2026-03-22-23-42-22.gh-issue-146313.RtDeAd.rst b/Misc/NEWS.d/next/Library/2026-03-22-23-42-22.gh-issue-146313.RtDeAd.rst new file mode 100644 index 00000000000000..1beea3694c422e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-22-23-42-22.gh-issue-146313.RtDeAd.rst @@ -0,0 +1,4 @@ +Fix a deadlock in :mod:`multiprocessing`'s resource tracker +where the parent process could hang indefinitely in :func:`os.waitpid` +during interpreter shutdown if a child created via :func:`os.fork` still +held the resource tracker's pipe open. From 9628a797da16a18e9dbacab40152bd827c053063 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:13:37 +0200 Subject: [PATCH 016/242] [3.13] gh-146450: Ensure Android gradle build uses custom cross-build dir (GH-148319) (#148471) Ensures that the testbed's Gradle configuration uses the cross-build environment variable, and that variable is passed to Gradle by the cross-build script. (cherry picked from commit b29afe62f7236f7161c2670dccc24368217a7fb1) Co-authored-by: Russell Keith-Magee Co-authored-by: Malcolm Smith --- Android/android.py | 56 +++++++++------------------- Android/testbed/app/build.gradle.kts | 2 +- 2 files changed, 18 insertions(+), 40 deletions(-) diff --git a/Android/android.py b/Android/android.py index 60d062aa66fce2..ecd5b3f1893fcb 100755 --- a/Android/android.py +++ b/Android/android.py @@ -393,17 +393,6 @@ def setup_testbed(): os.chmod(out_path, 0o755) -# run_testbed will build the app automatically, but it's useful to have this as -# a separate command to allow running the app outside of this script. -def build_testbed(context): - setup_sdk() - setup_testbed() - run( - [gradlew, "--console", "plain", "packageDebug", "packageDebugAndroidTest"], - cwd=TESTBED_DIR, - ) - - # Work around a bug involving sys.exit and TaskGroups # (https://github.com/python/cpython/issues/101515). def exit(*args): @@ -645,6 +634,10 @@ async def gradle_task(context): task_prefix = "connected" env["ANDROID_SERIAL"] = context.connected + # Ensure that CROSS_BUILD_DIR is in the Gradle environment, regardless + # of whether it was set by environment variable or `--cross-build-dir`. + env["CROSS_BUILD_DIR"] = CROSS_BUILD_DIR + if context.ci_mode: context.args[0:0] = [ # See _add_ci_python_opts in libregrtest/main.py. @@ -871,6 +864,18 @@ def parse_args(): def add_parser(*args, **kwargs): parser = subcommands.add_parser(*args, **kwargs) + parser.add_argument( + "--cross-build-dir", + action="store", + default=os.environ.get("CROSS_BUILD_DIR"), + dest="cross_build_dir", + type=Path, + help=( + "Path to the cross-build directory " + f"(default: {CROSS_BUILD_DIR}). Can also be set " + "with the CROSS_BUILD_DIR environment variable." + ), + ) parser.add_argument( "-v", "--verbose", action="count", default=0, help="Show verbose output. Use twice to be even more verbose.") @@ -883,7 +888,7 @@ def add_parser(*args, **kwargs): ) configure_build = add_parser( "configure-build", help="Run `configure` for the build Python") - make_build = add_parser( + add_parser( "make-build", help="Run `make` for the build Python") configure_host = add_parser( "configure-host", help="Run `configure` for Android") @@ -895,38 +900,12 @@ def add_parser(*args, **kwargs): help="Delete build directories for the selected target" ) - add_parser("build-testbed", help="Build the testbed app") test = add_parser("test", help="Run the testbed app") package = add_parser("package", help="Make a release package") ci = add_parser("ci", help="Run build, package and test") env = add_parser("env", help="Print environment variables") # Common arguments - # --cross-build-dir argument - for cmd in [ - clean, - configure_build, - make_build, - configure_host, - make_host, - build, - package, - test, - ci, - ]: - cmd.add_argument( - "--cross-build-dir", - action="store", - default=os.environ.get("CROSS_BUILD_DIR"), - dest="cross_build_dir", - type=Path, - help=( - "Path to the cross-build directory " - f"(default: {CROSS_BUILD_DIR}). Can also be set " - "with the CROSS_BUILD_DIR environment variable." - ), - ) - # --cache-dir option for cmd in [configure_host, build, ci]: cmd.add_argument( @@ -1031,7 +1010,6 @@ def main(): "make-host": make_host_python, "build": build_targets, "clean": clean_targets, - "build-testbed": build_testbed, "test": run_testbed, "package": package, "ci": ci, diff --git a/Android/testbed/app/build.gradle.kts b/Android/testbed/app/build.gradle.kts index b58edc04a929d9..bd8334b64bb0a8 100644 --- a/Android/testbed/app/build.gradle.kts +++ b/Android/testbed/app/build.gradle.kts @@ -8,7 +8,7 @@ plugins { val ANDROID_DIR = file("../..") val PYTHON_DIR = ANDROID_DIR.parentFile!! -val PYTHON_CROSS_DIR = file("$PYTHON_DIR/cross-build") +val PYTHON_CROSS_DIR = file(System.getenv("CROSS_BUILD_DIR") ?: "$PYTHON_DIR/cross-build") val inSourceTree = ( ANDROID_DIR.name == "Android" && file("$PYTHON_DIR/pyconfig.h.in").exists() ) From 74be9e2c7624d117d4d77f6bf67546a05b64b25c Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:13:53 +0200 Subject: [PATCH 017/242] [3.13] tests: use errno.EBADF instead of hardcoded number in _close_file() (GH-148345) (#148411) tests: use errno.EBADF instead of hardcoded number in _close_file() (GH-148345) test_interpreters: use errno.EBADF instead of hardcoded number in _close_file() Replace the hardcoded `9` check in `Lib/test/test_interpreters/utils.py` with `errno.EBADF`. Using `errno.EBADF` makes the helper portable across platforms with different errno numbering while preserving the intended behavior. (cherry picked from commit cef334fd4c4c24a542ce81ad940b1426b5a7cdbd) Co-authored-by: Artem Yarulin --- Lib/test/test_interpreters/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_interpreters/utils.py b/Lib/test/test_interpreters/utils.py index 980289f7ee7075..add156d0e36dab 100644 --- a/Lib/test/test_interpreters/utils.py +++ b/Lib/test/test_interpreters/utils.py @@ -1,5 +1,6 @@ from collections import namedtuple import contextlib +import errno import json import io import logging @@ -55,7 +56,7 @@ def _close_file(file): else: os.close(file) except OSError as exc: - if exc.errno != 9: + if exc.errno != errno.EBADF: raise # re-raise # It was closed already. From c3cf71c3366fe49acb776a639405c0eea6169c20 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:35:24 +0200 Subject: [PATCH 018/242] [3.13] gh-148395: Fix a possible UAF in `{LZMA,BZ2,_Zlib}Decompressor` (GH-148396) (#148479) gh-148395: Fix a possible UAF in `{LZMA,BZ2,_Zlib}Decompressor` (GH-148396) Fix dangling input pointer after `MemoryError` in _lzma/_bz2/_ZlibDecompressor.decompress (cherry picked from commit 8fc66aef6d7b3ae58f43f5c66f9366cc8cbbfcd2) Co-authored-by: Stan Ulbrych --- .../Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst | 5 +++++ Modules/_bz2module.c | 1 + Modules/_lzmamodule.c | 1 + Modules/zlibmodule.c | 1 + 4 files changed, 8 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst diff --git a/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst new file mode 100644 index 00000000000000..9502189ab199c1 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-04-10-16-28-21.gh-issue-148395.kfzm0G.rst @@ -0,0 +1,5 @@ +Fix a dangling input pointer in :class:`lzma.LZMADecompressor`, +:class:`bz2.BZ2Decompressor`, and internal :class:`!zlib._ZlibDecompressor` +when memory allocation fails with :exc:`MemoryError`, which could let a +subsequent :meth:`!decompress` call read or write through a stale pointer to +the already-released caller buffer. diff --git a/Modules/_bz2module.c b/Modules/_bz2module.c index 661847ad26702e..6c83ad3fa33d3d 100644 --- a/Modules/_bz2module.c +++ b/Modules/_bz2module.c @@ -589,6 +589,7 @@ decompress(BZ2Decompressor *d, char *data, size_t len, Py_ssize_t max_length) return result; error: + bzs->next_in = NULL; Py_XDECREF(result); return NULL; } diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c index 97f3a8f03da9a8..8d22ad318dabd1 100644 --- a/Modules/_lzmamodule.c +++ b/Modules/_lzmamodule.c @@ -1112,6 +1112,7 @@ decompress(Decompressor *d, uint8_t *data, size_t len, Py_ssize_t max_length) return result; error: + lzs->next_in = NULL; Py_XDECREF(result); return NULL; } diff --git a/Modules/zlibmodule.c b/Modules/zlibmodule.c index f59f9a76abd5ea..2b7def45cae476 100644 --- a/Modules/zlibmodule.c +++ b/Modules/zlibmodule.c @@ -1675,6 +1675,7 @@ decompress(ZlibDecompressor *self, uint8_t *data, return result; error: + self->zst.next_in = NULL; Py_XDECREF(result); return NULL; } From f35859bca609ac1ac9a3d1035990ea1cdf9c6302 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:34:05 +0200 Subject: [PATCH 019/242] [3.13] Fix "encodings" typo in argparse.FileType documentation (GH-148502) (#148514) Fix "encodings" typo in argparse.FileType documentation (GH-148502) (cherry picked from commit 8ecb6b8b0cff4105c4cca408409fb7a2fadc8b77) Co-authored-by: Gleb Popov --- Doc/library/argparse.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 84a242d29a059a..af39724170941d 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -1828,7 +1828,7 @@ FileType objects Namespace(infile=<_io.TextIOWrapper name='' encoding='UTF-8'>) .. versionchanged:: 3.4 - Added the *encodings* and *errors* parameters. + Added the *encoding* and *errors* parameters. Argument groups From 26105b03a62226be112e3362c5435afe66797da5 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 13 Apr 2026 20:52:47 +0200 Subject: [PATCH 020/242] [3.13] InternalDocs: Correct struct path for latin1 singletons in `string_interning.md` (GH-148358) (#148491) (cherry picked from commit 0274d8304e5eec23de100d827eb4da06ab7fd8aa) Co-authored-by: Stan Ulbrych --- InternalDocs/string_interning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InternalDocs/string_interning.md b/InternalDocs/string_interning.md index 358e2c070cd5fa..971e33a33c20c7 100644 --- a/InternalDocs/string_interning.md +++ b/InternalDocs/string_interning.md @@ -15,8 +15,8 @@ dynamic interning. The 256 possible one-character latin-1 strings, which can be retrieved with `_Py_LATIN1_CHR(c)`, are stored in statically allocated arrays, -`_PyRuntime.static_objects.strings.ascii` and -`_PyRuntime.static_objects.strings.latin1`. +`_PyRuntime.static_objects.singletons.strings.ascii` and +`_PyRuntime.static_objects.singletons.strings.latin1`. Longer singleton strings are marked in C source with `_Py_ID` (if the string is a valid C identifier fragment) or `_Py_STR` (if it needs a separate From d82c491b2ebb8fa952efa1dfddb2e873d867ccb2 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:30:52 +0200 Subject: [PATCH 021/242] [3.13] gh-146139: Disable `socketpair` authentication on WASI (GH-146140) (#148527) gh-146139: Disable `socketpair` authentication on WASI (GH-146140) Calling `connect(2)` on a non-blocking socket on WASI may leave the socket in a "connecting" but not yet "connected" state. In the former case, calling `getpeername(2)` on it will fail, leading to an unhandled exception in Python. (cherry picked from commit a5b76d53bb29afd864243f44ef22968f6385dfa0) Co-authored-by: Joel Dice Co-authored-by: Victor Stinner Co-authored-by: Brett Cannon --- Lib/socket.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index 35d87eff34deb1..55644bd4c17d5c 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -634,18 +634,22 @@ def _fallback_socketpair(family=AF_INET, type=SOCK_STREAM, proto=0): # Authenticating avoids using a connection from something else # able to connect to {host}:{port} instead of us. # We expect only AF_INET and AF_INET6 families. - try: - if ( - ssock.getsockname() != csock.getpeername() - or csock.getsockname() != ssock.getpeername() - ): - raise ConnectionError("Unexpected peer connection") - except: - # getsockname() and getpeername() can fail - # if either socket isn't connected. - ssock.close() - csock.close() - raise + # + # Note that we skip this on WASI because on that platorm the client socket + # may not have finished connecting by the time we've reached this point (gh-146139). + if sys.platform != "wasi": + try: + if ( + ssock.getsockname() != csock.getpeername() + or csock.getsockname() != ssock.getpeername() + ): + raise ConnectionError("Unexpected peer connection") + except: + # getsockname() and getpeername() can fail + # if either socket isn't connected. + ssock.close() + csock.close() + raise return (ssock, csock) From a69b14bc01a23aa4024660e6f1e52fc78308fafa Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Tue, 14 Apr 2026 01:05:38 +0200 Subject: [PATCH 022/242] [3.13] gh-148370: prevent quadratic behavior in `configparser.ParsingError.combine` (GH-148452) (#148533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gh-148370: prevent quadratic behavior in `configparser.ParsingError.combine` (GH-148452) (cherry picked from commit 2662db0c45aa16232136628457a53681b6683c25) Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Lib/configparser.py | 9 ++++++--- Lib/test/test_configparser.py | 13 +++++++++++++ .../2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst | 2 ++ 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index 05b86acb919bfd..71f842b3902e8c 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -316,12 +316,15 @@ def __init__(self, source, *args): def append(self, lineno, line): self.errors.append((lineno, line)) - self.message += '\n\t[line %2d]: %s' % (lineno, repr(line)) + self.message += f'\n\t[line {lineno:2d}]: {line!r}' def combine(self, others): + messages = [self.message] for other in others: - for error in other.errors: - self.append(*error) + for lineno, line in other.errors: + self.errors.append((lineno, line)) + messages.append(f'\n\t[line {lineno:2d}]: {line!r}') + self.message = "".join(messages) return self @staticmethod diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index d4fdd92bdc6b6a..bef3c11b941242 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -1729,6 +1729,19 @@ def test_error(self): self.assertEqual(e1.message, e2.message) self.assertEqual(repr(e1), repr(e2)) + def test_combine_error_linear_complexity(self): + # Ensure that ParsingError.combine() has linear complexity. + # See https://github.com/python/cpython/issues/148370. + n = 50000 + s = '[*]\n' + (err_line := '=\n') * n + p = configparser.ConfigParser(strict=False) + with self.assertRaises(configparser.ParsingError) as cm: + p.read_string(s) + errlines = cm.exception.message.splitlines() + self.assertEqual(len(errlines), n + 1) + self.assertTrue(errlines[0].startswith("Source contains parsing errors: ")) + self.assertEqual(errlines[42], f"\t[line {43:2d}]: {err_line!r}") + def test_nosectionerror(self): import pickle e1 = configparser.NoSectionError('section') diff --git a/Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst b/Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst new file mode 100644 index 00000000000000..3bb662350796f6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-12-16-40-11.gh-issue-148370.0Li2EK.rst @@ -0,0 +1,2 @@ +:mod:`configparser`: prevent quadratic behavior when a :exc:`~configparser.ParsingError` +is raised after a parser fails to parse multiple lines. Patch by Bénédikt Tran. From ff88ee4b3a404310d60a716aeb9c153e47dec07c Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Tue, 14 Apr 2026 05:54:43 +0200 Subject: [PATCH 023/242] [3.13] gh-148508: Add resilience to SSL preauth tests on iOS (GH-148536) (#148540) Adds handling for a test case seen in the iOS SSL tests where an SSL connection fails to handshake correctly. (cherry picked from commit c40e8b016a90820e4d799922903b90a505ffaf55) Co-authored-by: Russell Keith-Magee --- Lib/test/test_ssl.py | 17 +++++++++++------ ...26-04-14-09-04-35.gh-issue-148508.-GiXml.rst | 2 ++ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-14-09-04-35.gh-issue-148508.-GiXml.rst diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index fbbe918af75e69..453919d2a4aab9 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -5268,15 +5268,20 @@ def non_linux_skip_if_other_okay_error(self, err): return # Expect the full test setup to always work on Linux. if (isinstance(err, ConnectionResetError) or (isinstance(err, OSError) and err.errno == errno.EINVAL) or - re.search('wrong.version.number', str(getattr(err, "reason", "")), re.I)): + re.search('wrong.version.number', str(getattr(err, "reason", "")), re.I) or + re.search('record.layer.failure', str(getattr(err, "reason", "")), re.I) + ): # On Windows the TCP RST leads to a ConnectionResetError # (ECONNRESET) which Linux doesn't appear to surface to userspace. # If wrap_socket() winds up on the "if connected:" path and doing - # the actual wrapping... we get an SSLError from OpenSSL. Typically - # WRONG_VERSION_NUMBER. While appropriate, neither is the scenario - # we're specifically trying to test. The way this test is written - # is known to work on Linux. We'll skip it anywhere else that it - # does not present as doing so. + # the actual wrapping... we get an SSLError from OpenSSL. This is + # typically WRONG_VERSION_NUMBER. The same happens on iOS, but + # RECORD_LAYER_FAILURE is the error. + # + # While appropriate, neither is the scenario we're specifically + # trying to test. The way this test is written is known to work on + # Linux. We'll skip it anywhere else that it does not present as + # doing so. try: self.skipTest(f"Could not recreate conditions on {sys.platform}:" f" {err=}") diff --git a/Misc/NEWS.d/next/Library/2026-04-14-09-04-35.gh-issue-148508.-GiXml.rst b/Misc/NEWS.d/next/Library/2026-04-14-09-04-35.gh-issue-148508.-GiXml.rst new file mode 100644 index 00000000000000..7995dec397f7b7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-14-09-04-35.gh-issue-148508.-GiXml.rst @@ -0,0 +1,2 @@ +An intermittent timing error when running SSL tests on iOS has been +resolved. From 63c9aa60a6f9eec69954f5c21fbdda687fb9e8d9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 14 Apr 2026 12:08:32 +0200 Subject: [PATCH 024/242] [3.13] gh-148487: Fix issues in `test_add_python_opts` (#148507) (#148546) gh-148487: Fix issues in `test_add_python_opts` (#148507) (cherry picked from commit 44f1b987ed1908185d8021fd31703b2dd4d97e82) Co-authored-by: Stan Ulbrych --- Lib/test/test_regrtest.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_regrtest.py b/Lib/test/test_regrtest.py index 8e1f85090753cb..9206425d997ee5 100644 --- a/Lib/test/test_regrtest.py +++ b/Lib/test/test_regrtest.py @@ -2221,11 +2221,8 @@ def test_unload_tests(self): self.check_executed_tests(output, tests, stats=3) def check_add_python_opts(self, option): - # --fast-ci and --slow-ci add "-u -W default -bb -E" options to Python - try: - import _testinternalcapi - except ImportError: - raise unittest.SkipTest("requires _testinternalcapi") + # --fast-ci and --slow-ci add "-u -W error -bb -E" options to Python + code = textwrap.dedent(r""" import sys import unittest @@ -2239,25 +2236,27 @@ def check_add_python_opts(self, option): use_environment = (support.is_emscripten or support.is_wasi) class WorkerTests(unittest.TestCase): - @unittest.skipUnless(get_config is None, 'need get_config()') + @unittest.skipIf(get_config is None, 'need get_config()') def test_config(self): - config = get_config()['config'] + config = get_config() # -u option self.assertEqual(config['buffered_stdio'], 0) - # -W default option - self.assertTrue(config['warnoptions'], ['default']) + # -W error option + self.assertEqual(config['warnoptions'], + ['error', 'error::BytesWarning']) # -bb option - self.assertTrue(config['bytes_warning'], 2) + self.assertEqual(config['bytes_warning'], 2) # -E option - self.assertTrue(config['use_environment'], use_environment) + self.assertEqual(config['use_environment'], use_environment) def test_python_opts(self): # -u option self.assertTrue(sys.__stdout__.write_through) self.assertTrue(sys.__stderr__.write_through) - # -W default option - self.assertTrue(sys.warnoptions, ['default']) + # -W error option + self.assertEqual(sys.warnoptions, + ['error', 'error::BytesWarning']) # -bb option self.assertEqual(sys.flags.bytes_warning, 2) From 5a4143a392a64dbecec8f5718ef43b87cd15915e Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:21:55 +0200 Subject: [PATCH 025/242] [3.13] gh-148192: Fix Generator._make_boundary behavior with CRLF line endings. (GH-148193) (#148549) The Generator._make_boundary regex did not match on boundary phrases correctly when using CRLF line endings due to re.MULTILINE not considering \r\n as a line ending. (cherry picked from commit 4af46b4ab5af49d8df034320a9a70fcbb062f7cf) Co-authored-by: Henry Jones <44321887+henryivesjones@users.noreply.github.com> --- Lib/email/generator.py | 2 +- Lib/test/test_email/test_generator.py | 37 +++++++++++++++++++ ...-04-07-14-13-40.gh-issue-148192.34AUYQ.rst | 3 ++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-04-07-14-13-40.gh-issue-148192.34AUYQ.rst diff --git a/Lib/email/generator.py b/Lib/email/generator.py index a03eb1fbbc9288..444d7b0fae480c 100644 --- a/Lib/email/generator.py +++ b/Lib/email/generator.py @@ -392,7 +392,7 @@ def _make_boundary(cls, text=None): b = boundary counter = 0 while True: - cre = cls._compile_re('^--' + re.escape(b) + '(--)?$', re.MULTILINE) + cre = cls._compile_re('^--' + re.escape(b) + '(--)?\r?$', re.MULTILINE) if not cre.search(text): break b = boundary + '.' + str(counter) diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py index c2d7d09d591e86..3c9a86f3e8cf29 100644 --- a/Lib/test/test_email/test_generator.py +++ b/Lib/test/test_email/test_generator.py @@ -1,13 +1,20 @@ import io import textwrap import unittest +import random +import sys from email import message_from_string, message_from_bytes from email.message import EmailMessage +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from email.generator import Generator, BytesGenerator +import email.generator from email.headerregistry import Address from email import policy import email.errors from test.test_email import TestEmailBase, parameterize +import test.support + @parameterize @@ -288,6 +295,36 @@ def test_keep_long_encoded_newlines(self): g.flatten(msg) self.assertEqual(s.getvalue(), self.typ(expected)) + def _test_boundary_detection(self, linesep): + # Generate a boundary token in the same way as _make_boundary + token = random.randrange(sys.maxsize) + + def _patch_random_randrange(*args, **kwargs): + return token + + with test.support.swap_attr( + random, "randrange", _patch_random_randrange + ): + boundary = self.genclass._make_boundary(text=None) + boundary_in_part = ( + "this goes before the boundary\n--" + + boundary + + "\nthis goes after\n" + ) + msg = MIMEMultipart() + msg.attach(MIMEText(boundary_in_part)) + self.genclass(self.ioclass()).flatten(msg, linesep=linesep) + # Generator checks the message content for the string it is about + # to use as a boundary ('token' in this test) and when it finds it + # in our attachment appends .0 to make the boundary it uses unique. + self.assertEqual(msg.get_boundary(), boundary + ".0") + + def test_lf_boundary_detection(self): + self._test_boundary_detection("\n") + + def test_crlf_boundary_detection(self): + self._test_boundary_detection("\r\n") + class TestGenerator(TestGeneratorBase, TestEmailBase): diff --git a/Misc/NEWS.d/next/Library/2026-04-07-14-13-40.gh-issue-148192.34AUYQ.rst b/Misc/NEWS.d/next/Library/2026-04-07-14-13-40.gh-issue-148192.34AUYQ.rst new file mode 100644 index 00000000000000..87a568b50c17c3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-04-07-14-13-40.gh-issue-148192.34AUYQ.rst @@ -0,0 +1,3 @@ +``email.generator.Generator._make_boundary`` could fail to detect a duplicate +boundary string if linesep was not \n. It now correctly detects boundary +strings when linesep is \r\n as well. From ee34f95dbceefb1e0bc2510dd9778bb83e9e1e8d Mon Sep 17 00:00:00 2001 From: Savannah Ostrowski Date: Tue, 14 Apr 2026 10:53:11 -0700 Subject: [PATCH 026/242] [3.13] gh-72406: Document argument ordering in argparse help output (GH-148534) (#148567) gh-72406: Document argument ordering in argparse help output (#148534) (cherry picked from commit 4286227308ee8dafc867062df4cad73af2a84696) Co-authored-by: Santi Hernandez --- Doc/library/argparse.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index af39724170941d..89b9faf9f60d2a 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -1888,6 +1888,9 @@ Argument groups Note that any arguments not in your user-defined groups will end up back in the usual "positional arguments" and "optional arguments" sections. + Within each argument group, arguments are displayed in help output in the + order in which they are added. + .. versionchanged:: 3.11 Calling :meth:`add_argument_group` on an argument group is deprecated. This feature was never supported and does not always work correctly. From cb4b94c74d9a9776a153c2b841a354d614b289e4 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:45:32 +0200 Subject: [PATCH 027/242] [3.13] gh-148186: Improve `assertCountEqual` description in docs. (GH-148463) (#148586) gh-148186: Improve `assertCountEqual` description in docs. (GH-148463) (cherry picked from commit 94d42bf5c2b95417d6187a57fef94570ba017e33) Co-authored-by: Kliment Lamonov --- Doc/library/unittest.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 22316e25c5aa18..1a02066ff997f4 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -1211,9 +1211,9 @@ Test cases | :meth:`assertNotRegex(s, r) | ``not r.search(s)`` | 3.2 | | ` | | | +---------------------------------------+--------------------------------+--------------+ - | :meth:`assertCountEqual(a, b) | *a* and *b* have the same | 3.2 | - | ` | elements in the same number, | | - | | regardless of their order. | | + | :meth:`assertCountEqual(a, b) | *a* contains the same elements | 3.2 | + | ` | as *b*, regardless of their | | + | | order. | | +---------------------------------------+--------------------------------+--------------+ From ba2f30eb83fc05c60cf99802dcce6ac1d9a46cf0 Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:06:33 +0200 Subject: [PATCH 028/242] [3.13] gh-137335: remove a mktemp use in multiprocessing.connection to avoid security scanner noise (GH-148578) (#148584) gh-137335: remove a mktemp use in multiprocessing.connection to avoid security scanner noise (GH-148578) remove a mktemp use to avoid security scanner noise (cherry picked from commit fd81246bd55e4fab1976a7cca3e5d42582dbdac0) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> --- Lib/multiprocessing/connection.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py index efb9ea95ab4696..2a0151dab80234 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py @@ -75,7 +75,11 @@ def arbitrary_address(family): if family == 'AF_INET': return ('localhost', 0) elif family == 'AF_UNIX': - return tempfile.mktemp(prefix='sock-', dir=util.get_temp_dir()) + # NOTE: util.get_temp_dir() is a 0o700 per-process directory. A + # mktemp-style ToC vs ToU concern is not important; bind() surfaces + # the extremely unlikely collision as EADDRINUSE. + return os.path.join(util.get_temp_dir(), + f'sock-{os.urandom(6).hex()}') elif family == 'AF_PIPE': return (r'\\.\pipe\pyc-%d-%d-%s' % (os.getpid(), next(_mmap_counter), os.urandom(8).hex())) From e76aa128fea8691525b482a211bef6b1c514019d Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Wed, 15 Apr 2026 02:06:49 +0200 Subject: [PATCH 029/242] [3.13] tiny edit, fix a couple of minor typos in enum and sqlite3 docs (GH-148580) (#148582) tiny edit, fix a couple of minor typos in enum and sqlite3 docs (GH-148580) pair of minor doc typo fixes (cherry picked from commit 236aa0a4e2106f98757e12a9f656f98d91f03c13) Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> --- Doc/library/enum.rst | 2 +- Doc/library/sqlite3.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/enum.rst b/Doc/library/enum.rst index 6ddd7faf5a22a0..b5b44458d63a4f 100644 --- a/Doc/library/enum.rst +++ b/Doc/library/enum.rst @@ -245,7 +245,7 @@ Data types .. method:: EnumType.__len__(cls) - Returns the number of member in *cls*:: + Returns the number of members in *cls*:: >>> len(Color) 3 diff --git a/Doc/library/sqlite3.rst b/Doc/library/sqlite3.rst index 88393c8a0ddde8..c29ce6b056c16f 100644 --- a/Doc/library/sqlite3.rst +++ b/Doc/library/sqlite3.rst @@ -55,7 +55,7 @@ This document includes four main sections: PEP written by Marc-André Lemburg. -.. We use the following practises for SQL code: +.. We use the following practices for SQL code: - UPPERCASE for keywords - snake_case for schema - single quotes for string literals From a5969e8f0fda37aaf0e2f844fdcfca9d822a70b1 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 15 Apr 2026 12:11:10 +0200 Subject: [PATCH 030/242] [3.13] gh-146333: Fix quadratic regex backtracking in configparser option parsing (GH-146399) (GH-148559) Use negative lookahead in option regex to prevent backtracking, and to avoid changing logic outside the regexes (since people could use the regex directly). (cherry picked from commit 7e0a0be4097f9d29d66fe23f5af86f18a34ed7dd) Co-authored-by: Joshua Swanson <22283299+joshuaswanson@users.noreply.github.com> --- Lib/configparser.py | 8 ++++++-- Lib/test/test_configparser.py | 20 +++++++++++++++++++ ...3-25-00-51-03.gh-issue-146333.LqdL__bn.rst | 3 +++ 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-03-25-00-51-03.gh-issue-146333.LqdL__bn.rst diff --git a/Lib/configparser.py b/Lib/configparser.py index 71f842b3902e8c..3968ac45eed56b 100644 --- a/Lib/configparser.py +++ b/Lib/configparser.py @@ -600,7 +600,9 @@ class RawConfigParser(MutableMapping): \] # ] """ _OPT_TMPL = r""" - (?P