Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
=======

Expand Down
76 changes: 76 additions & 0 deletions relenv/build/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,80 @@

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:
Expand Down Expand Up @@ -909,6 +983,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]
Expand Down
2 changes: 1 addition & 1 deletion relenv/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down
58 changes: 58 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<patched_ssl>", "exec")
Loading