From 72fa7f84e961e660b3c322c620b8da64cc2e5d97 Mon Sep 17 00:00:00 2001 From: James Page Date: Wed, 26 Nov 2025 17:07:14 +0000 Subject: [PATCH 1/6] python-3.11: enable FIPS compatible multiprocessing Backport a slimmed down version of stronger hash algorithm support for multiprocessing; this change is backwards compatible in non-FIPS environments with older client/server versions. Clients in FIPS environments presenting MD5 based HMAC digests will be rejected. --- python-3.11.yaml | 6 +- python-3.11/gh-61460.patch | 473 +++++++++++++++++++++++++++++++++++++ 2 files changed, 477 insertions(+), 2 deletions(-) create mode 100644 python-3.11/gh-61460.patch diff --git a/python-3.11.yaml b/python-3.11.yaml index 2dbce63dcec..efed39aaca7 100644 --- a/python-3.11.yaml +++ b/python-3.11.yaml @@ -1,7 +1,7 @@ package: name: python-3.11 version: "3.11.14" - epoch: 2 + epoch: 3 description: "the Python programming language" copyright: - license: PSF-2.0 @@ -59,7 +59,9 @@ pipeline: - uses: patch with: - patches: gh-127301.patch + patches: | + gh-127301.patch + gh-61460.patch - uses: autoconf/configure with: diff --git a/python-3.11/gh-61460.patch b/python-3.11/gh-61460.patch new file mode 100644 index 00000000000..1a301a242ed --- /dev/null +++ b/python-3.11/gh-61460.patch @@ -0,0 +1,473 @@ +From de3306572edffe45b9457f1a4afb95c472f787f2 Mon Sep 17 00:00:00 2001 +From: James Page +Date: Wed, 26 Nov 2025 16:52:52 +0000 +Subject: [PATCH] [3.11] gh-61460: Use HMAC-SHA256 in multiprocessing + authentication +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Backport stronger HMAC authentication to multiprocessing for Python 3.11. +Uses SHA256 by default instead of MD5, with full backward compatibility +for Python 3.10 and earlier. + +Implementation details: +- Uses length-based protocol detection (32-byte challenge = SHA256, + 20-byte challenge = MD5) to avoid complex prefix parsing +- Modern servers send 32-byte challenges and accept both SHA256 and + MD5 responses (for legacy clients) +- Modern clients detect server protocol by challenge length and respond + appropriately +- Replaced unsafe assert with proper AuthenticationError exception +- Uses hmac.compare_digest() for constant-time comparison + +Backward compatibility verified with 6 new test cases covering: +- Modern client/server (SHA256) +- Legacy client with modern server (MD5 fallback) +- Modern client with legacy server (MD5 detection) +- Protocol error handling + +FIPS mode behavior: When MD5 is unavailable, connections to/from +Python ≤3.10 will fail as expected, since FIPS prohibits MD5. + +This is a simplified backport of commit 3ed57e4995d9 from Python 3.12+, +using only SHA256 and MD5 instead of the full multi-algorithm protocol. +--- + Lib/multiprocessing/connection.py | 155 +++++++++++++++- + Lib/test/_test_multiprocessing.py | 167 +++++++++++++++++- + .../2025-01-26-12-00-00.gh-61460.abc123.rst | 5 + + 3 files changed, 312 insertions(+), 15 deletions(-) + create mode 100644 Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst + +diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py +index 59c61d2aa29..ecc28c4c84e 100644 +--- a/Lib/multiprocessing/connection.py ++++ b/Lib/multiprocessing/connection.py +@@ -741,39 +741,178 @@ def PipeClient(address): + # + + MESSAGE_LENGTH = 20 ++MESSAGE_LENGTH_SHA256 = 32 ++_MD5_DIGEST_LEN = 16 ++_SHA256_DIGEST_LEN = 32 + + CHALLENGE = b'#CHALLENGE#' + WELCOME = b'#WELCOME#' + FAILURE = b'#FAILURE#' + ++# multiprocessing.connection Authentication Protocol (Simplified for 3.11) ++# ++# This is a simpler backport of the enhanced HMAC authentication from Python 3.12+. ++# It uses length-based protocol detection to maintain backward compatibility with ++# Python 3.10 and earlier while upgrading to SHA256 by default. ++# ++# Protocol Detection: ++# - Legacy (Python ≤3.10): 20-byte challenge → 16-byte MD5 HMAC response ++# - Modern (Python ≥3.11): 32-byte challenge → 32-byte SHA256 HMAC response ++# ++# Compatibility Matrix: ++# - Modern server + Modern client: SHA256 authentication (secure) ++# - Modern server + Legacy client: MD5 authentication (backward compatible) ++# - Legacy server + Modern client: MD5 authentication (backward compatible) ++# - Legacy server + Legacy client: MD5 authentication (works as before) ++# ++# FIPS Mode Behavior: ++# When MD5 is unavailable (e.g., FIPS mode), modern servers will reject legacy ++# clients that can only respond with MD5. This is expected behavior as FIPS ++# mode prohibits weak cryptographic algorithms. ++# ++# See the original implementation in Python 3.12+ (commit 3ed57e4995d9) for the ++# full protocol with multi-algorithm support and {digest} prefixes. ++ ++def _verify_response(connection, expected_digest, algorithm_name): ++ """Helper to verify HMAC response using constant-time comparison. ++ ++ Sends WELCOME if response matches, FAILURE + raises AuthenticationError if not. ++ """ ++ import hmac ++ if hmac.compare_digest(connection, expected_digest): ++ return True ++ return False ++ + def deliver_challenge(connection, authkey): ++ """Initiate HMAC authentication challenge (server side). ++ ++ Sends a random challenge to the client and verifies their HMAC response. ++ Uses SHA256 by default, but accepts MD5 responses from legacy clients. ++ ++ Protocol: ++ 1. Server generates random bytes (32 for SHA256, 20 for MD5-only mode) ++ 2. Server sends: CHALLENGE prefix + random bytes ++ 3. Server receives HMAC response from client ++ 4. Server detects algorithm by response length: ++ - 32 bytes: Verify as SHA256 ++ - 16 bytes: Verify as MD5 (legacy client) ++ 5. Server sends WELCOME on success, FAILURE on authentication error ++ ++ Args: ++ connection: Connection object with send_bytes/recv_bytes methods ++ authkey: Shared secret key (bytes) ++ ++ Raises: ++ ValueError: If authkey is not bytes ++ AuthenticationError: If client response verification fails ++ """ + import hmac + if not isinstance(authkey, bytes): + raise ValueError( + "Authkey must be bytes, not {0!s}".format(type(authkey))) +- message = os.urandom(MESSAGE_LENGTH) ++ ++ # Try SHA256 first, fall back to MD5 if unavailable ++ try: ++ # Use 32-byte challenge to signal modern SHA256 protocol ++ message = os.urandom(MESSAGE_LENGTH_SHA256) ++ expected_digest = hmac.new(authkey, message, 'sha256').digest() ++ algorithm_name = 'sha256' ++ except ValueError: ++ # SHA256 not available (unusual), use legacy MD5 protocol ++ message = os.urandom(MESSAGE_LENGTH) ++ expected_digest = hmac.new(authkey, message, 'md5').digest() ++ algorithm_name = 'md5' ++ + connection.send_bytes(CHALLENGE + message) +- digest = hmac.new(authkey, message, 'md5').digest() + response = connection.recv_bytes(256) # reject large message +- if response == digest: +- connection.send_bytes(WELCOME) ++ ++ # Verify response based on length ++ if len(response) == _SHA256_DIGEST_LEN and algorithm_name == 'sha256': ++ # Modern client using SHA256 ++ if _verify_response(response, expected_digest, 'sha256'): ++ connection.send_bytes(WELCOME) ++ return ++ connection.send_bytes(FAILURE) ++ raise AuthenticationError('SHA256 HMAC verification failed') ++ elif len(response) == _MD5_DIGEST_LEN: ++ # Legacy client using MD5, or we're in MD5-only mode ++ if algorithm_name == 'sha256': ++ # Modern server, legacy client - recompute with MD5 ++ try: ++ expected_digest = hmac.new(authkey, message, 'md5').digest() ++ except ValueError: ++ connection.send_bytes(FAILURE) ++ raise AuthenticationError( ++ 'client sent MD5 response but MD5 not available (FIPS mode?)') ++ if _verify_response(response, expected_digest, 'md5'): ++ connection.send_bytes(WELCOME) ++ return ++ connection.send_bytes(FAILURE) ++ raise AuthenticationError('MD5 HMAC verification failed') + else: + connection.send_bytes(FAILURE) +- raise AuthenticationError('digest received was wrong') ++ raise AuthenticationError( ++ 'unexpected response length {0} (expected {1} or {2})'.format( ++ len(response), _SHA256_DIGEST_LEN, _MD5_DIGEST_LEN)) + + def answer_challenge(connection, authkey): ++ """Respond to HMAC authentication challenge (client side). ++ ++ Receives a challenge from the server and sends back an HMAC response. ++ Detects the server's protocol version by challenge length. ++ ++ Protocol: ++ 1. Client receives: CHALLENGE prefix + random bytes ++ 2. Client detects algorithm by challenge length: ++ - 20 bytes: Legacy server, compute MD5 HMAC ++ - 32 bytes: Modern server, compute SHA256 HMAC ++ 3. Client sends HMAC response ++ 4. Client receives WELCOME on success or FAILURE on error ++ ++ Args: ++ connection: Connection object with send_bytes/recv_bytes methods ++ authkey: Shared secret key (bytes) ++ ++ Raises: ++ ValueError: If authkey is not bytes ++ AuthenticationError: If challenge format invalid or server rejects response ++ """ + import hmac + if not isinstance(authkey, bytes): + raise ValueError( + "Authkey must be bytes, not {0!s}".format(type(authkey))) + message = connection.recv_bytes(256) # reject large message +- assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message ++ ++ if not message.startswith(CHALLENGE): ++ raise AuthenticationError( ++ 'message missing challenge prefix (expected {0!r})'.format(CHALLENGE)) ++ + message = message[len(CHALLENGE):] +- digest = hmac.new(authkey, message, 'md5').digest() ++ ++ # Detect protocol based on challenge length ++ if len(message) == MESSAGE_LENGTH: ++ # Legacy 20-byte challenge: server expects MD5 ++ try: ++ digest = hmac.new(authkey, message, 'md5').digest() ++ except ValueError: ++ raise AuthenticationError( ++ 'legacy server requires MD5 but MD5 not available (FIPS mode?)') ++ elif len(message) == MESSAGE_LENGTH_SHA256: ++ # Modern 32-byte challenge: server expects SHA256 ++ try: ++ digest = hmac.new(authkey, message, 'sha256').digest() ++ except ValueError: ++ raise AuthenticationError( ++ 'modern server requires SHA256 but SHA256 not available') ++ else: ++ raise AuthenticationError( ++ 'unexpected challenge length {0} (expected {1} or {2})'.format( ++ len(message), MESSAGE_LENGTH, MESSAGE_LENGTH_SHA256)) ++ + connection.send_bytes(digest) + response = connection.recv_bytes(256) # reject large message + if response != WELCOME: +- raise AuthenticationError('digest sent was rejected') ++ raise AuthenticationError('server rejected authentication') + + # + # Support for using xmlrpclib for serialization +diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py +index e096401c4cf..40cec1057bb 100644 +--- a/Lib/test/_test_multiprocessing.py ++++ b/Lib/test/_test_multiprocessing.py +@@ -3120,7 +3120,7 @@ def test_remote(self): + del queue + + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class _TestManagerRestart(BaseTestCase): + + @classmethod +@@ -3633,7 +3633,7 @@ def test_dont_merge(self): + # + + @unittest.skipUnless(HAS_REDUCTION, "test needs multiprocessing.reduction") +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class _TestPicklingConnections(BaseTestCase): + + ALLOWED_TYPES = ('processes',) +@@ -3936,7 +3936,7 @@ def test_copy(self): + + + @unittest.skipUnless(HAS_SHMEM, "requires multiprocessing.shared_memory") +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class _TestSharedMemory(BaseTestCase): + + ALLOWED_TYPES = ('processes',) +@@ -4777,7 +4777,7 @@ def test_invalid_handles(self): + + + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class OtherTest(unittest.TestCase): + # TODO: add more tests for deliver/answer challenge. + def test_deliver_challenge_auth_failure(self): +@@ -4807,6 +4807,159 @@ def send_bytes(self, data): + multiprocessing.connection.answer_challenge, + _FakeConnection(), b'abc') + ++ def test_deliver_challenge_sha256_response(self): ++ """Test modern SHA256 authentication (32-byte challenge, 32-byte response).""" ++ import hmac ++ authkey = b'test_secret_key_123' ++ challenge_message = None ++ ++ class _FakeConnection(object): ++ def __init__(self): ++ self.sent = [] ++ def send_bytes(self, data): ++ self.sent.append(data) ++ def recv_bytes(self, size): ++ # Extract challenge and compute correct SHA256 response ++ nonlocal challenge_message ++ if challenge_message is None: ++ # First call gets the challenge ++ challenge_data = self.sent[0] ++ assert challenge_data.startswith( ++ multiprocessing.connection.CHALLENGE) ++ challenge_message = challenge_data[ ++ len(multiprocessing.connection.CHALLENGE):] ++ # Verify it's 32 bytes (SHA256 protocol) ++ assert len(challenge_message) == 32, ( ++ f"Expected 32-byte challenge, got {len(challenge_message)}") ++ return hmac.new(authkey, challenge_message, 'sha256').digest() ++ ++ conn = _FakeConnection() ++ multiprocessing.connection.deliver_challenge(conn, authkey) ++ # Should have sent WELCOME ++ self.assertIn(multiprocessing.connection.WELCOME, conn.sent) ++ ++ def test_deliver_challenge_md5_response_legacy_client(self): ++ """Test backward compatibility: modern server accepts MD5 from legacy client.""" ++ import hmac ++ authkey = b'test_secret_key_456' ++ challenge_message = None ++ ++ class _FakeConnection(object): ++ def __init__(self): ++ self.sent = [] ++ def send_bytes(self, data): ++ self.sent.append(data) ++ def recv_bytes(self, size): ++ # Legacy client receives 32-byte challenge but responds with MD5 ++ nonlocal challenge_message ++ if challenge_message is None: ++ challenge_data = self.sent[0] ++ assert challenge_data.startswith( ++ multiprocessing.connection.CHALLENGE) ++ challenge_message = challenge_data[ ++ len(multiprocessing.connection.CHALLENGE):] ++ # Legacy client computes MD5 HMAC (16 bytes) ++ return hmac.new(authkey, challenge_message, 'md5').digest() ++ ++ conn = _FakeConnection() ++ multiprocessing.connection.deliver_challenge(conn, authkey) ++ # Should have sent WELCOME (backward compatible) ++ self.assertIn(multiprocessing.connection.WELCOME, conn.sent) ++ ++ def test_answer_challenge_sha256_from_modern_server(self): ++ """Test client responds with SHA256 to 32-byte challenge.""" ++ import hmac ++ authkey = b'test_secret_key_789' ++ challenge_message = os.urandom(32) # Modern 32-byte challenge ++ ++ class _FakeConnection(object): ++ def __init__(self): ++ self.sent = [] ++ def recv_bytes(self, size): ++ if not self.sent: ++ # First call: return challenge ++ return (multiprocessing.connection.CHALLENGE + ++ challenge_message) ++ else: ++ # Second call: return WELCOME ++ return multiprocessing.connection.WELCOME ++ def send_bytes(self, data): ++ self.sent.append(data) ++ ++ conn = _FakeConnection() ++ multiprocessing.connection.answer_challenge(conn, authkey) ++ # Should have sent SHA256 digest (32 bytes) ++ self.assertEqual(len(conn.sent), 1) ++ response = conn.sent[0] ++ self.assertEqual(len(response), 32) ++ # Verify it's correct SHA256 ++ expected = hmac.new(authkey, challenge_message, 'sha256').digest() ++ self.assertEqual(response, expected) ++ ++ def test_answer_challenge_md5_from_legacy_server(self): ++ """Test client responds with MD5 to 20-byte challenge (legacy server).""" ++ import hmac ++ authkey = b'test_secret_key_012' ++ challenge_message = os.urandom(20) # Legacy 20-byte challenge ++ ++ class _FakeConnection(object): ++ def __init__(self): ++ self.sent = [] ++ def recv_bytes(self, size): ++ if not self.sent: ++ # First call: return legacy challenge ++ return (multiprocessing.connection.CHALLENGE + ++ challenge_message) ++ else: ++ # Second call: return WELCOME ++ return multiprocessing.connection.WELCOME ++ def send_bytes(self, data): ++ self.sent.append(data) ++ ++ conn = _FakeConnection() ++ multiprocessing.connection.answer_challenge(conn, authkey) ++ # Should have sent MD5 digest (16 bytes) ++ self.assertEqual(len(conn.sent), 1) ++ response = conn.sent[0] ++ self.assertEqual(len(response), 16) ++ # Verify it's correct MD5 ++ expected = hmac.new(authkey, challenge_message, 'md5').digest() ++ self.assertEqual(response, expected) ++ ++ def test_bad_response_length_rejected(self): ++ """Test that responses with unexpected lengths are rejected.""" ++ authkey = b'test_key' ++ ++ class _FakeConnection(object): ++ def __init__(self): ++ self.sent = [] ++ def send_bytes(self, data): ++ self.sent.append(data) ++ def recv_bytes(self, size): ++ # Return wrong length (not 16 or 32) ++ return b'x' * 24 ++ ++ conn = _FakeConnection() ++ with self.assertRaises(multiprocessing.AuthenticationError) as cm: ++ multiprocessing.connection.deliver_challenge(conn, authkey) ++ self.assertIn('unexpected response length', str(cm.exception)) ++ ++ def test_bad_challenge_length_rejected(self): ++ """Test that challenges with unexpected lengths are rejected.""" ++ authkey = b'test_key' ++ bad_challenge = os.urandom(25) # Not 20 or 32 ++ ++ class _FakeConnection(object): ++ def recv_bytes(self, size): ++ return multiprocessing.connection.CHALLENGE + bad_challenge ++ def send_bytes(self, data): ++ pass ++ ++ conn = _FakeConnection() ++ with self.assertRaises(multiprocessing.AuthenticationError) as cm: ++ multiprocessing.connection.answer_challenge(conn, authkey) ++ self.assertIn('unexpected challenge length', str(cm.exception)) ++ + # + # Test Manager.start()/Pool.__init__() initializer feature - see issue 5585 + # +@@ -4814,7 +4967,7 @@ def send_bytes(self, data): + def initializer(ns): + ns.test += 1 + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class TestInitializers(unittest.TestCase): + def setUp(self): + self.mgr = multiprocessing.Manager() +@@ -5729,7 +5882,7 @@ def is_alive(self): + any(process.is_alive() for process in forked_processes)) + + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class TestSyncManagerTypes(unittest.TestCase): + """Test all the types which can be shared between a parent and a + child process by using a manager which acts as an intermediary +@@ -6174,7 +6327,7 @@ def install_tests_in_module_dict(remote_globs, start_method, + class Temp(base, Mixin, unittest.TestCase): + pass + if type_ == 'manager': +- Temp = hashlib_helper.requires_hashdigest('md5')(Temp) ++ Temp = hashlib_helper.requires_hashdigest('sha256')(Temp) + Temp.__name__ = Temp.__qualname__ = newname + Temp.__module__ = __module__ + remote_globs[newname] = Temp +diff --git a/Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst b/Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst +new file mode 100644 +index 00000000000..117301d86af +--- /dev/null ++++ b/Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst +@@ -0,0 +1,5 @@ ++:mod:`multiprocessing` now uses HMAC-SHA256 instead of HMAC-MD5 for ++inter-process connection authentication. The implementation maintains ++backward compatibility with Python 3.10 and earlier using length-based ++protocol detection. Note: In FIPS mode where MD5 is unavailable, this ++version cannot authenticate with Python ≤3.10 clients/servers. +-- +2.51.2 + From 22c3f352fb62f83c2eeea064ac1759a3e8c15d82 Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 27 Nov 2025 09:21:07 +0000 Subject: [PATCH 2/6] Prefer patch from Python contributor used in DataDog's fork. --- python-3.11/gh-61460.patch | 661 +++++++++++++++++-------------------- 1 file changed, 309 insertions(+), 352 deletions(-) diff --git a/python-3.11/gh-61460.patch b/python-3.11/gh-61460.patch index 1a301a242ed..a4c911c4627 100644 --- a/python-3.11/gh-61460.patch +++ b/python-3.11/gh-61460.patch @@ -1,240 +1,309 @@ -From de3306572edffe45b9457f1a4afb95c472f787f2 Mon Sep 17 00:00:00 2001 -From: James Page -Date: Wed, 26 Nov 2025 16:52:52 +0000 -Subject: [PATCH] [3.11] gh-61460: Use HMAC-SHA256 in multiprocessing - authentication -MIME-Version: 1.0 -Content-Type: text/plain; charset=UTF-8 -Content-Transfer-Encoding: 8bit +From 9e0b30d517b0c01cfb2a29d761a69eb7310f8cc3 Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Sun, 21 May 2023 01:33:09 +0200 +Subject: [PATCH] [3.11] gh-61460: Stronger HMAC in multiprocessing (GH-20380) -Backport stronger HMAC authentication to multiprocessing for Python 3.11. -Uses SHA256 by default instead of MD5, with full backward compatibility -for Python 3.10 and earlier. +bpo-17258: `multiprocessing` now supports stronger HMAC algorithms for inter-process connection authentication rather than only HMAC-MD5. -Implementation details: -- Uses length-based protocol detection (32-byte challenge = SHA256, - 20-byte challenge = MD5) to avoid complex prefix parsing -- Modern servers send 32-byte challenges and accept both SHA256 and - MD5 responses (for legacy clients) -- Modern clients detect server protocol by challenge length and respond - appropriately -- Replaced unsafe assert with proper AuthenticationError exception -- Uses hmac.compare_digest() for constant-time comparison +Signed-off-by: Christian Heimes -Backward compatibility verified with 6 new test cases covering: -- Modern client/server (SHA256) -- Legacy client with modern server (MD5 fallback) -- Modern client with legacy server (MD5 detection) -- Protocol error handling +gpshead: I Reworked to be more robust while keeping the idea. -FIPS mode behavior: When MD5 is unavailable, connections to/from -Python ≤3.10 will fail as expected, since FIPS prohibits MD5. +The protocol modification idea remains, but we now take advantage of the +message length as an indicator of legacy vs modern protocol version. No +more regular expression usage. We now default to HMAC-SHA256, but do so +in a way that will be compatible when communicating with older clients +or older servers. No protocol transition period is needed. -This is a simplified backport of commit 3ed57e4995d9 from Python 3.12+, -using only SHA256 and MD5 instead of the full multi-algorithm protocol. +More integration tests to verify these claims remain true are required. I'm +unaware of anyone depending on multiprocessing connections between +different Python versions. + +--------- + +(cherry picked from commit 3ed57e4995d9f8583083483f397ddc3131720953) + +Co-authored-by: Christian Heimes +Signed-off-by: Christian Heimes +Co-authored-by: Gregory P. Smith [Google] --- - Lib/multiprocessing/connection.py | 155 +++++++++++++++- - Lib/test/_test_multiprocessing.py | 167 +++++++++++++++++- - .../2025-01-26-12-00-00.gh-61460.abc123.rst | 5 + - 3 files changed, 312 insertions(+), 15 deletions(-) - create mode 100644 Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst + Lib/multiprocessing/connection.py | 222 ++++++++++++++++-- + Lib/test/_test_multiprocessing.py | 57 ++++- + .../2020-05-25-12-42-36.bpo-17258.lf2554.rst | 2 + + 3 files changed, 254 insertions(+), 27 deletions(-) + create mode 100644 Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py -index 59c61d2aa29..ecc28c4c84e 100644 +index 59c61d2aa292cb..d0582e3cd5406a 100644 --- a/Lib/multiprocessing/connection.py +++ b/Lib/multiprocessing/connection.py -@@ -741,39 +741,178 @@ def PipeClient(address): +@@ -740,39 +740,227 @@ def PipeClient(address): + # Authentication stuff # - MESSAGE_LENGTH = 20 -+MESSAGE_LENGTH_SHA256 = 32 -+_MD5_DIGEST_LEN = 16 -+_SHA256_DIGEST_LEN = 32 +-MESSAGE_LENGTH = 20 ++MESSAGE_LENGTH = 40 # MUST be > 20 - CHALLENGE = b'#CHALLENGE#' - WELCOME = b'#WELCOME#' - FAILURE = b'#FAILURE#' +-CHALLENGE = b'#CHALLENGE#' +-WELCOME = b'#WELCOME#' +-FAILURE = b'#FAILURE#' ++_CHALLENGE = b'#CHALLENGE#' ++_WELCOME = b'#WELCOME#' ++_FAILURE = b'#FAILURE#' -+# multiprocessing.connection Authentication Protocol (Simplified for 3.11) +-def deliver_challenge(connection, authkey): ++# multiprocessing.connection Authentication Handshake Protocol Description ++# (as documented for reference after reading the existing code) ++# ============================================================================= +# -+# This is a simpler backport of the enhanced HMAC authentication from Python 3.12+. -+# It uses length-based protocol detection to maintain backward compatibility with -+# Python 3.10 and earlier while upgrading to SHA256 by default. ++# On Windows: native pipes with "overlapped IO" are used to send the bytes, ++# instead of the length prefix SIZE scheme described below. (ie: the OS deals ++# with message sizes for us) +# -+# Protocol Detection: -+# - Legacy (Python ≤3.10): 20-byte challenge → 16-byte MD5 HMAC response -+# - Modern (Python ≥3.11): 32-byte challenge → 32-byte SHA256 HMAC response ++# Protocol error behaviors: +# -+# Compatibility Matrix: -+# - Modern server + Modern client: SHA256 authentication (secure) -+# - Modern server + Legacy client: MD5 authentication (backward compatible) -+# - Legacy server + Modern client: MD5 authentication (backward compatible) -+# - Legacy server + Legacy client: MD5 authentication (works as before) ++# On POSIX, any failure to receive the length prefix into SIZE, for SIZE greater ++# than the requested maxsize to receive, or receiving fewer than SIZE bytes ++# results in the connection being closed and auth to fail. +# -+# FIPS Mode Behavior: -+# When MD5 is unavailable (e.g., FIPS mode), modern servers will reject legacy -+# clients that can only respond with MD5. This is expected behavior as FIPS -+# mode prohibits weak cryptographic algorithms. ++# On Windows, receiving too few bytes is never a low level _recv_bytes read ++# error, receiving too many will trigger an error only if receive maxsize ++# value was larger than 128 OR the if the data arrived in smaller pieces. +# -+# See the original implementation in Python 3.12+ (commit 3ed57e4995d9) for the -+# full protocol with multi-algorithm support and {digest} prefixes. ++# Serving side Client side ++# ------------------------------ --------------------------------------- ++# 0. Open a connection on the pipe. ++# 1. Accept connection. ++# 2. Random 20+ bytes -> MESSAGE ++# Modern servers always send ++# more than 20 bytes and include ++# a {digest} prefix on it with ++# their preferred HMAC digest. ++# Legacy ones send ==20 bytes. ++# 3. send 4 byte length (net order) ++# prefix followed by: ++# b'#CHALLENGE#' + MESSAGE ++# 4. Receive 4 bytes, parse as network byte ++# order integer. If it is -1, receive an ++# additional 8 bytes, parse that as network ++# byte order. The result is the length of ++# the data that follows -> SIZE. ++# 5. Receive min(SIZE, 256) bytes -> M1 ++# 6. Assert that M1 starts with: ++# b'#CHALLENGE#' ++# 7. Strip that prefix from M1 into -> M2 ++# 7.1. Parse M2: if it is exactly 20 bytes in ++# length this indicates a legacy server ++# supporting only HMAC-MD5. Otherwise the ++# 7.2. preferred digest is looked up from an ++# expected "{digest}" prefix on M2. No prefix ++# or unsupported digest? <- AuthenticationError ++# 7.3. Put divined algorithm name in -> D_NAME ++# 8. Compute HMAC-D_NAME of AUTHKEY, M2 -> C_DIGEST ++# 9. Send 4 byte length prefix (net order) ++# followed by C_DIGEST bytes. ++# 10. Receive 4 or 4+8 byte length ++# prefix (#4 dance) -> SIZE. ++# 11. Receive min(SIZE, 256) -> C_D. ++# 11.1. Parse C_D: legacy servers ++# accept it as is, "md5" -> D_NAME ++# 11.2. modern servers check the length ++# of C_D, IF it is 16 bytes? ++# 11.2.1. "md5" -> D_NAME ++# and skip to step 12. ++# 11.3. longer? expect and parse a "{digest}" ++# prefix into -> D_NAME. ++# Strip the prefix and store remaining ++# bytes in -> C_D. ++# 11.4. Don't like D_NAME? <- AuthenticationError ++# 12. Compute HMAC-D_NAME of AUTHKEY, ++# MESSAGE into -> M_DIGEST. ++# 13. Compare M_DIGEST == C_D: ++# 14a: Match? Send length prefix & ++# b'#WELCOME#' ++# <- RETURN ++# 14b: Mismatch? Send len prefix & ++# b'#FAILURE#' ++# <- CLOSE & AuthenticationError ++# 15. Receive 4 or 4+8 byte length prefix (net ++# order) again as in #4 into -> SIZE. ++# 16. Receive min(SIZE, 256) bytes -> M3. ++# 17. Compare M3 == b'#WELCOME#': ++# 17a. Match? <- RETURN ++# 17b. Mismatch? <- CLOSE & AuthenticationError ++# ++# If this RETURNed, the connection remains open: it has been authenticated. ++# ++# Length prefixes are used consistently. Even on the legacy protocol, this ++# was good fortune and allowed us to evolve the protocol by using the length ++# of the opening challenge or length of the returned digest as a signal as ++# to which protocol the other end supports. ++ ++_ALLOWED_DIGESTS = frozenset( ++ {b'md5', b'sha256', b'sha384', b'sha3_256', b'sha3_384'}) ++_MAX_DIGEST_LEN = max(len(_) for _ in _ALLOWED_DIGESTS) ++ ++# Old hmac-md5 only server versions from Python <=3.11 sent a message of this ++# length. It happens to not match the length of any supported digest so we can ++# use a message of this length to indicate that we should work in backwards ++# compatible md5-only mode without a {digest_name} prefix on our response. ++_MD5ONLY_MESSAGE_LENGTH = 20 ++_MD5_DIGEST_LEN = 16 ++_LEGACY_LENGTHS = (_MD5ONLY_MESSAGE_LENGTH, _MD5_DIGEST_LEN) + -+def _verify_response(connection, expected_digest, algorithm_name): -+ """Helper to verify HMAC response using constant-time comparison. + -+ Sends WELCOME if response matches, FAILURE + raises AuthenticationError if not. -+ """ -+ import hmac -+ if hmac.compare_digest(connection, expected_digest): -+ return True -+ return False ++def _get_digest_name_and_payload(message: bytes) -> (str, bytes): ++ """Returns a digest name and the payload for a response hash. + - def deliver_challenge(connection, authkey): -+ """Initiate HMAC authentication challenge (server side). ++ If a legacy protocol is detected based on the message length ++ or contents the digest name returned will be empty to indicate ++ legacy mode where MD5 and no digest prefix should be sent. ++ """ ++ # modern message format: b"{digest}payload" longer than 20 bytes ++ # legacy message format: 16 or 20 byte b"payload" ++ if len(message) in _LEGACY_LENGTHS: ++ # Either this was a legacy server challenge, or we're processing ++ # a reply from a legacy client that sent an unprefixed 16-byte ++ # HMAC-MD5 response. All messages using the modern protocol will ++ # be longer than either of these lengths. ++ return '', message ++ if (message.startswith(b'{') and ++ (curly := message.find(b'}', 1, _MAX_DIGEST_LEN+2)) > 0): ++ digest = message[1:curly] ++ if digest in _ALLOWED_DIGESTS: ++ payload = message[curly+1:] ++ return digest.decode('ascii'), payload ++ raise AuthenticationError( ++ 'unsupported message length, missing digest prefix, ' ++ f'or unsupported digest: {message=}') ++ ++ ++def _create_response(authkey, message): ++ """Create a MAC based on authkey and message ++ ++ The MAC algorithm defaults to HMAC-MD5, unless MD5 is not available or ++ the message has a '{digest_name}' prefix. For legacy HMAC-MD5, the response ++ is the raw MAC, otherwise the response is prefixed with '{digest_name}', ++ e.g. b'{sha256}abcdefg...' ++ ++ Note: The MAC protects the entire message including the digest_name prefix. ++ """ + import hmac ++ digest_name = _get_digest_name_and_payload(message)[0] ++ # The MAC protects the entire message: digest header and payload. ++ if not digest_name: ++ # Legacy server without a {digest} prefix on message. ++ # Generate a legacy non-prefixed HMAC-MD5 reply. ++ try: ++ return hmac.new(authkey, message, 'md5').digest() ++ except ValueError: ++ # HMAC-MD5 is not available (FIPS mode?), fall back to ++ # HMAC-SHA2-256 modern protocol. The legacy server probably ++ # doesn't support it and will reject us anyways. :shrug: ++ digest_name = 'sha256' ++ # Modern protocol, indicate the digest used in the reply. ++ response = hmac.new(authkey, message, digest_name).digest() ++ return b'{%s}%s' % (digest_name.encode('ascii'), response) + -+ Sends a random challenge to the client and verifies their HMAC response. -+ Uses SHA256 by default, but accepts MD5 responses from legacy clients. + -+ Protocol: -+ 1. Server generates random bytes (32 for SHA256, 20 for MD5-only mode) -+ 2. Server sends: CHALLENGE prefix + random bytes -+ 3. Server receives HMAC response from client -+ 4. Server detects algorithm by response length: -+ - 32 bytes: Verify as SHA256 -+ - 16 bytes: Verify as MD5 (legacy client) -+ 5. Server sends WELCOME on success, FAILURE on authentication error ++def _verify_challenge(authkey, message, response): ++ """Verify MAC challenge + -+ Args: -+ connection: Connection object with send_bytes/recv_bytes methods -+ authkey: Shared secret key (bytes) ++ If our message did not include a digest_name prefix, the client is allowed ++ to select a stronger digest_name from _ALLOWED_DIGESTS. + -+ Raises: -+ ValueError: If authkey is not bytes -+ AuthenticationError: If client response verification fails ++ In case our message is prefixed, a client cannot downgrade to a weaker ++ algorithm, because the MAC is calculated over the entire message ++ including the '{digest_name}' prefix. + """ - import hmac - if not isinstance(authkey, bytes): - raise ValueError( - "Authkey must be bytes, not {0!s}".format(type(authkey))) -- message = os.urandom(MESSAGE_LENGTH) -+ -+ # Try SHA256 first, fall back to MD5 if unavailable ++ import hmac ++ response_digest, response_mac = _get_digest_name_and_payload(response) ++ response_digest = response_digest or 'md5' + try: -+ # Use 32-byte challenge to signal modern SHA256 protocol -+ message = os.urandom(MESSAGE_LENGTH_SHA256) -+ expected_digest = hmac.new(authkey, message, 'sha256').digest() -+ algorithm_name = 'sha256' ++ expected = hmac.new(authkey, message, response_digest).digest() + except ValueError: -+ # SHA256 not available (unusual), use legacy MD5 protocol -+ message = os.urandom(MESSAGE_LENGTH) -+ expected_digest = hmac.new(authkey, message, 'md5').digest() -+ algorithm_name = 'md5' ++ raise AuthenticationError(f'{response_digest=} unsupported') ++ if len(expected) != len(response_mac): ++ raise AuthenticationError( ++ f'expected {response_digest!r} of length {len(expected)} ' ++ f'got {len(response_mac)}') ++ if not hmac.compare_digest(expected, response_mac): ++ raise AuthenticationError('digest received was wrong') ++ + - connection.send_bytes(CHALLENGE + message) ++def deliver_challenge(connection, authkey: bytes, digest_name='sha256'): + if not isinstance(authkey, bytes): + raise ValueError( + "Authkey must be bytes, not {0!s}".format(type(authkey))) ++ assert MESSAGE_LENGTH > _MD5ONLY_MESSAGE_LENGTH, "protocol constraint" + message = os.urandom(MESSAGE_LENGTH) +- connection.send_bytes(CHALLENGE + message) - digest = hmac.new(authkey, message, 'md5').digest() ++ message = b'{%s}%s' % (digest_name.encode('ascii'), message) ++ # Even when sending a challenge to a legacy client that does not support ++ # digest prefixes, they'll take the entire thing as a challenge and ++ # respond to it with a raw HMAC-MD5. ++ connection.send_bytes(_CHALLENGE + message) response = connection.recv_bytes(256) # reject large message - if response == digest: - connection.send_bytes(WELCOME) -+ -+ # Verify response based on length -+ if len(response) == _SHA256_DIGEST_LEN and algorithm_name == 'sha256': -+ # Modern client using SHA256 -+ if _verify_response(response, expected_digest, 'sha256'): -+ connection.send_bytes(WELCOME) -+ return -+ connection.send_bytes(FAILURE) -+ raise AuthenticationError('SHA256 HMAC verification failed') -+ elif len(response) == _MD5_DIGEST_LEN: -+ # Legacy client using MD5, or we're in MD5-only mode -+ if algorithm_name == 'sha256': -+ # Modern server, legacy client - recompute with MD5 -+ try: -+ expected_digest = hmac.new(authkey, message, 'md5').digest() -+ except ValueError: -+ connection.send_bytes(FAILURE) -+ raise AuthenticationError( -+ 'client sent MD5 response but MD5 not available (FIPS mode?)') -+ if _verify_response(response, expected_digest, 'md5'): -+ connection.send_bytes(WELCOME) -+ return -+ connection.send_bytes(FAILURE) -+ raise AuthenticationError('MD5 HMAC verification failed') ++ try: ++ _verify_challenge(authkey, message, response) ++ except AuthenticationError: ++ connection.send_bytes(_FAILURE) ++ raise else: - connection.send_bytes(FAILURE) +- connection.send_bytes(FAILURE) - raise AuthenticationError('digest received was wrong') -+ raise AuthenticationError( -+ 'unexpected response length {0} (expected {1} or {2})'.format( -+ len(response), _SHA256_DIGEST_LEN, _MD5_DIGEST_LEN)) ++ connection.send_bytes(_WELCOME) - def answer_challenge(connection, authkey): -+ """Respond to HMAC authentication challenge (client side). +-def answer_challenge(connection, authkey): +- import hmac + -+ Receives a challenge from the server and sends back an HMAC response. -+ Detects the server's protocol version by challenge length. -+ -+ Protocol: -+ 1. Client receives: CHALLENGE prefix + random bytes -+ 2. Client detects algorithm by challenge length: -+ - 20 bytes: Legacy server, compute MD5 HMAC -+ - 32 bytes: Modern server, compute SHA256 HMAC -+ 3. Client sends HMAC response -+ 4. Client receives WELCOME on success or FAILURE on error -+ -+ Args: -+ connection: Connection object with send_bytes/recv_bytes methods -+ authkey: Shared secret key (bytes) -+ -+ Raises: -+ ValueError: If authkey is not bytes -+ AuthenticationError: If challenge format invalid or server rejects response -+ """ - import hmac ++def answer_challenge(connection, authkey: bytes): if not isinstance(authkey, bytes): raise ValueError( "Authkey must be bytes, not {0!s}".format(type(authkey))) message = connection.recv_bytes(256) # reject large message - assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message -+ -+ if not message.startswith(CHALLENGE): -+ raise AuthenticationError( -+ 'message missing challenge prefix (expected {0!r})'.format(CHALLENGE)) -+ - message = message[len(CHALLENGE):] +- message = message[len(CHALLENGE):] - digest = hmac.new(authkey, message, 'md5').digest() -+ -+ # Detect protocol based on challenge length -+ if len(message) == MESSAGE_LENGTH: -+ # Legacy 20-byte challenge: server expects MD5 -+ try: -+ digest = hmac.new(authkey, message, 'md5').digest() -+ except ValueError: -+ raise AuthenticationError( -+ 'legacy server requires MD5 but MD5 not available (FIPS mode?)') -+ elif len(message) == MESSAGE_LENGTH_SHA256: -+ # Modern 32-byte challenge: server expects SHA256 -+ try: -+ digest = hmac.new(authkey, message, 'sha256').digest() -+ except ValueError: -+ raise AuthenticationError( -+ 'modern server requires SHA256 but SHA256 not available') -+ else: ++ if not message.startswith(_CHALLENGE): + raise AuthenticationError( -+ 'unexpected challenge length {0} (expected {1} or {2})'.format( -+ len(message), MESSAGE_LENGTH, MESSAGE_LENGTH_SHA256)) -+ ++ f'Protocol error, expected challenge: {message=}') ++ message = message[len(_CHALLENGE):] ++ if len(message) < _MD5ONLY_MESSAGE_LENGTH: ++ raise AuthenticationError('challenge too short: {len(message)} bytes') ++ digest = _create_response(authkey, message) connection.send_bytes(digest) response = connection.recv_bytes(256) # reject large message - if response != WELCOME: -- raise AuthenticationError('digest sent was rejected') -+ raise AuthenticationError('server rejected authentication') +- if response != WELCOME: ++ if response != _WELCOME: + raise AuthenticationError('digest sent was rejected') # - # Support for using xmlrpclib for serialization diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py -index e096401c4cf..40cec1057bb 100644 +index e096401c4cf316..0d7556b0735543 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py -@@ -3120,7 +3120,7 @@ def test_remote(self): +@@ -50,6 +50,7 @@ + import multiprocessing.managers + import multiprocessing.pool + import multiprocessing.queues ++from multiprocessing.connection import wait, AuthenticationError + + from multiprocessing import util + +@@ -136,8 +137,6 @@ def _resource_unlink(name, rtype): + + WIN32 = (sys.platform == "win32") + +-from multiprocessing.connection import wait +- + def wait_for_handle(handle, timeout): + if timeout is not None and timeout < 0.0: + timeout = None +@@ -3120,7 +3119,7 @@ def test_remote(self): del queue @@ -243,7 +312,7 @@ index e096401c4cf..40cec1057bb 100644 class _TestManagerRestart(BaseTestCase): @classmethod -@@ -3633,7 +3633,7 @@ def test_dont_merge(self): +@@ -3633,7 +3632,7 @@ def test_dont_merge(self): # @unittest.skipUnless(HAS_REDUCTION, "test needs multiprocessing.reduction") @@ -252,7 +321,7 @@ index e096401c4cf..40cec1057bb 100644 class _TestPicklingConnections(BaseTestCase): ALLOWED_TYPES = ('processes',) -@@ -3936,7 +3936,7 @@ def test_copy(self): +@@ -3936,7 +3935,7 @@ def test_copy(self): @unittest.skipUnless(HAS_SHMEM, "requires multiprocessing.shared_memory") @@ -261,7 +330,7 @@ index e096401c4cf..40cec1057bb 100644 class _TestSharedMemory(BaseTestCase): ALLOWED_TYPES = ('processes',) -@@ -4777,7 +4777,7 @@ def test_invalid_handles(self): +@@ -4777,7 +4776,7 @@ def test_invalid_handles(self): @@ -270,167 +339,61 @@ index e096401c4cf..40cec1057bb 100644 class OtherTest(unittest.TestCase): # TODO: add more tests for deliver/answer challenge. def test_deliver_challenge_auth_failure(self): -@@ -4807,6 +4807,159 @@ def send_bytes(self, data): +@@ -4797,7 +4796,7 @@ def __init__(self): + def recv_bytes(self, size): + self.count += 1 + if self.count == 1: +- return multiprocessing.connection.CHALLENGE ++ return multiprocessing.connection._CHALLENGE + elif self.count == 2: + return b'something bogus' + return b'' +@@ -4807,6 +4806,44 @@ def send_bytes(self, data): multiprocessing.connection.answer_challenge, _FakeConnection(), b'abc') -+ def test_deliver_challenge_sha256_response(self): -+ """Test modern SHA256 authentication (32-byte challenge, 32-byte response).""" -+ import hmac -+ authkey = b'test_secret_key_123' -+ challenge_message = None -+ -+ class _FakeConnection(object): -+ def __init__(self): -+ self.sent = [] -+ def send_bytes(self, data): -+ self.sent.append(data) -+ def recv_bytes(self, size): -+ # Extract challenge and compute correct SHA256 response -+ nonlocal challenge_message -+ if challenge_message is None: -+ # First call gets the challenge -+ challenge_data = self.sent[0] -+ assert challenge_data.startswith( -+ multiprocessing.connection.CHALLENGE) -+ challenge_message = challenge_data[ -+ len(multiprocessing.connection.CHALLENGE):] -+ # Verify it's 32 bytes (SHA256 protocol) -+ assert len(challenge_message) == 32, ( -+ f"Expected 32-byte challenge, got {len(challenge_message)}") -+ return hmac.new(authkey, challenge_message, 'sha256').digest() -+ -+ conn = _FakeConnection() -+ multiprocessing.connection.deliver_challenge(conn, authkey) -+ # Should have sent WELCOME -+ self.assertIn(multiprocessing.connection.WELCOME, conn.sent) + -+ def test_deliver_challenge_md5_response_legacy_client(self): -+ """Test backward compatibility: modern server accepts MD5 from legacy client.""" -+ import hmac -+ authkey = b'test_secret_key_456' -+ challenge_message = None -+ -+ class _FakeConnection(object): -+ def __init__(self): -+ self.sent = [] -+ def send_bytes(self, data): -+ self.sent.append(data) -+ def recv_bytes(self, size): -+ # Legacy client receives 32-byte challenge but responds with MD5 -+ nonlocal challenge_message -+ if challenge_message is None: -+ challenge_data = self.sent[0] -+ assert challenge_data.startswith( -+ multiprocessing.connection.CHALLENGE) -+ challenge_message = challenge_data[ -+ len(multiprocessing.connection.CHALLENGE):] -+ # Legacy client computes MD5 HMAC (16 bytes) -+ return hmac.new(authkey, challenge_message, 'md5').digest() -+ -+ conn = _FakeConnection() -+ multiprocessing.connection.deliver_challenge(conn, authkey) -+ # Should have sent WELCOME (backward compatible) -+ self.assertIn(multiprocessing.connection.WELCOME, conn.sent) -+ -+ def test_answer_challenge_sha256_from_modern_server(self): -+ """Test client responds with SHA256 to 32-byte challenge.""" -+ import hmac -+ authkey = b'test_secret_key_789' -+ challenge_message = os.urandom(32) # Modern 32-byte challenge -+ -+ class _FakeConnection(object): -+ def __init__(self): -+ self.sent = [] -+ def recv_bytes(self, size): -+ if not self.sent: -+ # First call: return challenge -+ return (multiprocessing.connection.CHALLENGE + -+ challenge_message) -+ else: -+ # Second call: return WELCOME -+ return multiprocessing.connection.WELCOME -+ def send_bytes(self, data): -+ self.sent.append(data) -+ -+ conn = _FakeConnection() -+ multiprocessing.connection.answer_challenge(conn, authkey) -+ # Should have sent SHA256 digest (32 bytes) -+ self.assertEqual(len(conn.sent), 1) -+ response = conn.sent[0] -+ self.assertEqual(len(response), 32) -+ # Verify it's correct SHA256 -+ expected = hmac.new(authkey, challenge_message, 'sha256').digest() -+ self.assertEqual(response, expected) -+ -+ def test_answer_challenge_md5_from_legacy_server(self): -+ """Test client responds with MD5 to 20-byte challenge (legacy server).""" -+ import hmac -+ authkey = b'test_secret_key_012' -+ challenge_message = os.urandom(20) # Legacy 20-byte challenge -+ -+ class _FakeConnection(object): -+ def __init__(self): -+ self.sent = [] -+ def recv_bytes(self, size): -+ if not self.sent: -+ # First call: return legacy challenge -+ return (multiprocessing.connection.CHALLENGE + -+ challenge_message) ++@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') ++class ChallengeResponseTest(unittest.TestCase): ++ authkey = b'supadupasecretkey' ++ ++ def create_response(self, message): ++ return multiprocessing.connection._create_response( ++ self.authkey, message ++ ) ++ ++ def verify_challenge(self, message, response): ++ return multiprocessing.connection._verify_challenge( ++ self.authkey, message, response ++ ) ++ ++ def test_challengeresponse(self): ++ for algo in [None, "md5", "sha256"]: ++ with self.subTest(f"{algo=}"): ++ msg = b'is-twenty-bytes-long' # The length of a legacy message. ++ if algo: ++ prefix = b'{%s}' % algo.encode("ascii") + else: -+ # Second call: return WELCOME -+ return multiprocessing.connection.WELCOME -+ def send_bytes(self, data): -+ self.sent.append(data) -+ -+ conn = _FakeConnection() -+ multiprocessing.connection.answer_challenge(conn, authkey) -+ # Should have sent MD5 digest (16 bytes) -+ self.assertEqual(len(conn.sent), 1) -+ response = conn.sent[0] -+ self.assertEqual(len(response), 16) -+ # Verify it's correct MD5 -+ expected = hmac.new(authkey, challenge_message, 'md5').digest() -+ self.assertEqual(response, expected) -+ -+ def test_bad_response_length_rejected(self): -+ """Test that responses with unexpected lengths are rejected.""" -+ authkey = b'test_key' -+ -+ class _FakeConnection(object): -+ def __init__(self): -+ self.sent = [] -+ def send_bytes(self, data): -+ self.sent.append(data) -+ def recv_bytes(self, size): -+ # Return wrong length (not 16 or 32) -+ return b'x' * 24 ++ prefix = b'' ++ msg = prefix + msg ++ response = self.create_response(msg) ++ if not response.startswith(prefix): ++ self.fail(response) ++ self.verify_challenge(msg, response) + -+ conn = _FakeConnection() -+ with self.assertRaises(multiprocessing.AuthenticationError) as cm: -+ multiprocessing.connection.deliver_challenge(conn, authkey) -+ self.assertIn('unexpected response length', str(cm.exception)) ++ # TODO(gpshead): We need integration tests for handshakes between modern ++ # deliver_challenge() and verify_response() code and connections running a ++ # test-local copy of the legacy Python <=3.11 implementations. + -+ def test_bad_challenge_length_rejected(self): -+ """Test that challenges with unexpected lengths are rejected.""" -+ authkey = b'test_key' -+ bad_challenge = os.urandom(25) # Not 20 or 32 -+ -+ class _FakeConnection(object): -+ def recv_bytes(self, size): -+ return multiprocessing.connection.CHALLENGE + bad_challenge -+ def send_bytes(self, data): -+ pass -+ -+ conn = _FakeConnection() -+ with self.assertRaises(multiprocessing.AuthenticationError) as cm: -+ multiprocessing.connection.answer_challenge(conn, authkey) -+ self.assertIn('unexpected challenge length', str(cm.exception)) ++ # TODO(gpshead): properly annotate tests for requires_hashdigest rather than ++ # only running these on a platform supporting everything. otherwise logic ++ # issues preventing it from working on FIPS mode setups will be hidden. + # # Test Manager.start()/Pool.__init__() initializer feature - see issue 5585 # -@@ -4814,7 +4967,7 @@ def send_bytes(self, data): +@@ -4814,7 +4851,7 @@ def send_bytes(self, data): def initializer(ns): ns.test += 1 @@ -439,7 +402,7 @@ index e096401c4cf..40cec1057bb 100644 class TestInitializers(unittest.TestCase): def setUp(self): self.mgr = multiprocessing.Manager() -@@ -5729,7 +5882,7 @@ def is_alive(self): +@@ -5729,7 +5766,7 @@ def is_alive(self): any(process.is_alive() for process in forked_processes)) @@ -448,7 +411,7 @@ index e096401c4cf..40cec1057bb 100644 class TestSyncManagerTypes(unittest.TestCase): """Test all the types which can be shared between a parent and a child process by using a manager which acts as an intermediary -@@ -6174,7 +6327,7 @@ def install_tests_in_module_dict(remote_globs, start_method, +@@ -6174,7 +6211,7 @@ def install_tests_in_module_dict(remote_globs, start_method, class Temp(base, Mixin, unittest.TestCase): pass if type_ == 'manager': @@ -457,17 +420,11 @@ index e096401c4cf..40cec1057bb 100644 Temp.__name__ = Temp.__qualname__ = newname Temp.__module__ = __module__ remote_globs[newname] = Temp -diff --git a/Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst b/Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst +diff --git a/Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst b/Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst new file mode 100644 -index 00000000000..117301d86af +index 00000000000000..18ebd6e140cff7 --- /dev/null -+++ b/Misc/NEWS.d/next/Library/2025-01-26-12-00-00.gh-61460.abc123.rst -@@ -0,0 +1,5 @@ -+:mod:`multiprocessing` now uses HMAC-SHA256 instead of HMAC-MD5 for -+inter-process connection authentication. The implementation maintains -+backward compatibility with Python 3.10 and earlier using length-based -+protocol detection. Note: In FIPS mode where MD5 is unavailable, this -+version cannot authenticate with Python ≤3.10 clients/servers. --- -2.51.2 - ++++ b/Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst +@@ -0,0 +1,2 @@ ++:mod:`multiprocessing` now supports stronger HMAC algorithms for inter-process ++connection authentication rather than only HMAC-MD5. From 2678f7ac3e2612200364119a3a1f6fc1d16d9ac3 Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 27 Nov 2025 09:27:34 +0000 Subject: [PATCH 3/6] Add Origin patch header --- python-3.11/gh-61460.patch | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python-3.11/gh-61460.patch b/python-3.11/gh-61460.patch index a4c911c4627..96aa5ce2253 100644 --- a/python-3.11/gh-61460.patch +++ b/python-3.11/gh-61460.patch @@ -1,3 +1,5 @@ +Origin: https://github.com/DataDog/cpython/commit/9e0b30d517b0c01cfb2a29d761a69eb7310f8cc3 + From 9e0b30d517b0c01cfb2a29d761a69eb7310f8cc3 Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sun, 21 May 2023 01:33:09 +0200 From f7e34868bc842b565e5cf24955cadb202d412206 Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 27 Nov 2025 09:34:55 +0000 Subject: [PATCH 4/6] Pick SHA256 as default for Python 3.10 as well. --- python-3.10.yaml | 9 +- python-3.10/gh-61460.patch | 430 ++++++++++++++++++++++++++++ python-3.10/test_multiprocessing.py | 23 ++ 3 files changed, 460 insertions(+), 2 deletions(-) create mode 100644 python-3.10/gh-61460.patch create mode 100644 python-3.10/test_multiprocessing.py diff --git a/python-3.10.yaml b/python-3.10.yaml index 7f013136bad..8df6bb92b6b 100644 --- a/python-3.10.yaml +++ b/python-3.10.yaml @@ -1,7 +1,7 @@ package: name: python-3.10 version: "3.10.19" - epoch: 2 + epoch: 3 description: "the Python programming language" copyright: - license: PSF-2.0 @@ -59,7 +59,9 @@ pipeline: - uses: patch with: - patches: gh-127301.patch + patches: | + gh-127301.patch + gh-61460.patch - uses: autoconf/configure with: @@ -261,6 +263,9 @@ test: $d/bin/pip list $d/bin/pip check rm -Rf "$d" + - name: Verify multiprocessing with SHA256 as default + runs: | + python3 test_multiprocessing.py update: enabled: true diff --git a/python-3.10/gh-61460.patch b/python-3.10/gh-61460.patch new file mode 100644 index 00000000000..020eef078be --- /dev/null +++ b/python-3.10/gh-61460.patch @@ -0,0 +1,430 @@ +From 42f82222a9ea530feb8c2b64af488624a8cb23fd Mon Sep 17 00:00:00 2001 +From: Christian Heimes +Date: Sun, 21 May 2023 01:33:09 +0200 +Subject: [PATCH] [3.10] gh-61460: Stronger HMAC in multiprocessing (GH-20380) + +bpo-17258: `multiprocessing` now supports stronger HMAC algorithms for inter-process connection authentication rather than only HMAC-MD5. + +Signed-off-by: Christian Heimes + +gpshead: I Reworked to be more robust while keeping the idea. + +The protocol modification idea remains, but we now take advantage of the +message length as an indicator of legacy vs modern protocol version. No +more regular expression usage. We now default to HMAC-SHA256, but do so +in a way that will be compatible when communicating with older clients +or older servers. No protocol transition period is needed. + +More integration tests to verify these claims remain true are required. I'm +unaware of anyone depending on multiprocessing connections between +different Python versions. + +--------- + +(cherry picked from commit 3ed57e4995d9f8583083483f397ddc3131720953) + +Co-authored-by: Christian Heimes +Signed-off-by: Christian Heimes +Co-authored-by: Gregory P. Smith [Google] +--- + Lib/multiprocessing/connection.py | 222 ++++++++++++++++-- + Lib/test/_test_multiprocessing.py | 57 ++++- + .../2020-05-25-12-42-36.bpo-17258.lf2554.rst | 2 + + 3 files changed, 254 insertions(+), 27 deletions(-) + create mode 100644 Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst + +diff --git a/Lib/multiprocessing/connection.py b/Lib/multiprocessing/connection.py +index 8e2facf92a94aa..d47bc7b600577c 100644 +--- a/Lib/multiprocessing/connection.py ++++ b/Lib/multiprocessing/connection.py +@@ -723,39 +723,227 @@ def PipeClient(address): + # Authentication stuff + # + +-MESSAGE_LENGTH = 20 ++MESSAGE_LENGTH = 40 # MUST be > 20 + +-CHALLENGE = b'#CHALLENGE#' +-WELCOME = b'#WELCOME#' +-FAILURE = b'#FAILURE#' ++_CHALLENGE = b'#CHALLENGE#' ++_WELCOME = b'#WELCOME#' ++_FAILURE = b'#FAILURE#' + +-def deliver_challenge(connection, authkey): ++# multiprocessing.connection Authentication Handshake Protocol Description ++# (as documented for reference after reading the existing code) ++# ============================================================================= ++# ++# On Windows: native pipes with "overlapped IO" are used to send the bytes, ++# instead of the length prefix SIZE scheme described below. (ie: the OS deals ++# with message sizes for us) ++# ++# Protocol error behaviors: ++# ++# On POSIX, any failure to receive the length prefix into SIZE, for SIZE greater ++# than the requested maxsize to receive, or receiving fewer than SIZE bytes ++# results in the connection being closed and auth to fail. ++# ++# On Windows, receiving too few bytes is never a low level _recv_bytes read ++# error, receiving too many will trigger an error only if receive maxsize ++# value was larger than 128 OR the if the data arrived in smaller pieces. ++# ++# Serving side Client side ++# ------------------------------ --------------------------------------- ++# 0. Open a connection on the pipe. ++# 1. Accept connection. ++# 2. Random 20+ bytes -> MESSAGE ++# Modern servers always send ++# more than 20 bytes and include ++# a {digest} prefix on it with ++# their preferred HMAC digest. ++# Legacy ones send ==20 bytes. ++# 3. send 4 byte length (net order) ++# prefix followed by: ++# b'#CHALLENGE#' + MESSAGE ++# 4. Receive 4 bytes, parse as network byte ++# order integer. If it is -1, receive an ++# additional 8 bytes, parse that as network ++# byte order. The result is the length of ++# the data that follows -> SIZE. ++# 5. Receive min(SIZE, 256) bytes -> M1 ++# 6. Assert that M1 starts with: ++# b'#CHALLENGE#' ++# 7. Strip that prefix from M1 into -> M2 ++# 7.1. Parse M2: if it is exactly 20 bytes in ++# length this indicates a legacy server ++# supporting only HMAC-MD5. Otherwise the ++# 7.2. preferred digest is looked up from an ++# expected "{digest}" prefix on M2. No prefix ++# or unsupported digest? <- AuthenticationError ++# 7.3. Put divined algorithm name in -> D_NAME ++# 8. Compute HMAC-D_NAME of AUTHKEY, M2 -> C_DIGEST ++# 9. Send 4 byte length prefix (net order) ++# followed by C_DIGEST bytes. ++# 10. Receive 4 or 4+8 byte length ++# prefix (#4 dance) -> SIZE. ++# 11. Receive min(SIZE, 256) -> C_D. ++# 11.1. Parse C_D: legacy servers ++# accept it as is, "md5" -> D_NAME ++# 11.2. modern servers check the length ++# of C_D, IF it is 16 bytes? ++# 11.2.1. "md5" -> D_NAME ++# and skip to step 12. ++# 11.3. longer? expect and parse a "{digest}" ++# prefix into -> D_NAME. ++# Strip the prefix and store remaining ++# bytes in -> C_D. ++# 11.4. Don't like D_NAME? <- AuthenticationError ++# 12. Compute HMAC-D_NAME of AUTHKEY, ++# MESSAGE into -> M_DIGEST. ++# 13. Compare M_DIGEST == C_D: ++# 14a: Match? Send length prefix & ++# b'#WELCOME#' ++# <- RETURN ++# 14b: Mismatch? Send len prefix & ++# b'#FAILURE#' ++# <- CLOSE & AuthenticationError ++# 15. Receive 4 or 4+8 byte length prefix (net ++# order) again as in #4 into -> SIZE. ++# 16. Receive min(SIZE, 256) bytes -> M3. ++# 17. Compare M3 == b'#WELCOME#': ++# 17a. Match? <- RETURN ++# 17b. Mismatch? <- CLOSE & AuthenticationError ++# ++# If this RETURNed, the connection remains open: it has been authenticated. ++# ++# Length prefixes are used consistently. Even on the legacy protocol, this ++# was good fortune and allowed us to evolve the protocol by using the length ++# of the opening challenge or length of the returned digest as a signal as ++# to which protocol the other end supports. ++ ++_ALLOWED_DIGESTS = frozenset( ++ {b'md5', b'sha256', b'sha384', b'sha3_256', b'sha3_384'}) ++_MAX_DIGEST_LEN = max(len(_) for _ in _ALLOWED_DIGESTS) ++ ++# Old hmac-md5 only server versions from Python <=3.11 sent a message of this ++# length. It happens to not match the length of any supported digest so we can ++# use a message of this length to indicate that we should work in backwards ++# compatible md5-only mode without a {digest_name} prefix on our response. ++_MD5ONLY_MESSAGE_LENGTH = 20 ++_MD5_DIGEST_LEN = 16 ++_LEGACY_LENGTHS = (_MD5ONLY_MESSAGE_LENGTH, _MD5_DIGEST_LEN) ++ ++ ++def _get_digest_name_and_payload(message: bytes) -> (str, bytes): ++ """Returns a digest name and the payload for a response hash. ++ ++ If a legacy protocol is detected based on the message length ++ or contents the digest name returned will be empty to indicate ++ legacy mode where MD5 and no digest prefix should be sent. ++ """ ++ # modern message format: b"{digest}payload" longer than 20 bytes ++ # legacy message format: 16 or 20 byte b"payload" ++ if len(message) in _LEGACY_LENGTHS: ++ # Either this was a legacy server challenge, or we're processing ++ # a reply from a legacy client that sent an unprefixed 16-byte ++ # HMAC-MD5 response. All messages using the modern protocol will ++ # be longer than either of these lengths. ++ return '', message ++ if (message.startswith(b'{') and ++ (curly := message.find(b'}', 1, _MAX_DIGEST_LEN+2)) > 0): ++ digest = message[1:curly] ++ if digest in _ALLOWED_DIGESTS: ++ payload = message[curly+1:] ++ return digest.decode('ascii'), payload ++ raise AuthenticationError( ++ 'unsupported message length, missing digest prefix, ' ++ f'or unsupported digest: {message=}') ++ ++ ++def _create_response(authkey, message): ++ """Create a MAC based on authkey and message ++ ++ The MAC algorithm defaults to HMAC-MD5, unless MD5 is not available or ++ the message has a '{digest_name}' prefix. For legacy HMAC-MD5, the response ++ is the raw MAC, otherwise the response is prefixed with '{digest_name}', ++ e.g. b'{sha256}abcdefg...' ++ ++ Note: The MAC protects the entire message including the digest_name prefix. ++ """ + import hmac ++ digest_name = _get_digest_name_and_payload(message)[0] ++ # The MAC protects the entire message: digest header and payload. ++ if not digest_name: ++ # Legacy server without a {digest} prefix on message. ++ # Generate a legacy non-prefixed HMAC-MD5 reply. ++ try: ++ return hmac.new(authkey, message, 'md5').digest() ++ except ValueError: ++ # HMAC-MD5 is not available (FIPS mode?), fall back to ++ # HMAC-SHA2-256 modern protocol. The legacy server probably ++ # doesn't support it and will reject us anyways. :shrug: ++ digest_name = 'sha256' ++ # Modern protocol, indicate the digest used in the reply. ++ response = hmac.new(authkey, message, digest_name).digest() ++ return b'{%s}%s' % (digest_name.encode('ascii'), response) ++ ++ ++def _verify_challenge(authkey, message, response): ++ """Verify MAC challenge ++ ++ If our message did not include a digest_name prefix, the client is allowed ++ to select a stronger digest_name from _ALLOWED_DIGESTS. ++ ++ In case our message is prefixed, a client cannot downgrade to a weaker ++ algorithm, because the MAC is calculated over the entire message ++ including the '{digest_name}' prefix. ++ """ ++ import hmac ++ response_digest, response_mac = _get_digest_name_and_payload(response) ++ response_digest = response_digest or 'md5' ++ try: ++ expected = hmac.new(authkey, message, response_digest).digest() ++ except ValueError: ++ raise AuthenticationError(f'{response_digest=} unsupported') ++ if len(expected) != len(response_mac): ++ raise AuthenticationError( ++ f'expected {response_digest!r} of length {len(expected)} ' ++ f'got {len(response_mac)}') ++ if not hmac.compare_digest(expected, response_mac): ++ raise AuthenticationError('digest received was wrong') ++ ++ ++def deliver_challenge(connection, authkey: bytes, digest_name='sha256'): + if not isinstance(authkey, bytes): + raise ValueError( + "Authkey must be bytes, not {0!s}".format(type(authkey))) ++ assert MESSAGE_LENGTH > _MD5ONLY_MESSAGE_LENGTH, "protocol constraint" + message = os.urandom(MESSAGE_LENGTH) +- connection.send_bytes(CHALLENGE + message) +- digest = hmac.new(authkey, message, 'md5').digest() ++ message = b'{%s}%s' % (digest_name.encode('ascii'), message) ++ # Even when sending a challenge to a legacy client that does not support ++ # digest prefixes, they'll take the entire thing as a challenge and ++ # respond to it with a raw HMAC-MD5. ++ connection.send_bytes(_CHALLENGE + message) + response = connection.recv_bytes(256) # reject large message +- if response == digest: +- connection.send_bytes(WELCOME) ++ try: ++ _verify_challenge(authkey, message, response) ++ except AuthenticationError: ++ connection.send_bytes(_FAILURE) ++ raise + else: +- connection.send_bytes(FAILURE) +- raise AuthenticationError('digest received was wrong') ++ connection.send_bytes(_WELCOME) + +-def answer_challenge(connection, authkey): +- import hmac ++ ++def answer_challenge(connection, authkey: bytes): + if not isinstance(authkey, bytes): + raise ValueError( + "Authkey must be bytes, not {0!s}".format(type(authkey))) + message = connection.recv_bytes(256) # reject large message +- assert message[:len(CHALLENGE)] == CHALLENGE, 'message = %r' % message +- message = message[len(CHALLENGE):] +- digest = hmac.new(authkey, message, 'md5').digest() ++ if not message.startswith(_CHALLENGE): ++ raise AuthenticationError( ++ f'Protocol error, expected challenge: {message=}') ++ message = message[len(_CHALLENGE):] ++ if len(message) < _MD5ONLY_MESSAGE_LENGTH: ++ raise AuthenticationError('challenge too short: {len(message)} bytes') ++ digest = _create_response(authkey, message) + connection.send_bytes(digest) + response = connection.recv_bytes(256) # reject large message +- if response != WELCOME: ++ if response != _WELCOME: + raise AuthenticationError('digest sent was rejected') + + # +diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py +index 57eada634c4459..298e87ad3040fc 100644 +--- a/Lib/test/_test_multiprocessing.py ++++ b/Lib/test/_test_multiprocessing.py +@@ -47,6 +47,7 @@ + import multiprocessing.managers + import multiprocessing.pool + import multiprocessing.queues ++from multiprocessing.connection import wait, AuthenticationError + + from multiprocessing import util + +@@ -125,8 +126,6 @@ def _resource_unlink(name, rtype): + + WIN32 = (sys.platform == "win32") + +-from multiprocessing.connection import wait +- + def wait_for_handle(handle, timeout): + if timeout is not None and timeout < 0.0: + timeout = None +@@ -2983,7 +2982,7 @@ def test_remote(self): + del queue + + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class _TestManagerRestart(BaseTestCase): + + @classmethod +@@ -3468,7 +3467,7 @@ def test_dont_merge(self): + # + + @unittest.skipUnless(HAS_REDUCTION, "test needs multiprocessing.reduction") +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class _TestPicklingConnections(BaseTestCase): + + ALLOWED_TYPES = ('processes',) +@@ -3771,7 +3770,7 @@ def test_copy(self): + + + @unittest.skipUnless(HAS_SHMEM, "requires multiprocessing.shared_memory") +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class _TestSharedMemory(BaseTestCase): + + ALLOWED_TYPES = ('processes',) +@@ -4578,7 +4577,7 @@ def test_invalid_handles(self): + + + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class OtherTest(unittest.TestCase): + # TODO: add more tests for deliver/answer challenge. + def test_deliver_challenge_auth_failure(self): +@@ -4598,7 +4597,7 @@ def __init__(self): + def recv_bytes(self, size): + self.count += 1 + if self.count == 1: +- return multiprocessing.connection.CHALLENGE ++ return multiprocessing.connection._CHALLENGE + elif self.count == 2: + return b'something bogus' + return b'' +@@ -4608,6 +4607,44 @@ def send_bytes(self, data): + multiprocessing.connection.answer_challenge, + _FakeConnection(), b'abc') + ++ ++@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') ++class ChallengeResponseTest(unittest.TestCase): ++ authkey = b'supadupasecretkey' ++ ++ def create_response(self, message): ++ return multiprocessing.connection._create_response( ++ self.authkey, message ++ ) ++ ++ def verify_challenge(self, message, response): ++ return multiprocessing.connection._verify_challenge( ++ self.authkey, message, response ++ ) ++ ++ def test_challengeresponse(self): ++ for algo in [None, "md5", "sha256"]: ++ with self.subTest(f"{algo=}"): ++ msg = b'is-twenty-bytes-long' # The length of a legacy message. ++ if algo: ++ prefix = b'{%s}' % algo.encode("ascii") ++ else: ++ prefix = b'' ++ msg = prefix + msg ++ response = self.create_response(msg) ++ if not response.startswith(prefix): ++ self.fail(response) ++ self.verify_challenge(msg, response) ++ ++ # TODO(gpshead): We need integration tests for handshakes between modern ++ # deliver_challenge() and verify_response() code and connections running a ++ # test-local copy of the legacy Python <=3.11 implementations. ++ ++ # TODO(gpshead): properly annotate tests for requires_hashdigest rather than ++ # only running these on a platform supporting everything. otherwise logic ++ # issues preventing it from working on FIPS mode setups will be hidden. ++ + # + # Test Manager.start()/Pool.__init__() initializer feature - see issue 5585 + # +@@ -4615,7 +4652,7 @@ def send_bytes(self, data): + def initializer(ns): + ns.test += 1 + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class TestInitializers(unittest.TestCase): + def setUp(self): + self.mgr = multiprocessing.Manager() +@@ -5478,7 +5515,7 @@ def is_alive(self): + any(process.is_alive() for process in forked_processes)) + + +-@hashlib_helper.requires_hashdigest('md5') ++@hashlib_helper.requires_hashdigest('sha256') + class TestSyncManagerTypes(unittest.TestCase): + """Test all the types which can be shared between a parent and a + child process by using a manager which acts as an intermediary +@@ -5905,7 +5942,7 @@ def install_tests_in_module_dict(remote_globs, start_method): + class Temp(base, Mixin, unittest.TestCase): + pass + if type_ == 'manager': +- Temp = hashlib_helper.requires_hashdigest('md5')(Temp) ++ Temp = hashlib_helper.requires_hashdigest('sha256')(Temp) + Temp.__name__ = Temp.__qualname__ = newname + Temp.__module__ = __module__ + remote_globs[newname] = Temp +diff --git a/Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst b/Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst +new file mode 100644 +index 00000000000000..18ebd6e140cff7 +--- /dev/null ++++ b/Misc/NEWS.d/next/Library/2020-05-25-12-42-36.bpo-17258.lf2554.rst +@@ -0,0 +1,2 @@ ++:mod:`multiprocessing` now supports stronger HMAC algorithms for inter-process ++connection authentication rather than only HMAC-MD5. diff --git a/python-3.10/test_multiprocessing.py b/python-3.10/test_multiprocessing.py new file mode 100644 index 00000000000..e61bf74a083 --- /dev/null +++ b/python-3.10/test_multiprocessing.py @@ -0,0 +1,23 @@ +import multiprocessing + +def worker(managed_list, process_id): + print(f"Process {process_id}: Starting") + managed_list.append(process_id) + +if __name__ == '__main__': + + with multiprocessing.Manager() as manager: + shared_list = manager.list() + processes = [] + num_processes = 5 + for i in range(num_processes): + p = multiprocessing.Process(target=worker, args=(shared_list, i)) + processes.append(p) + p.start() + + for p in processes: + p.join() + + print("\nAll processes finished.") + print(f"Final shared list: {list(shared_list)}") + print(f"List length: {len(shared_list)}") From 142ed256c32974042088ac3f113b7342b1344c3d Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 27 Nov 2025 09:35:14 +0000 Subject: [PATCH 5/6] python-3.11: add test for multiprocessing. --- python-3.11.yaml | 3 +++ python-3.11/test_multiprocessing.py | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 python-3.11/test_multiprocessing.py diff --git a/python-3.11.yaml b/python-3.11.yaml index efed39aaca7..3d8349397f9 100644 --- a/python-3.11.yaml +++ b/python-3.11.yaml @@ -263,6 +263,9 @@ test: $d/bin/pip list $d/bin/pip check rm -Rf "$d" + - name: Verify multiprocessing with SHA256 as default + runs: | + python3 test_multiprocessing.py update: enabled: true diff --git a/python-3.11/test_multiprocessing.py b/python-3.11/test_multiprocessing.py new file mode 100644 index 00000000000..e61bf74a083 --- /dev/null +++ b/python-3.11/test_multiprocessing.py @@ -0,0 +1,23 @@ +import multiprocessing + +def worker(managed_list, process_id): + print(f"Process {process_id}: Starting") + managed_list.append(process_id) + +if __name__ == '__main__': + + with multiprocessing.Manager() as manager: + shared_list = manager.list() + processes = [] + num_processes = 5 + for i in range(num_processes): + p = multiprocessing.Process(target=worker, args=(shared_list, i)) + processes.append(p) + p.start() + + for p in processes: + p.join() + + print("\nAll processes finished.") + print(f"Final shared list: {list(shared_list)}") + print(f"List length: {len(shared_list)}") From cf6f023b70796cd8297ad06445207b13e7075d92 Mon Sep 17 00:00:00 2001 From: James Page Date: Thu, 27 Nov 2025 09:36:44 +0000 Subject: [PATCH 6/6] Add origin header for Python 3.10 patch. --- python-3.10/gh-61460.patch | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python-3.10/gh-61460.patch b/python-3.10/gh-61460.patch index 020eef078be..1535aa63873 100644 --- a/python-3.10/gh-61460.patch +++ b/python-3.10/gh-61460.patch @@ -1,3 +1,5 @@ +Origin: https://github.com/DataDog/cpython/commit/42f82222a9ea530feb8c2b64af488624a8cb23fd + From 42f82222a9ea530feb8c2b64af488624a8cb23fd Mon Sep 17 00:00:00 2001 From: Christian Heimes Date: Sun, 21 May 2023 01:33:09 +0200