Submitted By: Xi Ruoyao Date: 2026-05-11 Initial Package Version: 3.14.5 Upstream Status: Applied Origin: Upstream, see the cherry picked from labels for SHA. The changes to kTLS (which is only supported since 3.15) are dropped. The 4th change is edited to avoid the conflict with "fix reference leaks in `ssl.SSLContext` objects": the latter is already in both 3.15 (1decc7ee20cf) and 3.14.5 (3a2a686cc45d), but in 3.15 it's before our 4th change, so the different order causes a conflict. Description: Fix build failure and runtime issues with OpenSSL 4.0. Updated By: Bruce Dubbs Date: 2026-05-27 Initial Package Version: 3.14.6 Upstream Status: Applied Origin: As above plus https://github.com/python/cpython/pull/151545 CVE-2026-12003 Allow builds of Python to be run from an in-tree layout https://github.com/python/cpython/pull/143929 CVE-2026-0864 Do not allow carriage return characters (\r) when using the "configparser" module to write configuration files https://github.com/python/cpython/pull/151559 CVE-2026-11940 Do not allow tarfile.extractall() with the 'data' or 'tar' filter to be bypassed diff -Naur Python-3.14.6/Lib/configparser.py Python-3.14.6-patched/Lib/configparser.py --- Python-3.14.6/Lib/configparser.py 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Lib/configparser.py 2026-06-27 19:25:32.364624182 -0500 @@ -992,7 +992,9 @@ value = self._interpolation.before_write(self, section_name, key, value) if value is not None or not self._allow_no_value: - value = delimiter + str(value).replace('\n', '\n\t') + # Convert all possible line-endings into '\n\t' + value = (delimiter + str(value).replace('\r\n', '\n') + .replace('\r', '\n').replace('\n', '\n\t')) else: value = "" fp.write("{}{}\n".format(key, value)) diff -Naur Python-3.14.6/Lib/tarfile.py Python-3.14.6-patched/Lib/tarfile.py --- Python-3.14.6/Lib/tarfile.py 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Lib/tarfile.py 2026-06-27 19:25:55.886332255 -0500 @@ -2782,6 +2782,9 @@ "makelink_with_filter: if filter_function is not None, " + "extraction_root must also not be None") try: + filter_function( + unfiltered.replace(name=tarinfo.name, deep=False), + extraction_root) filtered = filter_function(unfiltered, extraction_root) except _FILTER_ERRORS as cause: raise LinkFallbackError(tarinfo, unfiltered.name) from cause diff -Naur Python-3.14.6/Lib/test/test_configparser.py Python-3.14.6-patched/Lib/test/test_configparser.py --- Python-3.14.6/Lib/test/test_configparser.py 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Lib/test/test_configparser.py 2026-06-27 19:25:32.365231633 -0500 @@ -526,6 +526,17 @@ cf.get(self.default_section, "Foo"), "Bar", "could not locate option, expecting case-insensitive defaults") + def test_crlf_normalization(self): + cf = self.newconfig({"key1": "a\nb","key2": "a\rb", "key3": "a\r\nb", "key4": "a\r\nb"}) + buf = io.StringIO() + cf.write(buf) + cf_str = buf.getvalue() + self.assertNotIn("\r", cf_str) + self.assertNotIn("\r\n", cf_str) + self.assertEqual(cf_str.count("\n"), 10) + self.assertEqual(cf_str.count("\n\t"), 4) + self.assertTrue(cf_str.endswith("\n\n")) + def test_parse_errors(self): cf = self.newconfig() self.parse_error(cf, configparser.ParsingError, diff -Naur Python-3.14.6/Lib/test/test_ssl.py Python-3.14.6-patched/Lib/test/test_ssl.py --- Python-3.14.6/Lib/test/test_ssl.py 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Lib/test/test_ssl.py 2026-06-27 19:26:20.623259319 -0500 @@ -396,7 +396,7 @@ ssl.OP_NO_COMPRESSION self.assertEqual(ssl.HAS_SNI, True) self.assertEqual(ssl.HAS_ECDH, True) - self.assertEqual(ssl.HAS_TLSv1_2, True) + self.assertIsInstance(ssl.HAS_TLSv1_2, bool) self.assertEqual(ssl.HAS_TLSv1_3, True) ssl.OP_NO_SSLv2 ssl.OP_NO_SSLv3 @@ -587,11 +587,11 @@ # Some sanity checks follow # >= 1.1.1 self.assertGreaterEqual(n, 0x10101000) - # < 4.0 - self.assertLess(n, 0x40000000) + # < 5.0 + self.assertLess(n, 0x50000000) major, minor, fix, patch, status = t self.assertGreaterEqual(major, 1) - self.assertLess(major, 4) + self.assertLess(major, 5) self.assertGreaterEqual(minor, 0) self.assertLess(minor, 256) self.assertGreaterEqual(fix, 0) @@ -657,12 +657,14 @@ ssl.OP_NO_TLSv1_2, ssl.OP_NO_TLSv1_3 ] - protocols = [ - ssl.PROTOCOL_TLSv1, - ssl.PROTOCOL_TLSv1_1, - ssl.PROTOCOL_TLSv1_2, - ssl.PROTOCOL_TLS - ] + protocols = [] + if hasattr(ssl, 'PROTOCOL_TLSv1'): + protocols.append(ssl.PROTOCOL_TLSv1) + if hasattr(ssl, 'PROTOCOL_TLSv1_1'): + protocols.append(ssl.PROTOCOL_TLSv1_1) + if hasattr(ssl, 'PROTOCOL_TLSv1_2'): + protocols.append(ssl.PROTOCOL_TLSv1_2) + protocols.append(ssl.PROTOCOL_TLS) versions = [ ssl.TLSVersion.SSLv3, ssl.TLSVersion.TLSv1, @@ -1156,6 +1158,7 @@ ssl.TLSVersion.TLSv1, ssl.TLSVersion.TLSv1_1, ssl.TLSVersion.TLSv1_2, + ssl.TLSVersion.TLSv1_3, ssl.TLSVersion.SSLv3, } ) @@ -1169,7 +1172,7 @@ with self.assertRaises(ValueError): ctx.minimum_version = 42 - if has_tls_protocol(ssl.PROTOCOL_TLSv1_1): + if has_tls_protocol('PROTOCOL_TLSv1_1'): ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1_1) self.assertIn( @@ -1717,23 +1720,24 @@ self.assertFalse(ctx.check_hostname) self._assert_context_options(ctx) - if has_tls_protocol(ssl.PROTOCOL_TLSv1): + if has_tls_protocol('PROTOCOL_TLSv1'): with warnings_helper.check_warnings(): ctx = ssl._create_stdlib_context(ssl.PROTOCOL_TLSv1) self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1) self.assertEqual(ctx.verify_mode, ssl.CERT_NONE) self._assert_context_options(ctx) - with warnings_helper.check_warnings(): - ctx = ssl._create_stdlib_context( - ssl.PROTOCOL_TLSv1_2, - cert_reqs=ssl.CERT_REQUIRED, - check_hostname=True - ) - self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1_2) - self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED) - self.assertTrue(ctx.check_hostname) - self._assert_context_options(ctx) + if has_tls_protocol('PROTOCOL_TLSv1_2'): + with warnings_helper.check_warnings(): + ctx = ssl._create_stdlib_context( + ssl.PROTOCOL_TLSv1_2, + cert_reqs=ssl.CERT_REQUIRED, + check_hostname=True + ) + self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLSv1_2) + self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED) + self.assertTrue(ctx.check_hostname) + self._assert_context_options(ctx) ctx = ssl._create_stdlib_context(purpose=ssl.Purpose.CLIENT_AUTH) self.assertEqual(ctx.protocol, ssl.PROTOCOL_TLS_SERVER) @@ -2760,6 +2764,36 @@ def stop(self): self.active = False +class TestEOFServer(threading.Thread): + def __init__(self): + super().__init__() + self.listening = threading.Event() + self.address = None + + def run(self): + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + context.load_cert_chain(CERTFILE) + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + with server_sock: + server_sock.settimeout(support.SHORT_TIMEOUT) + server_sock.bind((HOST, 0)) + server_sock.listen(5) + + self.address = server_sock.getsockname() + self.listening.set() + + sock, addr = server_sock.accept() + sslconn = context.wrap_socket(sock, server_side=True) + with sslconn: + request = b'' + while chunk := sslconn.recv(1024): + request += chunk + if b'\n' in chunk: + break + + sslconn.sendall(b'server\n') + sslconn.shutdown(socket.SHUT_WR) + class AsyncoreEchoServer(threading.Thread): # this one's based on asyncore.dispatcher @@ -3629,10 +3663,10 @@ client_options=ssl.OP_NO_TLSv1_2) try_protocol_combo(ssl.PROTOCOL_TLS, ssl.PROTOCOL_TLSv1_2, 'TLSv1.2') - if has_tls_protocol(ssl.PROTOCOL_TLSv1): + if has_tls_protocol('PROTOCOL_TLSv1'): try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1, False) try_protocol_combo(ssl.PROTOCOL_TLSv1, ssl.PROTOCOL_TLSv1_2, False) - if has_tls_protocol(ssl.PROTOCOL_TLSv1_1): + if has_tls_protocol('PROTOCOL_TLSv1_1'): try_protocol_combo(ssl.PROTOCOL_TLSv1_2, ssl.PROTOCOL_TLSv1_1, False) try_protocol_combo(ssl.PROTOCOL_TLSv1_1, ssl.PROTOCOL_TLSv1_2, False) @@ -4794,6 +4828,58 @@ if cm.exc_value is not None: raise cm.exc_value + def test_got_eof(self): + # gh-148292: Test that _ssl._SSLSocket behaves the same on all OpenSSL + # versions on calling methods after EOF (after the first SSLEOFError). + + server = TestEOFServer() + server.start() + if not server.listening.wait(support.SHORT_TIMEOUT): + raise RuntimeError("server took too long") + self.addCleanup(server.join) + + context = ssl.create_default_context(cafile=CERTFILE) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(support.SHORT_TIMEOUT) + sock.connect(server.address) + sslsock = context.wrap_socket(sock, server_hostname='localhost') + with sslsock: + sslsock.sendall(b'client\n') + # test the _ssl._SSLSocket object, not ssl.SSLSocket + sslobj = sslsock._sslobj + + data = sslobj.read(1024) + self.assertEqual(data, b'server\n') + + # The second read gets EOF error and sets got_eof_error to 1 + with self.assertRaises(ssl.SSLEOFError): + sslobj.read(1024) + + # Following read(), sendfile(), write() and do_handshake() calls + # must raise SSLEOFError + with self.assertRaises(ssl.SSLEOFError): + # The _SSLSocket remembers the previous EOF error + # and raises again SSLEOFError + sslobj.read(1024) + if hasattr(sslobj, 'sendfile'): + with open(__file__, "rb") as fp: + with self.assertRaises(ssl.SSLEOFError): + sslobj.sendfile(fp.fileno(), 0, 1) + with self.assertRaises(ssl.SSLEOFError): + sslobj.write(b'client2\n') + with self.assertRaises(ssl.SSLEOFError): + sslsock.do_handshake() + + self.assertEqual(sslsock.pending(), 0) + try: + sslsock.shutdown(socket.SHUT_WR) + except OSError as exc: + self.assertEqual(exc.errno, errno.ENOTCONN) + else: + # On Windows and on OpenSSL 1.1.1, shutdown() doesn't + # raise an error + pass + @unittest.skipUnless(has_tls_version('TLSv1_3') and ssl.HAS_PHA, "Test needs TLS 1.3 PHA") @@ -5220,6 +5306,20 @@ with self.assertRaises(TypeError): client_context._msg_callback = object() + def test_msg_callback_exception(self): + client_context, server_context, hostname = testing_context() + + def msg_cb(conn, direction, version, content_type, msg_type, data): + raise RuntimeError("msg_cb exception") + + client_context._msg_callback = msg_cb + server = ThreadedEchoServer(context=server_context, chatty=False) + with server: + with client_context.wrap_socket(socket.socket(), + server_hostname=hostname) as s: + with self.assertRaisesRegex(RuntimeError, "msg_cb exception"): + s.connect((HOST, server.port)) + def test_msg_callback_tls12(self): client_context, server_context, hostname = testing_context() client_context.maximum_version = ssl.TLSVersion.TLSv1_2 diff -Naur Python-3.14.6/Lib/test/test_tarfile.py Python-3.14.6-patched/Lib/test/test_tarfile.py --- Python-3.14.6/Lib/test/test_tarfile.py 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Lib/test/test_tarfile.py 2026-06-27 19:25:55.886689848 -0500 @@ -4345,6 +4345,30 @@ self.expect_file("c", symlink_to='b') @symlink_test + def test_sneaky_hardlink_fallback_deep(self): + # (CVE-2026-11940) + with ArchiveMaker() as arc: + arc.add("a/b/s", symlink_to=os.path.join("..", "escape")) + arc.add("s", hardlink_to=os.path.join("a", "b", "s")) + + with self.check_context(arc.open(), 'data'): + e = self.expect_exception( + tarfile.LinkFallbackError, + "link 's' would be extracted as a copy of " + + "'a/b/s', which was rejected") + self.assertIsInstance(e.__cause__, + tarfile.LinkOutsideDestinationError) + + for filter in 'tar', 'fully_trusted': + with self.subTest(filter), self.check_context(arc.open(), filter): + if not os_helper.can_symlink(): + self.expect_file("a/") + self.expect_file("a/b/") + else: + self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape')) + self.expect_file("s", symlink_to=os.path.join('..', 'escape')) + + @symlink_test def test_exfiltration_via_symlink(self): # (CVE-2025-4138) # Test changing symlinks that result in a symlink pointing outside diff -Naur Python-3.14.6/Makefile.pre.in Python-3.14.6-patched/Makefile.pre.in --- Python-3.14.6/Makefile.pre.in 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Makefile.pre.in 2026-06-27 19:25:45.149174104 -0500 @@ -1679,6 +1679,8 @@ _bootstrap_python: $(LIBRARY_OBJS_OMIT_FROZEN) Programs/_bootstrap_python.o Modules/getpath.o Modules/Setup.local $(LINKCC) $(PY_LDFLAGS_NOLTO) -o $@ $(LIBRARY_OBJS_OMIT_FROZEN) \ Programs/_bootstrap_python.o Modules/getpath.o $(LIBS) $(MODLIBS) $(SYSLIBS) + # Dummy pybuilddir.txt is needed for _bootstrap_python to be runnable + @echo "none" > ./pybuilddir.txt ############################################################################ diff -Naur Python-3.14.6/Misc/NEWS.d/next/Library/2025-12-08-18-12-44.gh-issue-142438.UF_0nd.rst Python-3.14.6-patched/Misc/NEWS.d/next/Library/2025-12-08-18-12-44.gh-issue-142438.UF_0nd.rst --- Python-3.14.6/Misc/NEWS.d/next/Library/2025-12-08-18-12-44.gh-issue-142438.UF_0nd.rst 1969-12-31 18:00:00.000000000 -0600 +++ Python-3.14.6-patched/Misc/NEWS.d/next/Library/2025-12-08-18-12-44.gh-issue-142438.UF_0nd.rst 2026-06-27 19:26:20.621995065 -0500 @@ -0,0 +1 @@ +Fixed a possible leaked GIL in _PySSL_keylog_callback. diff -Naur Python-3.14.6/Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst Python-3.14.6-patched/Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst --- Python-3.14.6/Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst 1969-12-31 18:00:00.000000000 -0600 +++ Python-3.14.6-patched/Misc/NEWS.d/next/Library/2026-04-28-17-47-55.gh-issue-148292.oIq3ml.rst 2026-06-27 19:26:20.623462484 -0500 @@ -0,0 +1,7 @@ +:mod:`ssl`: Update :class:`ssl.SSLSocket` and :class:`ssl.SSLObject` for +OpenSSL 4. The classes now remember if they get a :exc:`ssl.SSLEOFError`. In this +case, following :meth:`~ssl.SSLSocket.read`, :meth:`!sendfile`, +:meth:`~ssl.SSLSocket.write`, and :meth:`~ssl.SSLSocket.do_handshake` calls +raise :exc:`ssl.SSLEOFError` without calling the underlying OpenSSL function. +Thanks to that, :class:`ssl.SSLSocket` behaves the same on all OpenSSL versions +on EOF. Patch by Victor Stinner. diff -Naur Python-3.14.6/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst Python-3.14.6-patched/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst --- Python-3.14.6/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst 1969-12-31 18:00:00.000000000 -0600 +++ Python-3.14.6-patched/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst 2026-06-27 19:25:32.365334556 -0500 @@ -0,0 +1,2 @@ +Normalize all line endings (CR, CRLF, and LF) to LF+TAB when writing +multi-line configparser values. diff -Naur Python-3.14.6/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst Python-3.14.6-patched/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst --- Python-3.14.6/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst 1969-12-31 18:00:00.000000000 -0600 +++ Python-3.14.6-patched/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst 2026-06-27 19:25:55.885857628 -0500 @@ -0,0 +1,3 @@ +Fixed an vulnerability in the :mod:`tarfile` ``data`` and ``tar`` extraction +filters where crafted archives could create a symlink pointing outside the +destination directory. This was a bypass of :cve:`2025-4330`. diff -Naur Python-3.14.6/Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst Python-3.14.6-patched/Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst --- Python-3.14.6/Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst 1969-12-31 18:00:00.000000000 -0600 +++ Python-3.14.6-patched/Misc/NEWS.d/next/Security/2026-06-16-14-58-02.gh-issue-151544._bexVy.rst 2026-06-27 19:25:45.148527980 -0500 @@ -0,0 +1,4 @@ +:file:`Modules/Setup.local` is no longer used as a landmark to discover +whether Python is running in a source tree, as it could potentially affect +actual installs. The :file:`pybuilddir.txt` file is now the sole indicator +of running in a source tree. diff -Naur Python-3.14.6/Modules/getpath.py Python-3.14.6-patched/Modules/getpath.py --- Python-3.14.6/Modules/getpath.py 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Modules/getpath.py 2026-06-27 19:25:45.148621944 -0500 @@ -129,8 +129,7 @@ # checked by looking for the BUILDDIR_TXT file, which contains the # relative path to the platlib dir. The executable_dir value is # derived from joining the VPATH preprocessor variable to the -# directory containing pybuilddir.txt. If it is not found, the -# BUILD_LANDMARK file is found, which is part of the source tree. +# directory containing pybuilddir.txt. # prefix is then found by searching up for a file that should only # exist in the source tree, and the stdlib dir is set to prefix/Lib. @@ -177,7 +176,6 @@ if os_name == 'posix' or os_name == 'darwin': BUILDDIR_TXT = 'pybuilddir.txt' - BUILD_LANDMARK = 'Modules/Setup.local' DEFAULT_PROGRAM_NAME = f'python{VERSION_MAJOR}' STDLIB_SUBDIR = f'{platlibdir}/python{VERSION_MAJOR}.{VERSION_MINOR}{ABI_THREAD}' STDLIB_LANDMARKS = [f'{STDLIB_SUBDIR}/os.py', f'{STDLIB_SUBDIR}/os.pyc'] @@ -190,7 +188,6 @@ elif os_name == 'nt': BUILDDIR_TXT = 'pybuilddir.txt' - BUILD_LANDMARK = f'{VPATH}\\Modules\\Setup.local' DEFAULT_PROGRAM_NAME = f'python' STDLIB_SUBDIR = 'Lib' STDLIB_LANDMARKS = [f'{STDLIB_SUBDIR}\\os.py', f'{STDLIB_SUBDIR}\\os.pyc'] @@ -512,13 +509,9 @@ platstdlib_dir = real_executable_dir build_prefix = joinpath(real_executable_dir, VPATH) except (FileNotFoundError, PermissionError): - if isfile(joinpath(real_executable_dir, BUILD_LANDMARK)): - build_prefix = joinpath(real_executable_dir, VPATH) - if os_name == 'nt': - # QUIRK: Windows builds need platstdlib_dir to be the executable - # dir. Normally the builddir marker handles this, but in this - # case we need to correct manually. - platstdlib_dir = real_executable_dir + # We used to check for an alternate landmark here, but now we require + # BUILDDIR_TXT to exist. (gh-151544; CVE-2026-12003) + pass if build_prefix: if os_name == 'nt': diff -Naur Python-3.14.6/Modules/_ssl/cert.c Python-3.14.6-patched/Modules/_ssl/cert.c --- Python-3.14.6/Modules/_ssl/cert.c 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Modules/_ssl/cert.c 2026-06-27 19:26:20.621111999 -0500 @@ -128,7 +128,8 @@ } static PyObject* -_x509name_print(_sslmodulestate *state, X509_NAME *name, int indent, unsigned long flags) +_x509name_print(_sslmodulestate *state, const X509_NAME *name, + int indent, unsigned long flags) { PyObject *res; BIO *biobuf; diff -Naur Python-3.14.6/Modules/_ssl/debughelpers.c Python-3.14.6-patched/Modules/_ssl/debughelpers.c --- Python-3.14.6/Modules/_ssl/debughelpers.c 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Modules/_ssl/debughelpers.c 2026-06-27 19:26:20.623070745 -0500 @@ -26,6 +26,8 @@ return; } + PyObject *exc = PyErr_GetRaisedException(); + PyObject *ssl_socket; /* ssl.SSLSocket or ssl.SSLObject */ if (ssl_obj->owner) PyWeakref_GetRef(ssl_obj->owner, &ssl_socket); @@ -73,13 +75,13 @@ version, content_type, msg_type, buf, len ); - if (res == NULL) { - ssl_obj->exc = PyErr_GetRaisedException(); - } else { - Py_DECREF(res); - } + Py_XDECREF(res); Py_XDECREF(ssl_socket); + if (exc != NULL) { + _PyErr_ChainExceptions1(exc); + } + PyGILState_Release(threadstate); } @@ -122,16 +124,19 @@ { PyGILState_STATE threadstate; PySSLSocket *ssl_obj = NULL; /* ssl._SSLSocket, borrowed ref */ + PyObject *exc; int res, e; threadstate = PyGILState_Ensure(); + exc = PyErr_GetRaisedException(); + ssl_obj = (PySSLSocket *)SSL_get_app_data(ssl); assert(Py_IS_TYPE(ssl_obj, get_state_sock(ssl_obj)->PySSLSocket_Type)); PyThread_type_lock lock = get_state_sock(ssl_obj)->keylog_lock; assert(lock != NULL); if (ssl_obj->ctx->keylog_bio == NULL) { - return; + goto done; } /* * The lock is neither released on exit nor on fork(). The lock is @@ -153,7 +158,11 @@ errno = e; PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, ssl_obj->ctx->keylog_filename); - ssl_obj->exc = PyErr_GetRaisedException(); + } + +done: + if (exc != NULL) { + _PyErr_ChainExceptions1(exc); } PyGILState_Release(threadstate); } diff -Naur Python-3.14.6/Modules/_ssl.c Python-3.14.6-patched/Modules/_ssl.c --- Python-3.14.6/Modules/_ssl.c 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Modules/_ssl.c 2026-06-27 19:26:20.623794346 -0500 @@ -135,6 +135,17 @@ #error Unsupported OpenSSL version #endif +#if (OPENSSL_VERSION_NUMBER >= 0x40000000L) +# define OPENSSL_NO_SSL3 +# define OPENSSL_NO_TLS1 +# define OPENSSL_NO_TLS1_1 +# define OPENSSL_NO_TLS1_2 +# define OPENSSL_NO_SSL3_METHOD +# define OPENSSL_NO_TLS1_METHOD +# define OPENSSL_NO_TLS1_1_METHOD +# define OPENSSL_NO_TLS1_2_METHOD +#endif + /* OpenSSL API 1.1.0+ does not include version methods */ #ifndef OPENSSL_NO_SSL3_METHOD extern const SSL_METHOD *SSLv3_method(void); @@ -334,12 +345,16 @@ enum py_ssl_server_or_client socket_type; PyObject *owner; /* weakref to Python level "owner" passed to servername callback */ PyObject *server_hostname; - _PySSLError err; /* last seen error from various sources */ - /* Some SSL callbacks don't have error reporting. Callback wrappers - * store exception information on the socket. The handshake, read, write, - * and shutdown methods check for chained exceptions. - */ - PyObject *exc; + // gh-148292: If non-zero, read(), sendfile(), write() and do_handshake() + // methods raise SSLEOFError without calling the underlying OpenSSL + // function. Set to 1 on PY_SSL_ERROR_EOF error. + // + // On OpenSSL 4, if SSL_read_ex() fails with + // SSL_R_UNEXPECTED_EOF_WHILE_READING, the following SSL_read_ex() call + // fails with a generic protocol error (ERR_peek_last_error() returns 0). + // Use got_eof_error to have the same behavior on OpenSSL 4 and newer and + // on OpenSSL 3 and older. + int got_eof_error; } PySSLSocket; #define PySSLSocket_CAST(op) ((PySSLSocket *)(op)) @@ -487,6 +502,10 @@ PyObject *init_value, *msg, *key; PyUnicodeWriter *writer = NULL; + if (ssl_errno == PY_SSL_ERROR_EOF && sslsock != NULL) { + sslsock->got_eof_error = 1; + } + if (errcode != 0) { int lib, reason; @@ -632,22 +651,27 @@ PyUnicodeWriter_Discard(writer); } -static int -PySSL_ChainExceptions(PySSLSocket *sslsock) { - if (sslsock->exc == NULL) - return 0; - _PyErr_ChainExceptions1(sslsock->exc); - sslsock->exc = NULL; - return -1; +static void +set_eof_error(PySSLSocket *sslsock) +{ + _sslmodulestate *state = get_state_sock(sslsock); + fill_and_set_sslerror(state, sslsock, state->PySSLEOFErrorObject, + PY_SSL_ERROR_EOF, + "EOF occurred in violation of protocol", + __LINE__, 0); } + +// Set the appropriate SSL error exception. +// err - error information from SSL and libc +// exc - if not NULL, an exception from _debughelpers.c callback to be chained static PyObject * -PySSL_SetError(PySSLSocket *sslsock, const char *filename, int lineno) +PySSL_SetError(PySSLSocket *sslsock, _PySSLError err, PyObject *exc, + const char *filename, int lineno) { PyObject *type; char *errstr = NULL; - _PySSLError err; enum py_ssl_error p = PY_SSL_ERROR_NONE; unsigned long e = 0; @@ -660,8 +684,6 @@ e = ERR_peek_last_error(); if (sslsock->ssl != NULL) { - err = sslsock->err; - switch (err.ssl) { case SSL_ERROR_ZERO_RETURN: errstr = "TLS/SSL connection has been closed (EOF)"; @@ -754,7 +776,7 @@ } fill_and_set_sslerror(state, sslsock, type, p, errstr, lineno, e); ERR_clear_error(); - PySSL_ChainExceptions(sslsock); + _PyErr_ChainExceptions1(exc); // chain any exceptions from callbacks return NULL; } @@ -859,7 +881,6 @@ { PySSLSocket *self; SSL_CTX *ctx = sslctx->ctx; - _PySSLError err = { 0 }; if ((socket_type == PY_SSL_SERVER) && (sslctx->protocol == PY_SSL_VERSION_TLS_CLIENT)) { @@ -887,8 +908,7 @@ self->shutdown_seen_zero = 0; self->owner = NULL; self->server_hostname = NULL; - self->err = err; - self->exc = NULL; + self->got_eof_error = 0; /* Make sure the SSL error state is initialized */ ERR_clear_error(); @@ -1009,6 +1029,7 @@ { int ret; _PySSLError err; + PyObject *exc = NULL; int sockstate, nonblocking; PySocketSockObject *sock = GET_SOCKET(self); PyTime_t timeout, deadline = 0; @@ -1029,6 +1050,11 @@ BIO_set_nbio(SSL_get_wbio(self->ssl), nonblocking); } + if (self->got_eof_error) { + set_eof_error(self); + goto error; + } + timeout = GET_SOCKET_TIMEOUT(sock); has_timeout = (timeout > 0); if (has_timeout) { @@ -1043,7 +1069,12 @@ err = _PySSL_errno(ret < 1, self->ssl, ret); Py_END_ALLOW_THREADS; _PySSL_FIX_ERRNO; - self->err = err; + + // Get any exception that occurred in a debughelpers.c callback + exc = PyErr_GetRaisedException(); + if (exc != NULL) { + break; + } if (PyErr_CheckSignals()) goto error; @@ -1079,13 +1110,15 @@ Py_XDECREF(sock); if (ret < 1) - return PySSL_SetError(self, __FILE__, __LINE__); - if (PySSL_ChainExceptions(self) < 0) + return PySSL_SetError(self, err, exc, __FILE__, __LINE__); + if (exc != NULL) { + PyErr_SetRaisedException(exc); return NULL; + } Py_RETURN_NONE; error: + assert(exc == NULL); Py_XDECREF(sock); - PySSL_ChainExceptions(self); return NULL; } @@ -1134,7 +1167,7 @@ static PyObject * _create_tuple_for_attribute(_sslmodulestate *state, - ASN1_OBJECT *name, ASN1_STRING *value) + const ASN1_OBJECT *name, const ASN1_STRING *value) { Py_ssize_t buflen; PyObject *pyattr; @@ -1163,16 +1196,16 @@ } static PyObject * -_create_tuple_for_X509_NAME (_sslmodulestate *state, X509_NAME *xname) +_create_tuple_for_X509_NAME(_sslmodulestate *state, const X509_NAME *xname) { PyObject *dn = NULL; /* tuple which represents the "distinguished name" */ PyObject *rdn = NULL; /* tuple to hold a "relative distinguished name" */ PyObject *rdnt; PyObject *attr = NULL; /* tuple to hold an attribute */ int entry_count = X509_NAME_entry_count(xname); - X509_NAME_ENTRY *entry; - ASN1_OBJECT *name; - ASN1_STRING *value; + const X509_NAME_ENTRY *entry; + const ASN1_OBJECT *name; + const ASN1_STRING *value; int index_counter; int rdn_level = -1; int retcode; @@ -2337,9 +2370,7 @@ static int PySSL_traverse(PyObject *op, visitproc visit, void *arg) { - PySSLSocket *self = PySSLSocket_CAST(op); - Py_VISIT(self->exc); - Py_VISIT(Py_TYPE(self)); + Py_VISIT(Py_TYPE(op)); return 0; } @@ -2351,7 +2382,6 @@ Py_CLEAR(self->ctx); Py_CLEAR(self->owner); Py_CLEAR(self->server_hostname); - Py_CLEAR(self->exc); return 0; } @@ -2471,6 +2501,7 @@ int retval; int sockstate; _PySSLError err; + PyObject *exc = NULL; int nonblocking; PySocketSockObject *sock = GET_SOCKET(self); PyTime_t timeout, deadline = 0; @@ -2493,6 +2524,11 @@ BIO_set_nbio(SSL_get_wbio(self->ssl), nonblocking); } + if (self->got_eof_error) { + set_eof_error(self); + goto error; + } + timeout = GET_SOCKET_TIMEOUT(sock); has_timeout = (timeout > 0); if (has_timeout) { @@ -2520,7 +2556,11 @@ err = _PySSL_errno(retval == 0, self->ssl, retval); Py_END_ALLOW_THREADS; _PySSL_FIX_ERRNO; - self->err = err; + + exc = PyErr_GetRaisedException(); + if (exc != NULL) { + break; + } if (PyErr_CheckSignals()) goto error; @@ -2553,13 +2593,15 @@ Py_XDECREF(sock); if (retval == 0) - return PySSL_SetError(self, __FILE__, __LINE__); - if (PySSL_ChainExceptions(self) < 0) + return PySSL_SetError(self, err, exc, __FILE__, __LINE__); + if (exc != NULL) { + PyErr_SetRaisedException(exc); return NULL; + } return PyLong_FromSize_t(count); error: + assert(exc == NULL); Py_XDECREF(sock); - PySSL_ChainExceptions(self); return NULL; } @@ -2582,10 +2624,9 @@ err = _PySSL_errno(count < 0, self->ssl, count); Py_END_ALLOW_THREADS; _PySSL_FIX_ERRNO; - self->err = err; if (count < 0) - return PySSL_SetError(self, __FILE__, __LINE__); + return PySSL_SetError(self, err, NULL, __FILE__, __LINE__); else return PyLong_FromLong(count); } @@ -2613,6 +2654,7 @@ int retval; int sockstate; _PySSLError err; + PyObject *exc = NULL; int nonblocking; PySocketSockObject *sock = GET_SOCKET(self); PyTime_t timeout, deadline = 0; @@ -2633,6 +2675,11 @@ Py_INCREF(sock); } + if (self->got_eof_error) { + set_eof_error(self); + goto error; + } + if (!group_right_1) { dest = PyBytes_FromStringAndSize(NULL, len); if (dest == NULL) @@ -2677,7 +2724,11 @@ err = _PySSL_errno(retval == 0, self->ssl, retval); Py_END_ALLOW_THREADS; _PySSL_FIX_ERRNO; - self->err = err; + + exc = PyErr_GetRaisedException(); + if (exc != NULL) { + break; + } if (PyErr_CheckSignals()) goto error; @@ -2710,13 +2761,18 @@ err.ssl == SSL_ERROR_WANT_WRITE); if (retval == 0) { - PySSL_SetError(self, __FILE__, __LINE__); + PySSL_SetError(self, err, exc, __FILE__, __LINE__); + exc = NULL; goto error; } - if (self->exc != NULL) + else if (exc != NULL) { + PyErr_SetRaisedException(exc); + exc = NULL; goto error; + } done: + assert(exc == NULL); Py_XDECREF(sock); if (!group_right_1) { _PyBytes_Resize(&dest, count); @@ -2727,7 +2783,7 @@ } error: - PySSL_ChainExceptions(self); + assert(exc == NULL); Py_XDECREF(sock); if (!group_right_1) Py_XDECREF(dest); @@ -2746,6 +2802,7 @@ /*[clinic end generated code: output=ca1aa7ed9d25ca42 input=98d9635cd4e16514]*/ { _PySSLError err; + PyObject *exc = NULL; int sockstate, nonblocking, ret; int zeros = 0; PySocketSockObject *sock = GET_SOCKET(self); @@ -2790,7 +2847,11 @@ err = _PySSL_errno(ret < 0, self->ssl, ret); Py_END_ALLOW_THREADS; _PySSL_FIX_ERRNO; - self->err = err; + + exc = PyErr_GetRaisedException(); + if (exc != NULL) { + break; + } /* If err == 1, a secure shutdown with SSL_shutdown() is complete */ if (ret > 0) @@ -2838,11 +2899,14 @@ } if (ret < 0) { Py_XDECREF(sock); - PySSL_SetError(self, __FILE__, __LINE__); + PySSL_SetError(self, err, exc, __FILE__, __LINE__); + return NULL; + } + else if (exc != NULL) { + Py_XDECREF(sock); + PyErr_SetRaisedException(exc); return NULL; } - if (self->exc != NULL) - goto error; if (sock) /* It's already INCREF'ed */ return (PyObject *) sock; @@ -2850,8 +2914,8 @@ Py_RETURN_NONE; error: + assert(exc == NULL); Py_XDECREF(sock); - PySSL_ChainExceptions(self); return NULL; } @@ -3054,7 +3118,6 @@ {Py_tp_getset, ssl_getsetlist}, {Py_tp_dealloc, PySSL_dealloc}, {Py_tp_traverse, PySSL_traverse}, - {Py_tp_clear, PySSL_clear}, {0, 0}, }; @@ -6513,9 +6576,15 @@ ADD_INT_CONST("PROTOCOL_TLS", PY_SSL_VERSION_TLS); ADD_INT_CONST("PROTOCOL_TLS_CLIENT", PY_SSL_VERSION_TLS_CLIENT); ADD_INT_CONST("PROTOCOL_TLS_SERVER", PY_SSL_VERSION_TLS_SERVER); +#ifndef OPENSSL_NO_TLS1 ADD_INT_CONST("PROTOCOL_TLSv1", PY_SSL_VERSION_TLS1); +#endif +#ifndef OPENSSL_NO_TLS1_1 ADD_INT_CONST("PROTOCOL_TLSv1_1", PY_SSL_VERSION_TLS1_1); +#endif +#ifndef OPENSSL_NO_TLS1_2 ADD_INT_CONST("PROTOCOL_TLSv1_2", PY_SSL_VERSION_TLS1_2); +#endif #define ADD_OPTION(NAME, VALUE) if (sslmodule_add_option(m, NAME, (VALUE)) < 0) return -1 diff -Naur Python-3.14.6/Tools/ssl/multissltests.py Python-3.14.6-patched/Tools/ssl/multissltests.py --- Python-3.14.6/Tools/ssl/multissltests.py 2026-06-10 05:03:53.000000000 -0500 +++ Python-3.14.6-patched/Tools/ssl/multissltests.py 2026-06-27 19:26:20.621164308 -0500 @@ -414,9 +414,11 @@ def _post_install(self): if self.version.startswith("3."): self._post_install_3xx() + elif self.version.startswith("4."): + self._post_install_4xx() def _build_src(self, config_args=()): - if self.version.startswith("3."): + if self.version.startswith(("3.", "4.")): config_args += ("enable-fips",) super()._build_src(config_args) @@ -432,6 +434,9 @@ lib64 = self.lib_dir + "64" os.symlink(lib64, self.lib_dir) + def _post_install_4xx(self): + self._post_install_3xx() + @property def short_version(self): """Short version for OpenSSL download URL"""