From 41d571f1e790880ee7b3900ede1f51f36be7706f Mon Sep 17 00:00:00 2001 From: "a.burlakov" Date: Fri, 6 Nov 2026 15:46:44 +0300 Subject: [PATCH 1/3] Fix: INTEGER_OVERFLOW.CHECKED Problem: In _PySSL_msg_callback (SSL3_RT_HEADER branch, Modules/_ssl/debughelpers.c), the TLS record header version bytes are read through a signed char pointer (const char *cbuf) and combined as version = cbuf[1] << 8 | cbuf[2]. OpenSSL invokes the message callback for a received record header before the version is validated, so both bytes are controlled by the remote peer. When cbuf[1] has its high bit set, a negative value is shifted left (undefined behaviour in C11); when cbuf[2] has its high bit set, sign extension clobbers the upper byte, and the Python msg_callback receives a wrong (negative or sign-extended) version value. Solution: cast both header bytes to unsigned char before the shift and the OR, so the version is always assembled as an unsigned 16-bit value. Signed-off-by: Aleksandr Burlakov --- Modules/_ssl/debughelpers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_ssl/debughelpers.c b/Modules/_ssl/debughelpers.c index 0e5422b73a89d4f..b65ea030a2e93a8 100644 --- a/Modules/_ssl/debughelpers.c +++ b/Modules/_ssl/debughelpers.c @@ -54,7 +54,7 @@ _PySSL_msg_callback(int write_p, int version, int content_type, #ifdef SSL3_RT_HEADER case SSL3_RT_HEADER: /* frame header encodes version in bytes 1..2 */ - version = cbuf[1] << 8 | cbuf[2]; + version = ((unsigned char)cbuf[1] << 8) | (unsigned char)cbuf[2]; msg_type = (int)cbuf[0]; break; #endif From dc4dd8e25c231086fa817f864efb1dacd916d71a Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:54:32 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2026-11-06-13-54-30.gh-issue-161472.dfa7DG.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2026-11-06-13-54-30.gh-issue-161472.dfa7DG.rst diff --git a/Misc/NEWS.d/next/Library/2026-11-06-13-54-30.gh-issue-161472.dfa7DG.rst b/Misc/NEWS.d/next/Library/2026-11-06-13-54-30.gh-issue-161472.dfa7DG.rst new file mode 100644 index 000000000000000..b597c38c08b4148 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-11-06-13-54-30.gh-issue-161472.dfa7DG.rst @@ -0,0 +1 @@ +Fix :mod:`ssl` message callbacks receiving incorrect TLS record header version values when either version byte has its high bit set. From f10388f5b9fe099f510f3aa9a824ba2e9f727a69 Mon Sep 17 00:00:00 2001 From: "a.burlakov" Date: Fri, 6 Nov 2026 17:19:38 +0300 Subject: [PATCH 3/3] gh-161472: Add regression test for SSL record header versions --- Lib/test/test_ssl.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 5b4e4f080607726..4e56d485870e9e4 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -6465,6 +6465,45 @@ def msg_cb(conn, direction, version, content_type, msg_type, data): msg ) + def test_msg_callback_header_version_unsigned(self): + # OpenSSL passes the record header to the callback before it + # validates the version, and both version bytes are controlled by + # the peer. + received = [] + + def msg_cb(conn, direction, version, content_type, msg_type, data): + if (direction == "read" and + content_type == _TLSContentType.HEADER): + received.append((version, data)) + + versions = [ + (b"\x80\x03", 0x8003), + (b"\x03\x80", 0x0380), + (b"\x80\x80", 0x8080), + ] + if IS_AWS_LC: + # AWS-LC validates the record version before invoking the + # callback, so headers whose major version byte is not 3 + # never reach it. + versions = [(b"\x03\x80", 0x0380)] + + for version_bytes, expected_version in versions: + with self.subTest(version_bytes=version_bytes): + received.clear() + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context._msg_callback = msg_cb + incoming = ssl.MemoryBIO() + outgoing = ssl.MemoryBIO() + sslobj = context.wrap_bio(incoming, outgoing, server_side=True) + + header = b"\x16" + version_bytes + b"\x00\x00" + incoming.write(header) + + with self.assertRaises(ssl.SSLError): + sslobj.do_handshake() + + self.assertEqual(received, [(expected_version, header)]) + def test_msg_callback_deadlock_bpo43577(self): client_context, server_context, hostname = testing_context() server_context2 = testing_context()[1]