From 4a0f60c9f49f82e18b897ae914fa33351470a5a4 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Wed, 24 Jun 2026 15:06:09 -0700 Subject: [PATCH 1/3] Apply cpython#104135 work-around to bundled ssl.py on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python 3.10 and 3.11 stdlib's ssl._load_windows_store_certs concatenates every Windows root-store certificate and feeds the lot to OpenSSL's load_verify_locations as one cadata blob. OpenSSL 3.5.x (shipped since relenv 0.22.13) rejects the entire blob on a single ASN.1-malformed cert with "[ASN1: NOT_ENOUGH_DATA]", killing every TLS connection inside the onedir — pip-over-HTTPS, salt-call to a master, anything. Upstream merged an iterate-and-skip rewrite onto the 3.12 branch but never backported it; see https://github.com/python/cpython/issues/104135. Append the same iterate-and-skip replacement to the bundled Lib/ssl.py during the Windows build (relenv/build/windows.py). The patch self-disables on Python 3.12+ and on non-Windows, so calling it unconditionally from the Windows builder is safe. Idempotent via a marker comment, and the marker matches the one in Salt's cicd/windows-ssl-104135-patch.py so an ssl.py patched by either side is left alone by the other during the transition window. Once Salt picks up a relenv release carrying this, they drop the patch script, the three workflow steps in build-deps-ci-action.yml / test-action.yml / test-packages-action.yml, the salt/__init__.py monkey-patch, and the salt/ext/tornado/netutil.py certifi pin. Bumps __version__ to 0.22.15 and adds a CHANGELOG.md entry. --- CHANGELOG.md | 14 ++++++++ relenv/build/windows.py | 75 +++++++++++++++++++++++++++++++++++++++++ relenv/common.py | 2 +- 3 files changed, 90 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47cd3b27..d6123713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +0.22.15 +======= + +* Patch bundled Lib/ssl.py on Windows Python 3.10 / 3.11 to work around + cpython#104135. The pre-fix _load_windows_store_certs concatenated + every Windows root-store certificate and handed the blob to OpenSSL, + which (3.5.x) rejects the lot on a single ASN.1-malformed cert with + [ASN1: NOT_ENOUGH_DATA] — breaking pip-over-HTTPS and any other TLS + use inside an onedir. Upstream merged an iterate-and-skip rewrite + for Python 3.12+ but never backported it; relenv now applies the same + rewrite at build time. Self-disables on Python 3.12+ and on + non-Windows. + + 0.22.14 ======= diff --git a/relenv/build/windows.py b/relenv/build/windows.py index d7bd61d4..9f6b5162 100644 --- a/relenv/build/windows.py +++ b/relenv/build/windows.py @@ -43,6 +43,79 @@ ARCHES = arches[WIN32] +# cpython#104135 work-around. Python 3.10 / 3.11 stdlib's +# ssl._load_windows_store_certs concatenates every Windows root-store +# certificate and hands the blob to OpenSSL's load_verify_locations. +# OpenSSL 3.5.x rejects the whole blob on a single ASN.1-malformed cert +# with `[ASN1: NOT_ENOUGH_DATA]`, which kills any TLS connection inside +# the onedir (pip install, salt-call to a master, etc). Upstream +# merged an iterate-and-skip rewrite onto the 3.12 branch but never +# backported it to 3.10 or 3.11. See +# https://github.com/python/cpython/issues/104135. +# +# Append the iterate-and-skip replacement to the bundled stdlib's +# Lib/ssl.py during the build. The patch self-disables on Python +# 3.12+ and on non-Windows, so writing it unconditionally here is safe; +# we only ever invoke this on the Windows builder anyway. +# +# Marker matches Salt's cicd/windows-ssl-104135-patch.py so an ssl.py +# patched by either side is left alone by the other during the +# transition window. Once a relenv release carrying this patch is +# pinned by Salt, Salt drops its workaround chain (the patch script, +# the three workflow steps, the salt/__init__.py monkey-patch, and the +# salt/ext/tornado/netutil.py certifi pin). +_SSL_PATCH_MARKER = "# >>> cpython#104135 patch (windows-ssl-104135-patch.py) <<<" +_SSL_PATCH_BODY = f""" + +{_SSL_PATCH_MARKER} +# Applied at relenv build time; see relenv/build/windows.py. +import sys as _patch_sys + +if _patch_sys.platform == "win32" and _patch_sys.version_info < (3, 12): + + def _salt_safe_load_windows_store_certs( + self, storename, purpose, _SSLError=SSLError + ): + try: + from _ssl import enum_certificates + except ImportError: + return + try: + for cert, encoding, trust in enum_certificates(storename): + if encoding != "x509_asn": + continue + if trust is True or purpose.oid in trust: + try: + self.load_verify_locations(cadata=cert) + except _SSLError: + pass + except PermissionError: + pass + + SSLContext._load_windows_store_certs = _salt_safe_load_windows_store_certs +""" + + +def patch_ssl_for_cpython_104135(source_dir: pathlib.Path) -> None: + """ + Apply the cpython#104135 iterate-and-skip work-around to ``Lib/ssl.py``. + + Idempotent: skips if the marker is already present. Self-disabling + on Python 3.12+ and non-Windows; safe to call unconditionally from + the Windows build path. + """ + ssl_py = source_dir / "Lib" / "ssl.py" + if not ssl_py.exists(): + log.warning("Lib/ssl.py not found at %s; cpython#104135 patch skipped", ssl_py) + return + contents = ssl_py.read_text(encoding="utf-8") + if _SSL_PATCH_MARKER in contents: + log.info("Lib/ssl.py already carries cpython#104135 patch; skipping") + return + log.info("Appending cpython#104135 work-around to Lib/ssl.py") + with open(ssl_py, "a", encoding="utf-8") as fh: + fh.write(_SSL_PATCH_BODY) + EnvMapping = MutableMapping[str, str] if sys.platform == WIN32: @@ -909,6 +982,8 @@ def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None: with open(str(batch_file), "w") as f: f.write("@echo off\necho skipping fetch\n") + patch_ssl_for_cpython_104135(dirs.source) + arch_to_plat = {"amd64": "x64", "x86": "win32", "arm64": "arm64"} arch = env["RELENV_HOST_ARCH"] plat = arch_to_plat[arch] diff --git a/relenv/common.py b/relenv/common.py index 72055c28..07e79c3f 100644 --- a/relenv/common.py +++ b/relenv/common.py @@ -27,7 +27,7 @@ from collections.abc import Callable, Iterable, Mapping # relenv package version -__version__ = "0.22.14" +__version__ = "0.22.15" log = logging.getLogger(__name__) From 0f826df8cef040f4b9a43106cd8745be1297cda2 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Wed, 24 Jun 2026 18:47:31 -0700 Subject: [PATCH 2/3] Add regression tests for cpython#104135 ssl.py patcher Covers: - append + marker present on a clean Lib/ssl.py - idempotency: applying twice leaves exactly one marker block - missing Lib/ssl.py logs a warning and returns without raising - appended block compiles cleanly as Python (with SSLError / SSLContext stubs to match what ssl.py provides at module scope) Behaviour of the patched _load_windows_store_certs itself is exercised end-to-end by Salt's onedir CI on real Windows runners; these unit tests guard the file plumbing relenv owns. --- tests/test_build.py | 58 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/test_build.py b/tests/test_build.py index d3085eb2..1d24857a 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -532,3 +532,61 @@ def test_copy_pyconfig_h_missing_legacy_raises(tmp_path: pathlib.Path) -> None: with pytest.raises(RuntimeError, match="Expected pyconfig.h at"): copy_pyconfig_h(source, build_dir, dest_dir) assert not (dest_dir / "pyconfig.h").exists() + + +def test_patch_ssl_for_cpython_104135_appends_when_marker_missing(tmp_path: pathlib.Path) -> None: + """The helper writes the patch body into Lib/ssl.py and adds the marker.""" + from relenv.build.windows import _SSL_PATCH_MARKER, patch_ssl_for_cpython_104135 + + (tmp_path / "Lib").mkdir() + ssl_py = tmp_path / "Lib" / "ssl.py" + original = '"""stub ssl module."""\n\nclass SSLContext: pass\nclass SSLError(Exception): pass\n' + ssl_py.write_text(original, encoding="utf-8") + + patch_ssl_for_cpython_104135(tmp_path) + + patched = ssl_py.read_text(encoding="utf-8") + assert patched.startswith(original) + assert _SSL_PATCH_MARKER in patched + assert "_load_windows_store_certs" in patched + + +def test_patch_ssl_for_cpython_104135_is_idempotent(tmp_path: pathlib.Path) -> None: + """Applying twice leaves exactly one marker (no double append).""" + from relenv.build.windows import _SSL_PATCH_MARKER, patch_ssl_for_cpython_104135 + + (tmp_path / "Lib").mkdir() + ssl_py = tmp_path / "Lib" / "ssl.py" + ssl_py.write_text("# stub\n", encoding="utf-8") + + patch_ssl_for_cpython_104135(tmp_path) + after_first = ssl_py.read_text(encoding="utf-8") + patch_ssl_for_cpython_104135(tmp_path) + after_second = ssl_py.read_text(encoding="utf-8") + + assert after_first == after_second + assert after_second.count(_SSL_PATCH_MARKER) == 1 + + +def test_patch_ssl_for_cpython_104135_missing_file_is_noop( + tmp_path: pathlib.Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """A source tree without Lib/ssl.py logs a warning and does not raise.""" + from relenv.build.windows import patch_ssl_for_cpython_104135 + + with caplog.at_level(logging.WARNING, logger="relenv.build.windows"): + patch_ssl_for_cpython_104135(tmp_path) + + assert any("Lib/ssl.py not found" in r.message for r in caplog.records) + assert not (tmp_path / "Lib").exists() + + +def test_patch_ssl_for_cpython_104135_body_is_valid_python() -> None: + """The appended block must compile cleanly as a Python module.""" + from relenv.build.windows import _SSL_PATCH_BODY + + # ssl.py defines SSLError / SSLContext at module scope; supply stubs so + # the patch body resolves the same names when concatenated. + preamble = "class SSLError(Exception): pass\nclass SSLContext: pass\n" + compile(preamble + _SSL_PATCH_BODY, "", "exec") From fd75c47021a8e2c08c2da6e5cf8732b4182ba501 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Wed, 24 Jun 2026 18:55:43 -0700 Subject: [PATCH 3/3] Format: ruff-format blank line after patch_ssl_for_cpython_104135 Pre-commit ruff-format wanted two blank lines between the new function and the EnvMapping type alias; CI surfaced this after GitHub's rebase. --- relenv/build/windows.py | 1 + 1 file changed, 1 insertion(+) diff --git a/relenv/build/windows.py b/relenv/build/windows.py index 9f6b5162..145b8d09 100644 --- a/relenv/build/windows.py +++ b/relenv/build/windows.py @@ -116,6 +116,7 @@ def patch_ssl_for_cpython_104135(source_dir: pathlib.Path) -> None: with open(ssl_py, "a", encoding="utf-8") as fh: fh.write(_SSL_PATCH_BODY) + EnvMapping = MutableMapping[str, str] if sys.platform == WIN32: