From 766614f88af0433c2c14f5c4ed11c92d0fb04e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 22 Aug 2025 13:45:01 +0200 Subject: [PATCH 1/5] gh-136134: smtplib: fix CRAM-MD5 on FIPS-only environments (#136623) --- Lib/smtplib.py | 23 +++++++-- Lib/test/test_smtplib.py | 47 +++++++++++++++++-- ...-07-13-13-31-22.gh-issue-136134.mh6VjS.rst | 5 ++ 3 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 84d6d858e7dec1..b71fee8777e866 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -177,6 +177,15 @@ def _quote_periods(bindata): def _fix_eols(data): return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data) + +try: + hmac.digest(b'', b'', 'md5') +except ValueError: + _have_cram_md5_support = False +else: + _have_cram_md5_support = True + + try: import ssl except ImportError: @@ -665,8 +674,11 @@ def auth_cram_md5(self, challenge=None): # CRAM-MD5 does not support initial-response. if challenge is None: return None - return self.user + " " + hmac.HMAC( - self.password.encode('ascii'), challenge, 'md5').hexdigest() + if not _have_cram_md5_support: + raise SMTPException("CRAM-MD5 is not supported") + password = self.password.encode('ascii') + authcode = hmac.HMAC(password, challenge, 'md5') + return f"{self.user} {authcode.hexdigest()}" def auth_plain(self, challenge=None): """ Authobject to use with PLAIN authentication. Requires self.user and @@ -718,9 +730,10 @@ def login(self, user, password, *, initial_response_ok=True): advertised_authlist = self.esmtp_features["auth"].split() # Authentication methods we can handle in our preferred order: - preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN'] - - # We try the supported authentications in our preferred order, if + if _have_cram_md5_support: + preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN'] + else: + preferred_auths = ['PLAIN', 'LOGIN'] # the server supports them. authlist = [auth for auth in preferred_auths if auth in advertised_authlist] diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 4c9fc14bd43f54..b8aac8c20202a2 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -17,6 +17,7 @@ import threading import unittest +import unittest.mock as mock from test import support, mock_socket from test.support import hashlib_helper from test.support import socket_helper @@ -926,11 +927,14 @@ def _auth_cram_md5(self, arg=None): except ValueError as e: self.push('535 Splitting response {!r} into user and password ' 'failed: {}'.format(logpass, e)) - return False - valid_hashed_pass = hmac.HMAC( - sim_auth[1].encode('ascii'), - self._decode_base64(sim_cram_md5_challenge).encode('ascii'), - 'md5').hexdigest() + return + pwd = sim_auth[1].encode('ascii') + msg = self._decode_base64(sim_cram_md5_challenge).encode('ascii') + try: + valid_hashed_pass = hmac.HMAC(pwd, msg, 'md5').hexdigest() + except ValueError: + self.push('504 CRAM-MD5 is not supported') + return self._authenticated(user, hashed_pass == valid_hashed_pass) # end AUTH related stuff. @@ -1181,6 +1185,39 @@ def testAUTH_CRAM_MD5(self): self.assertEqual(resp, (235, b'Authentication Succeeded')) smtp.close() + @hashlib_helper.block_algorithm('md5') + @mock.patch("smtplib._have_cram_md5_support", False) + def testAUTH_CRAM_MD5_blocked(self): + # CRAM-MD5 is the only "known" method by the server, + # but it is not supported by the client. In particular, + # no challenge will ever be sent. + self.serv.add_feature("AUTH CRAM-MD5") + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', + timeout=support.LOOPBACK_TIMEOUT) + self.addCleanup(smtp.close) + msg = re.escape("No suitable authentication method found.") + with self.assertRaisesRegex(smtplib.SMTPException, msg): + smtp.login(sim_auth[0], sim_auth[1]) + + @hashlib_helper.block_algorithm('md5') + @mock.patch("smtplib._have_cram_md5_support", False) + def testAUTH_CRAM_MD5_blocked_and_fallback(self): + # Test that PLAIN is tried after CRAM-MD5 failed + self.serv.add_feature("AUTH CRAM-MD5 PLAIN") + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', + timeout=support.LOOPBACK_TIMEOUT) + self.addCleanup(smtp.close) + with ( + mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5, + mock.patch.object( + smtp, "auth_plain", wraps=smtp.auth_plain + ) as smtp_auth_plain + ): + resp = smtp.login(sim_auth[0], sim_auth[1]) + smtp_auth_plain.assert_called_once() + smtp_auth_cram_md5.assert_not_called() + self.assertEqual(resp, (235, b'Authentication Succeeded')) + @hashlib_helper.requires_hashdigest('md5', openssl=True) def testAUTH_multiple(self): # Test that multiple authentication methods are tried. diff --git a/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst b/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst new file mode 100644 index 00000000000000..f0290be9ba1e05 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst @@ -0,0 +1,5 @@ +:meth:`!SMTP.auth_cram_md5` now raises an :exc:`~smtplib.SMTPException` +instead of a :exc:`ValueError` if Python has been built without MD5 support. +In particular, :class:`~smtplib.SMTP` clients will not attempt to use this +method even if the remote server is assumed to support it. Patch by Bénédikt +Tran. From 8952b826a7c9a5f53d0e3dbba11c7163de72d2c5 Mon Sep 17 00:00:00 2001 From: Ken Jin Date: Fri, 22 Aug 2025 20:15:40 +0800 Subject: [PATCH 2/5] gh-138042: Fix homebrew for tail-calling macOS CI (GH-138043) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- .github/workflows/tail-call.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index e32cbf0aaa3c3e..57c92e193a9aec 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -114,8 +114,8 @@ jobs: find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete brew install llvm@${{ matrix.llvm }} export SDKROOT="$(xcrun --show-sdk-path)" - export PATH="/usr/local/opt/llvm/bin:$PATH" - export PATH="/opt/homebrew/opt/llvm/bin:$PATH" + export PATH="/usr/local/opt/llvm@${{ matrix.llvm }}/bin:$PATH" + export PATH="/opt/homebrew/opt/llvm@${{ matrix.llvm }}/bin:$PATH" CC=clang-20 ./configure --with-tail-call-interp make all --jobs 4 ./python.exe -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3 From 5b0f00e616738ededcf4b178dee8c30f5005e7ae Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 22 Aug 2025 16:22:14 +0300 Subject: [PATCH 3/5] gh-135386: Skip readonly tests for the root user (GH-138058) --- Lib/test/test_dbm_sqlite3.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_dbm_sqlite3.py b/Lib/test/test_dbm_sqlite3.py index 15826f51c54180..f367a98865d4aa 100644 --- a/Lib/test/test_dbm_sqlite3.py +++ b/Lib/test/test_dbm_sqlite3.py @@ -16,6 +16,11 @@ from dbm.sqlite3 import _normalize_uri +root_in_posix = False +if hasattr(os, 'geteuid'): + root_in_posix = (os.geteuid() == 0) + + class _SQLiteDbmTests(unittest.TestCase): def setUp(self): @@ -92,6 +97,7 @@ def test_readonly_iter(self): self.assertEqual([k for k in self.db], [b"key1", b"key2"]) +@unittest.skipIf(root_in_posix, "test is meanless with root privilege") class ReadOnlyFilesystem(unittest.TestCase): def setUp(self): From b9bcd02e9fe976e0a6a8e8a0b336d598b1d73b13 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 22 Aug 2025 19:10:43 +0530 Subject: [PATCH 4/5] gh-137384: fix crash when accessing warnings state late in runtime shutdown (#138027) --- Lib/test/test_gc.py | 13 +++++++++++++ .../2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst | 1 + Python/pystate.c | 5 ++++- 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 7c9adf3049a131..4328909053465e 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1580,6 +1580,19 @@ def test_ast_fini(self): """) assert_python_ok("-c", code) + def test_warnings_fini(self): + # See https://github.com/python/cpython/issues/137384 + code = textwrap.dedent(''' + import asyncio + from contextvars import ContextVar + + context_loop = ContextVar("context_loop", default=None) + loop = asyncio.new_event_loop() + context_loop.set(loop) + ''') + + assert_python_ok("-c", code) + def setUpModule(): global enabled, debug diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst new file mode 100644 index 00000000000000..583d751a460eb6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst @@ -0,0 +1 @@ +Fix a crash when using the :mod:`warnings` module in a finalizer at shutdown. Patch by Kumar Aditya. diff --git a/Python/pystate.c b/Python/pystate.c index 9091057f6f62cf..2465d8667472dc 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -805,7 +805,6 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) _Py_ClearExecutorDeletionList(interp); #endif _PyAST_Fini(interp); - _PyWarnings_Fini(interp); _PyAtExit_Fini(interp); // All Python types must be destroyed before the last GC collection. Python @@ -815,6 +814,10 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) /* Last garbage collection on this interpreter */ _PyGC_CollectNoFail(tstate); _PyGC_Fini(interp); + + // Finalize warnings after last gc so that any finalizers can + // access warnings state + _PyWarnings_Fini(interp); struct _PyExecutorObject *cold = interp->cold_executor; if (cold != NULL) { interp->cold_executor = NULL; From f278afcabf1a1c4162a0bfd4912662517d5d04a2 Mon Sep 17 00:00:00 2001 From: Krishna Chaitanya <141550576+XChaitanyaX@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:58:33 +0530 Subject: [PATCH 5/5] gh-91116: Add hyperlink from `sys.settrace` to frame objects (GH-138062) --- Doc/library/sys.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/sys.rst b/Doc/library/sys.rst index 771e0f2709a4aa..30fd4db1fd1851 100644 --- a/Doc/library/sys.rst +++ b/Doc/library/sys.rst @@ -1764,7 +1764,7 @@ always available. Unless explicitly noted otherwise, all variables are read-only :func:`settrace` for each thread being debugged or use :func:`threading.settrace`. Trace functions should have three arguments: *frame*, *event*, and - *arg*. *frame* is the current stack frame. *event* is a string: ``'call'``, + *arg*. *frame* is the :ref:`current stack frame `. *event* is a string: ``'call'``, ``'line'``, ``'return'``, ``'exception'`` or ``'opcode'``. *arg* depends on the event type.