From 40a5fb0d6b88305e22401e3754f30a082679aa7b Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Tue, 27 Oct 2026 16:53:38 -0700 Subject: [PATCH 1/9] CI: fail jobs that run an unpatched CPython interpreter Print sys.version in the harden-pip action and fail when the interpreter is older than the first release of its minor series that rejects the compromised eMudhra/emSign intermediate CA keys. --- .github/actions/harden-pip/action.yml | 14 +++- .../harden-pip/check_python_patched.py | 79 ++++++++++++++++++ ci/tools/tests/test_check_python_patched.py | 83 +++++++++++++++++++ ruff.toml | 1 + 4 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 .github/actions/harden-pip/check_python_patched.py create mode 100644 ci/tools/tests/test_check_python_patched.py diff --git a/.github/actions/harden-pip/action.yml b/.github/actions/harden-pip/action.yml index 868c30295f5..f4ccb7b0fb1 100644 --- a/.github/actions/harden-pip/action.yml +++ b/.github/actions/harden-pip/action.yml @@ -9,7 +9,10 @@ description: >- compromised eMudhra/emSign intermediate CAs. The sha256 fragment guarantees the bootstrap download's integrity even if the bootstrapping pip's trust store is compromised. See https://github.com/NVIDIA/cuda-python/issues/3195 and - https://discuss.python.org/t/109576. + https://discuss.python.org/t/109576. Before that, print sys.version and fail + if the interpreter is older than the first CPython release of its minor + series whose ssl module rejects the compromised intermediate keys (3.11.18, + 3.12.16, 3.13.18, 3.14.10, 3.15.2). inputs: python: @@ -19,6 +22,15 @@ inputs: runs: using: composite steps: + - name: Check Python interpreter is patched + shell: bash --noprofile --norc -xeuo pipefail {0} + env: + HARDEN_PIP_PYTHON: ${{ inputs.python }} + # The script lives next to this action so that it matches the action's + # commit even when the action is referenced from another checkout. + run: | + "${HARDEN_PIP_PYTHON}" "${GITHUB_ACTION_PATH}/check_python_patched.py" + - name: Install hash-pinned pip 26.2.2 shell: bash --noprofile --norc -xeuo pipefail {0} env: diff --git a/.github/actions/harden-pip/check_python_patched.py b/.github/actions/harden-pip/check_python_patched.py new file mode 100644 index 00000000000..51c16a6a022 --- /dev/null +++ b/.github/actions/harden-pip/check_python_patched.py @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + +"""Fail when the running CPython is older than the fix for the eMudhra/emSign CA compromise. + +Print ``sys.version`` and compare the interpreter with the first release of its +minor series whose ``ssl`` module rejects the compromised eMudhra intermediate +keys (CVE-2026-115557). Any release at or after that patch release passes. + +Release announcement: +https://discuss.python.org/t/python-3-15-2-3-14-10-3-13-18-3-12-16-and-3-11-18-are-out/109678 + +This file uses only the standard library because it runs on whichever +interpreter the job is about to use, before any package is installed. +""" + +from __future__ import annotations + +import sys + +# Minor series -> first patch release carrying the fix. +FIXED_MICRO = { + (3, 11): 18, + (3, 12): 16, + (3, 13): 18, + (3, 14): 10, + (3, 15): 2, +} + + +def classify(version_info) -> tuple[str, str]: + """Return ``(status, detail)`` where status is ``"ok"``, ``"warn"`` or ``"fail"``.""" + series = (version_info.major, version_info.minor) + found = f"{version_info.major}.{version_info.minor}.{version_info.micro}" + + if series in FIXED_MICRO: + fixed = f"{series[0]}.{series[1]}.{FIXED_MICRO[series]}" + if version_info.releaselevel != "final": + return ( + "fail", + f"Python {found} ({version_info.releaselevel}) is a pre-release; use a final release >= {fixed}.", + ) + if version_info.micro >= FIXED_MICRO[series]: + return "ok", f"Python {found} includes the fix (first fixed release of this series: {fixed})." + return ( + "fail", + f"Python {found} is older than {fixed}, the first release of this series that rejects the compromised eMudhra intermediate CA keys.", + ) + + if series > max(FIXED_MICRO): + # Newer than every series we know a fix for (for example 3.16 pre-releases). + return ( + "warn", + f"Python {found}: no fixed release is known for the {series[0]}.{series[1]} series yet; update FIXED_MICRO once one exists.", + ) + + return ( + "fail", + f"Python {series[0]}.{series[1]} has no release that rejects the compromised eMudhra intermediate CA keys.", + ) + + +def main() -> int: + print(f"sys.version: {sys.version}") + print(f"sys.executable: {sys.executable}") + status, detail = classify(sys.version_info) + if status == "ok": + print(detail) + return 0 + if status == "warn": + print(f"::warning title=Python patch level unverified::{detail}") + return 0 + print(f"::error title=Unpatched Python interpreter::{detail}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/ci/tools/tests/test_check_python_patched.py b/ci/tools/tests/test_check_python_patched.py new file mode 100644 index 00000000000..4494804e626 --- /dev/null +++ b/ci/tools/tests/test_check_python_patched.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the interpreter patch-level gate used by the harden-pip action.""" + +from __future__ import annotations + +import importlib.util +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest + +ACTION_DIR = Path(__file__).resolve().parents[3] / ".github" / "actions" / "harden-pip" +SCRIPT_PATH = ACTION_DIR / "check_python_patched.py" + +_spec = importlib.util.spec_from_file_location("check_python_patched", SCRIPT_PATH) +assert _spec is not None and _spec.loader is not None +check_python_patched = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(check_python_patched) + + +def _version(major: int, minor: int, micro: int, releaselevel: str = "final") -> SimpleNamespace: + return SimpleNamespace(major=major, minor=minor, micro=micro, releaselevel=releaselevel) + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +@pytest.mark.parametrize( + ("version", "status"), + [ + # First fixed release and anything later in the same series passes. + ((3, 11, 18), "ok"), + ((3, 11, 19), "ok"), + ((3, 12, 16), "ok"), + ((3, 13, 18), "ok"), + ((3, 14, 10), "ok"), + ((3, 14, 11), "ok"), + ((3, 15, 2), "ok"), + ((3, 15, 3), "ok"), + # The patch release before the fix fails. + ((3, 11, 17), "fail"), + ((3, 12, 15), "fail"), + ((3, 13, 17), "fail"), + ((3, 14, 9), "fail"), + ((3, 15, 1), "fail"), + ((3, 15, 0), "fail"), + # Series without any fixed release. + ((3, 10, 99), "fail"), + ((3, 9, 25), "fail"), + ], +) +def test_classify_final_releases(version: tuple[int, int, int], status: str) -> None: + assert check_python_patched.classify(_version(*version))[0] == status + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_classify_prerelease_of_fixed_series_fails() -> None: + assert check_python_patched.classify(_version(3, 15, 2, "candidate"))[0] == "fail" + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_classify_newer_series_warns() -> None: + assert check_python_patched.classify(_version(3, 16, 0, "alpha"))[0] == "warn" + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_script_prints_sys_version_and_matches_classification() -> None: + result = subprocess.run( # noqa: S603 + [sys.executable, str(SCRIPT_PATH)], capture_output=True, text=True, check=False + ) + assert f"sys.version: {sys.version}" in result.stdout + expected_status, _ = check_python_patched.classify(sys.version_info) + assert (result.returncode == 1) == (expected_status == "fail") + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_harden_pip_action_runs_the_check_before_installing_pip() -> None: + text = (ACTION_DIR / "action.yml").read_text(encoding="utf-8") + check = text.index("check_python_patched.py") + install = text.index("pip install") + assert check < install diff --git a/ruff.toml b/ruff.toml index 5a2778cfb80..5b01f55bf09 100644 --- a/ruff.toml +++ b/ruff.toml @@ -158,6 +158,7 @@ inline-quotes = "double" "qa/**" = ["T201"] "toolshed/**" = ["T201"] "ci/**" = ["T201"] +".github/actions/**" = ["T201"] "**/build_hooks.py" = ["T201"] "**/_build_shared.py" = ["T201"] "**/docs/**/conf.py" = ["T201", "ARG001"] From fa74283656ba5cf07eff1475fc957f6a9113a132 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 15:40:04 -0700 Subject: [PATCH 2/9] =?UTF-8?q?CI:=20simplify=20harden-pip=20=E2=80=94=20d?= =?UTF-8?q?rop=20hash-pinned=20bootstrap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that check_python_patched.py confirms the interpreter carries the ssl-module fix (CVE-2026-115557), the hash-pinned pip download is no longer needed for security: Python's own ssl rejects any MITM'd cert, so a plain 'pip install -U pip' is safe. Replace the pinned-URL step with 'pip install -U pip' and update the action description accordingly. The python input and all 13 call sites are unchanged. --- .github/actions/harden-pip/action.yml | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/.github/actions/harden-pip/action.yml b/.github/actions/harden-pip/action.yml index f4ccb7b0fb1..39ae733d925 100644 --- a/.github/actions/harden-pip/action.yml +++ b/.github/actions/harden-pip/action.yml @@ -4,15 +4,12 @@ name: Harden pip description: >- - Install the security-fixed pip (26.2.2) from a hash-pinned wheel so that later - pip invocations do not fall back on the vendored certifi bundle that trusts the - compromised eMudhra/emSign intermediate CAs. The sha256 fragment guarantees the - bootstrap download's integrity even if the bootstrapping pip's trust store is - compromised. See https://github.com/NVIDIA/cuda-python/issues/3195 and - https://discuss.python.org/t/109576. Before that, print sys.version and fail - if the interpreter is older than the first CPython release of its minor - series whose ssl module rejects the compromised intermediate keys (3.11.18, - 3.12.16, 3.13.18, 3.14.10, 3.15.2). + Verify the CPython interpreter carries the eMudhra/emSign CA fix in its ssl + module (3.11.18+, 3.12.16+, 3.13.18+, 3.14.10+, 3.15.2+; CVE-2026-115557), + then update pip. The version check guarantees pip's TLS connections use the + patched ssl module, so a hash-pinned bootstrap is no longer necessary. + See https://github.com/NVIDIA/cuda-python/issues/3195 and + https://discuss.python.org/t/109678. inputs: python: @@ -31,10 +28,9 @@ runs: run: | "${HARDEN_PIP_PYTHON}" "${GITHUB_ACTION_PATH}/check_python_patched.py" - - name: Install hash-pinned pip 26.2.2 + - name: Update pip shell: bash --noprofile --norc -xeuo pipefail {0} env: HARDEN_PIP_PYTHON: ${{ inputs.python }} run: | - "${HARDEN_PIP_PYTHON}" -m pip install \ - "pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70" + "${HARDEN_PIP_PYTHON}" -m pip install -U pip From 4db603eb708b53256e57f99094afd01c6889d7f3 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 15:48:26 -0700 Subject: [PATCH 3/9] CI: force setup-python to resolve latest patch in pre-commit The harden-pip interpreter guard rejected the pre-commit job's Python 3.14.9: with a minor-only spec and the default check-latest: false, setup-python reused the hosted image's cached 3.14.9, which predates the eMudhra ssl fix (3.14.10). Set check-latest: true so it resolves the patched patch release from the manifest. Cache-key prefix logic is unchanged (still keyed on the resolved version / PYTHON_VERSION minor). --- .github/workflows/pre-commit.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index eaa6da7368b..91cbecef0b0 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -66,6 +66,10 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ env.PYTHON_VERSION }} + # Resolve from the manifest, not a stale tool cache: with a minor-only + # spec setup-python otherwise reuses a cached patch that may predate + # the eMudhra ssl fix (e.g. 3.14.9), which harden-pip then rejects. + check-latest: true - name: Set pre-commit cache location run: echo "PRE_COMMIT_HOME=${RUNNER_TEMP}/pre-commit" >> "${GITHUB_ENV}" From 9a474d520940b3ba8debae5b5100279aaa4ffecc Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 15:56:44 -0700 Subject: [PATCH 4/9] CI: enforce patched Python floor in setup-python; drop guard script With the setup-python call sites floored to the first patched release of each minor series, the ssl module is guaranteed clean before harden-pip runs, making the inline runtime check (check_python_patched.py) and its unit test redundant. Drop both, revert the ruff.toml T201 exception that accompanied them, and replace the check-latest workaround with explicit tilde-range floor specs. pre-commit 3.14 -> ~3.14.10 release 3.12 -> ~3.12.16 test-sdist-linux 3.12 -> ~3.12.16 test-sdist-win 3.12 -> ~3.12.16 build-wheel:112 3.12 -> ~3.12.16 (cibuildwheel host python) setup-python resolves a tilde-range against the manifest even when a stale cached patch does not satisfy the floor, so no check-latest is needed. CVE-2026-115557. --- .github/actions/harden-pip/action.yml | 18 ++-- .../harden-pip/check_python_patched.py | 79 ------------------ .github/workflows/build-wheel.yml | 2 +- .github/workflows/pre-commit.yml | 11 ++- .github/workflows/release.yml | 2 +- .github/workflows/test-sdist-linux.yml | 2 +- .github/workflows/test-sdist-windows.yml | 2 +- ci/tools/tests/test_check_python_patched.py | 83 ------------------- ruff.toml | 1 - 9 files changed, 14 insertions(+), 186 deletions(-) delete mode 100644 .github/actions/harden-pip/check_python_patched.py delete mode 100644 ci/tools/tests/test_check_python_patched.py diff --git a/.github/actions/harden-pip/action.yml b/.github/actions/harden-pip/action.yml index 39ae733d925..52b9835d581 100644 --- a/.github/actions/harden-pip/action.yml +++ b/.github/actions/harden-pip/action.yml @@ -4,10 +4,11 @@ name: Harden pip description: >- - Verify the CPython interpreter carries the eMudhra/emSign CA fix in its ssl - module (3.11.18+, 3.12.16+, 3.13.18+, 3.14.10+, 3.15.2+; CVE-2026-115557), - then update pip. The version check guarantees pip's TLS connections use the - patched ssl module, so a hash-pinned bootstrap is no longer necessary. + Update pip. Call sites must use setup-python with a floor spec that ensures + the interpreter's ssl module rejects the compromised eMudhra/emSign CA keys + (CVE-2026-115557; first fixed releases: 3.11.18, 3.12.16, 3.13.18, 3.14.10, + 3.15.2). With a patched ssl module, pip install -U pip is safe and picks up + the latest pip, which uses truststore (the OS certificate store) by default. See https://github.com/NVIDIA/cuda-python/issues/3195 and https://discuss.python.org/t/109678. @@ -19,15 +20,6 @@ inputs: runs: using: composite steps: - - name: Check Python interpreter is patched - shell: bash --noprofile --norc -xeuo pipefail {0} - env: - HARDEN_PIP_PYTHON: ${{ inputs.python }} - # The script lives next to this action so that it matches the action's - # commit even when the action is referenced from another checkout. - run: | - "${HARDEN_PIP_PYTHON}" "${GITHUB_ACTION_PATH}/check_python_patched.py" - - name: Update pip shell: bash --noprofile --norc -xeuo pipefail {0} env: diff --git a/.github/actions/harden-pip/check_python_patched.py b/.github/actions/harden-pip/check_python_patched.py deleted file mode 100644 index 51c16a6a022..00000000000 --- a/.github/actions/harden-pip/check_python_patched.py +++ /dev/null @@ -1,79 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -"""Fail when the running CPython is older than the fix for the eMudhra/emSign CA compromise. - -Print ``sys.version`` and compare the interpreter with the first release of its -minor series whose ``ssl`` module rejects the compromised eMudhra intermediate -keys (CVE-2026-115557). Any release at or after that patch release passes. - -Release announcement: -https://discuss.python.org/t/python-3-15-2-3-14-10-3-13-18-3-12-16-and-3-11-18-are-out/109678 - -This file uses only the standard library because it runs on whichever -interpreter the job is about to use, before any package is installed. -""" - -from __future__ import annotations - -import sys - -# Minor series -> first patch release carrying the fix. -FIXED_MICRO = { - (3, 11): 18, - (3, 12): 16, - (3, 13): 18, - (3, 14): 10, - (3, 15): 2, -} - - -def classify(version_info) -> tuple[str, str]: - """Return ``(status, detail)`` where status is ``"ok"``, ``"warn"`` or ``"fail"``.""" - series = (version_info.major, version_info.minor) - found = f"{version_info.major}.{version_info.minor}.{version_info.micro}" - - if series in FIXED_MICRO: - fixed = f"{series[0]}.{series[1]}.{FIXED_MICRO[series]}" - if version_info.releaselevel != "final": - return ( - "fail", - f"Python {found} ({version_info.releaselevel}) is a pre-release; use a final release >= {fixed}.", - ) - if version_info.micro >= FIXED_MICRO[series]: - return "ok", f"Python {found} includes the fix (first fixed release of this series: {fixed})." - return ( - "fail", - f"Python {found} is older than {fixed}, the first release of this series that rejects the compromised eMudhra intermediate CA keys.", - ) - - if series > max(FIXED_MICRO): - # Newer than every series we know a fix for (for example 3.16 pre-releases). - return ( - "warn", - f"Python {found}: no fixed release is known for the {series[0]}.{series[1]} series yet; update FIXED_MICRO once one exists.", - ) - - return ( - "fail", - f"Python {series[0]}.{series[1]} has no release that rejects the compromised eMudhra intermediate CA keys.", - ) - - -def main() -> int: - print(f"sys.version: {sys.version}") - print(f"sys.executable: {sys.executable}") - status, detail = classify(sys.version_info) - if status == "ok": - print(detail) - return 0 - if status == "warn": - print(f"::warning title=Python patch level unverified::{detail}") - return 0 - print(f"::error title=Unpatched Python interpreter::{detail}") - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 1daf83eed9b..862c1df23a8 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -109,7 +109,7 @@ jobs: with: # WAR: setup-python is not relocatable, and cibuildwheel hard-wires to 3.12... # see https://github.com/actions/setup-python/issues/871 - python-version: "3.12" + python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix (CVE-2026-115557) architecture: ${{ ((inputs.host-platform == 'linux-aarch64' || inputs.host-platform == 'win-arm64') && 'arm64') || 'x64' }} - name: Harden pip diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 91cbecef0b0..059b61f1227 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -46,7 +46,7 @@ jobs: # install a Git commit hook. All other hooks, including Lychee, run. SKIP: check-precommit-installed PYTHONUTF8: "1" - # Minor version used by setup-python and as the cache restore-key prefix. + # Minor version used as the cache restore-key prefix (see setup-python step). PYTHON_VERSION: "3.14" steps: - name: Enable Git symlinks (Windows, must precede checkout) @@ -65,11 +65,10 @@ jobs: id: python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: ${{ env.PYTHON_VERSION }} - # Resolve from the manifest, not a stale tool cache: with a minor-only - # spec setup-python otherwise reuses a cached patch that may predate - # the eMudhra ssl fix (e.g. 3.14.9), which harden-pip then rejects. - check-latest: true + # Floor spec ensures the interpreter's ssl module rejects the compromised + # eMudhra keys (CVE-2026-115557). PYTHON_VERSION stays minor-only ("3.14") + # for the cache restore-key prefix below. + python-version: "~3.14.10" - name: Set pre-commit cache location run: echo "PRE_COMMIT_HOME=${RUNNER_TEMP}/pre-commit" >> "${GITHUB_ENV}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index acefd058a58..12cc2d26a3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -179,7 +179,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix (CVE-2026-115557) - name: Check versioned release notes exist id: release-notes diff --git a/.github/workflows/test-sdist-linux.yml b/.github/workflows/test-sdist-linux.yml index 124a61966cb..b104b76a11d 100644 --- a/.github/workflows/test-sdist-linux.yml +++ b/.github/workflows/test-sdist-linux.yml @@ -49,7 +49,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix (CVE-2026-115557) - name: Harden pip uses: ./.github/actions/harden-pip diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml index 00a2c07041a..4293a220c87 100644 --- a/.github/workflows/test-sdist-windows.yml +++ b/.github/workflows/test-sdist-windows.yml @@ -58,7 +58,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix (CVE-2026-115557) - name: Harden pip uses: ./.github/actions/harden-pip diff --git a/ci/tools/tests/test_check_python_patched.py b/ci/tools/tests/test_check_python_patched.py deleted file mode 100644 index 4494804e626..00000000000 --- a/ci/tools/tests/test_check_python_patched.py +++ /dev/null @@ -1,83 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Tests for the interpreter patch-level gate used by the harden-pip action.""" - -from __future__ import annotations - -import importlib.util -import subprocess -import sys -from pathlib import Path -from types import SimpleNamespace - -import pytest - -ACTION_DIR = Path(__file__).resolve().parents[3] / ".github" / "actions" / "harden-pip" -SCRIPT_PATH = ACTION_DIR / "check_python_patched.py" - -_spec = importlib.util.spec_from_file_location("check_python_patched", SCRIPT_PATH) -assert _spec is not None and _spec.loader is not None -check_python_patched = importlib.util.module_from_spec(_spec) -_spec.loader.exec_module(check_python_patched) - - -def _version(major: int, minor: int, micro: int, releaselevel: str = "final") -> SimpleNamespace: - return SimpleNamespace(major=major, minor=minor, micro=micro, releaselevel=releaselevel) - - -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -@pytest.mark.parametrize( - ("version", "status"), - [ - # First fixed release and anything later in the same series passes. - ((3, 11, 18), "ok"), - ((3, 11, 19), "ok"), - ((3, 12, 16), "ok"), - ((3, 13, 18), "ok"), - ((3, 14, 10), "ok"), - ((3, 14, 11), "ok"), - ((3, 15, 2), "ok"), - ((3, 15, 3), "ok"), - # The patch release before the fix fails. - ((3, 11, 17), "fail"), - ((3, 12, 15), "fail"), - ((3, 13, 17), "fail"), - ((3, 14, 9), "fail"), - ((3, 15, 1), "fail"), - ((3, 15, 0), "fail"), - # Series without any fixed release. - ((3, 10, 99), "fail"), - ((3, 9, 25), "fail"), - ], -) -def test_classify_final_releases(version: tuple[int, int, int], status: str) -> None: - assert check_python_patched.classify(_version(*version))[0] == status - - -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_classify_prerelease_of_fixed_series_fails() -> None: - assert check_python_patched.classify(_version(3, 15, 2, "candidate"))[0] == "fail" - - -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_classify_newer_series_warns() -> None: - assert check_python_patched.classify(_version(3, 16, 0, "alpha"))[0] == "warn" - - -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_script_prints_sys_version_and_matches_classification() -> None: - result = subprocess.run( # noqa: S603 - [sys.executable, str(SCRIPT_PATH)], capture_output=True, text=True, check=False - ) - assert f"sys.version: {sys.version}" in result.stdout - expected_status, _ = check_python_patched.classify(sys.version_info) - assert (result.returncode == 1) == (expected_status == "fail") - - -@pytest.mark.agent_authored(model="claude-sonnet-5.5") -def test_harden_pip_action_runs_the_check_before_installing_pip() -> None: - text = (ACTION_DIR / "action.yml").read_text(encoding="utf-8") - check = text.index("check_python_patched.py") - install = text.index("pip install") - assert check < install diff --git a/ruff.toml b/ruff.toml index 5b01f55bf09..5a2778cfb80 100644 --- a/ruff.toml +++ b/ruff.toml @@ -158,7 +158,6 @@ inline-quotes = "double" "qa/**" = ["T201"] "toolshed/**" = ["T201"] "ci/**" = ["T201"] -".github/actions/**" = ["T201"] "**/build_hooks.py" = ["T201"] "**/_build_shared.py" = ["T201"] "**/docs/**/conf.py" = ["T201", "ARG001"] From 0734199b42d107e48fb60049b8660e4d777c83e1 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 16:16:43 -0700 Subject: [PATCH 5/9] CI: floor Python in the remaining harden-pip jobs Cover the sites the tilde-range sweep missed: - release.yml verify jobs (shared anchor): add setup-python ~3.12.16 before the venv is created, so python3 -m venv inherits a patched ssl module. These jobs have no checkout and build the venv from the system python3 otherwise. - ci-nightly.yml test-ci-tools-for-release: add setup-python ~3.12.16 before harden-pip (the job previously ran on the system interpreter). - coverage.yml: add PY_VER_FLOOR (~3.14.10) and use it as the setup-python spec in all four jobs; PY_VER stays minor-only for env-vars/naming. CVE-2026-115557. --- .github/workflows/ci-nightly.yml | 5 +++++ .github/workflows/coverage.yml | 12 ++++++++---- .github/workflows/release.yml | 8 ++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index d76e9a9cb7b..882f130373b 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -58,6 +58,11 @@ jobs: with: fetch-depth: 1 + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix (CVE-2026-115557) + - name: Harden pip uses: ./.github/actions/harden-pip diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7e36e47587b..afe1cd2741d 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -11,6 +11,10 @@ on: env: PY_VER: "3.14" + # Floor spec for setup-python: first 3.14 whose ssl module rejects the + # compromised eMudhra keys (CVE-2026-115557). PY_VER stays minor-only for + # env-vars/naming; PY_VER_FLOOR is used only as the setup-python spec. + PY_VER_FLOOR: "~3.14.10" LOCAL_CTK: "1" GPU: "a100" DRIVER: "latest" @@ -104,7 +108,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: ${{ env.PY_VER }} + python-version: ${{ env.PY_VER_FLOOR }} env: # we use self-hosted runners on which setup-python behaves weirdly... AGENT_TOOLSDIRECTORY: "/opt/hostedtoolcache" @@ -252,7 +256,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: ${{ env.PY_VER }} + python-version: ${{ env.PY_VER_FLOOR }} - name: Set up MSVC uses: step-security/msvc-dev-cmd@22c98154b708dbd743e6f27a933cf6ceba3305c4 # v1.13.1 @@ -391,7 +395,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: ${{ env.PY_VER }} + python-version: ${{ env.PY_VER_FLOOR }} - name: Set up mini CTK uses: ./.github/actions/fetch_ctk @@ -501,7 +505,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: ${{ env.PY_VER }} + python-version: ${{ env.PY_VER_FLOOR }} - name: Harden pip uses: ./.github/actions/harden-pip diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12cc2d26a3c..d0e8ec5893a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -342,6 +342,14 @@ jobs: EXTRA_INDEX_URL: https://pypi.org/simple/ INDEX_NAME: TestPyPI steps: &verify-pathfinder-steps + - name: Set up Python + # Floor the interpreter so the venv below inherits an ssl module that + # rejects the compromised eMudhra keys (CVE-2026-115557). setup-python + # puts its shim first on PATH, so `python3 -m venv` uses it. + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix + - name: Create verification environment run: | [[ -n "$VERSION" ]] || { echo "::error::Empty version output"; exit 1; } From 0e852e62270b03dc761629ef1ab5ee991deb6f9c Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 16:24:02 -0700 Subject: [PATCH 6/9] CI: resolve latest Python patch in matrix setup-python steps The matrix jobs in test-wheel-linux, test-wheel-windows and build-wheel run setup-python with matrix.PY_VER across 3.11 through 3.15t, each with a different first-fixed patch, so a single tilde-range floor cannot cover them. Set check-latest: true so setup-python resolves the newest patch from the manifest instead of a stale tool-cache entry; every series in the matrix has a fixed release, so the newest patch always carries the eMudhra ssl fix (CVE-2026-115557). --- .github/workflows/build-wheel.yml | 4 ++++ .github/workflows/test-wheel-linux.yml | 4 ++++ .github/workflows/test-wheel-windows.yml | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 862c1df23a8..7be070b1e03 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -445,6 +445,10 @@ jobs: with: python-version: ${{ matrix.python-version }} allow-prereleases: ${{ startsWith(matrix.python-version, '3.16') }} + # Resolve the newest patch from the manifest rather than a stale tool + # cache; every matrix series' newest patch carries the eMudhra ssl fix + # (CVE-2026-115557), which a single floor spec cannot express here. + check-latest: true - name: Harden pip if: ${{ env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }} diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 0582901a975..d4309eb6645 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -338,6 +338,10 @@ jobs: with: python-version: ${{ matrix.PY_VER }} allow-prereleases: ${{ startsWith(matrix.PY_VER, '3.16') }} + # Resolve the newest patch from the manifest rather than a stale tool + # cache; every matrix series' newest patch carries the eMudhra ssl fix + # (CVE-2026-115557), which a single floor spec cannot express here. + check-latest: true env: # we use self-hosted runners on which setup-python behaves weirdly (Python include can't be found)... AGENT_TOOLSDIRECTORY: "/opt/hostedtoolcache" diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 375dae14ef4..0906cb69f5a 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -320,6 +320,10 @@ jobs: with: python-version: ${{ matrix.PY_VER }} allow-prereleases: ${{ startsWith(matrix.PY_VER, '3.16') }} + # Resolve the newest patch from the manifest rather than a stale tool + # cache; every matrix series' newest patch carries the eMudhra ssl fix + # (CVE-2026-115557), which a single floor spec cannot express here. + check-latest: true - name: Harden pip # $/ resolves at the workflow's commit. ./ would resolve against the From cb8baed9bf1c66aa5ee19a5fd8229f771d937ced Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 16:38:48 -0700 Subject: [PATCH 7/9] CI: use check-latest instead of tilde-range for Windows setup-python ~3.12.16 resolves correctly on Linux but fails on Windows because the Windows python-versions manifest lags behind Linux when a security batch ships. Switch build-wheel (cibuildwheel host) and test-sdist-windows to check-latest: true with the minor-only spec so they resolve the newest available patch on any OS, consistent with the matrix setup-python steps. --- .github/workflows/build-wheel.yml | 5 ++++- .github/workflows/test-sdist-windows.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 7be070b1e03..d8fdd95b0a1 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -109,7 +109,10 @@ jobs: with: # WAR: setup-python is not relocatable, and cibuildwheel hard-wires to 3.12... # see https://github.com/actions/setup-python/issues/871 - python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix (CVE-2026-115557) + python-version: "3.12" + # Resolve newest patch from manifest (not stale tool cache); tilde-range + # (~3.12.16) is not used because the Windows manifest may lag behind Linux. + check-latest: true architecture: ${{ ((inputs.host-platform == 'linux-aarch64' || inputs.host-platform == 'win-arm64') && 'arm64') || 'x64' }} - name: Harden pip diff --git a/.github/workflows/test-sdist-windows.yml b/.github/workflows/test-sdist-windows.yml index 4293a220c87..fce182f679d 100644 --- a/.github/workflows/test-sdist-windows.yml +++ b/.github/workflows/test-sdist-windows.yml @@ -58,7 +58,10 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "~3.12.16" # floor: first 3.12 with eMudhra ssl fix (CVE-2026-115557) + python-version: "3.12" + # Resolve newest patch from manifest (not stale tool cache); tilde-range + # (~3.12.16) is not used because the Windows manifest may lag behind Linux. + check-latest: true - name: Harden pip uses: ./.github/actions/harden-pip From dd043cd872e452e5771444d82c64844190003a5a Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 19:31:12 -0700 Subject: [PATCH 8/9] CI: reinstate interpreter guard inline in harden-pip Re-add the version check dropped in the earlier simplification commit, this time inlined in action.yml rather than in a separate .py file, so no additional source file or test is needed. The guard fails the step (and the job) when the interpreter predates the first release of its minor series that rejects the compromised eMudhra CA keys (CVE-2026-115557), emitting a ::error:: workflow annotation. This surfaces the Windows python-versions manifest lag explicitly (e.g. 3.12.10 instead of 3.12.16) so it shows as a red job rather than a silent unpatched pip upgrade. --- .github/actions/harden-pip/action.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/actions/harden-pip/action.yml b/.github/actions/harden-pip/action.yml index 52b9835d581..cc6b6894a4a 100644 --- a/.github/actions/harden-pip/action.yml +++ b/.github/actions/harden-pip/action.yml @@ -20,6 +20,31 @@ inputs: runs: using: composite steps: + - name: Check Python interpreter is patched (CVE-2026-115557) + shell: bash --noprofile --norc -euo pipefail {0} + env: + HARDEN_PIP_PYTHON: ${{ inputs.python }} + run: | + "${HARDEN_PIP_PYTHON}" -c ' + import sys + v = sys.version_info + print("sys.version:", sys.version) + FIXED = {(3,11):18,(3,12):16,(3,13):18,(3,14):10,(3,15):2} + s = (v.major, v.minor) + p = str(v.major)+"."+str(v.minor)+"."+str(v.micro) + if s in FIXED: + fl = str(v.major)+"."+str(v.minor)+"."+str(FIXED[s]) + if v.micro < FIXED[s]: + print("::error::Python "+p+" is older than "+fl+", the first "+str(v.major)+"."+str(v.minor)+" release that rejects the compromised eMudhra CA keys (CVE-2026-115557).") + sys.exit(1) + print("Python "+p+": ssl module carries the fix (first fixed: "+fl+").") + elif s > max(FIXED): + print("::warning::Python "+p+": no fixed release known for "+str(v.major)+"."+str(v.minor)+" yet.") + else: + print("::error::Python "+str(v.major)+"."+str(v.minor)+" has no release that rejects the compromised eMudhra CA keys (CVE-2026-115557).") + sys.exit(1) + ' + - name: Update pip shell: bash --noprofile --norc -xeuo pipefail {0} env: From 680f0dc2d47aa8a6c4ced63b28e857c805dbc000 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Wed, 28 Oct 2026 19:40:57 -0700 Subject: [PATCH 9/9] CI: floor Python to ~3.14.10 in lychee.yml and build-docs.yml Both workflows pass their Python interpreter to lychee-links, which calls harden-pip. Neither had a patched-version floor, and the docs CI run (job 123173870397) confirmed 3.14.9 was resolved before the simplified pip upgrade ran (no hash-pinned bootstrap to compensate). Apply the same ~3.14.10 tilde-range floor used in pre-commit.yml: - lychee.yml: inline the floor in the python-version field; PYTHON_VERSION stays minor-only ("3.14") for the cache restore-key prefix. - build-docs.yml: direct literal, no cache interaction to preserve. 3.14.10 is present in the manifest for all runner platforms, so the tilde-range resolves cleanly. Raised by rwgk in review. --- .github/workflows/build-docs.yml | 2 +- .github/workflows/lychee.yml | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml index e28fbd5dd87..ccfd8e280ff 100644 --- a/.github/workflows/build-docs.yml +++ b/.github/workflows/build-docs.yml @@ -307,7 +307,7 @@ jobs: if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.14" + python-version: "~3.14.10" # floor: first 3.14 with eMudhra ssl fix (CVE-2026-115557) - name: Check rendered documentation links through pre-commit if: ${{ !inputs.is-release && startsWith(github.ref_name, 'pull-request/') }} diff --git a/.github/workflows/lychee.yml b/.github/workflows/lychee.yml index e73df49a34b..6549086fd73 100644 --- a/.github/workflows/lychee.yml +++ b/.github/workflows/lychee.yml @@ -53,7 +53,7 @@ jobs: kind: [authored, rendered] env: PYTHONUTF8: "1" - # Reuse the same minor version for Python setup and cache fallback. + # Minor version used as the cache restore-key prefix (see setup-python step). PYTHON_VERSION: "3.14" steps: - name: Checkout checked revision @@ -70,7 +70,10 @@ jobs: id: python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: ${{ env.PYTHON_VERSION }} + # Floor spec ensures the interpreter's ssl module rejects the compromised + # eMudhra keys (CVE-2026-115557). PYTHON_VERSION stays minor-only ("3.14") + # for the cache restore-key prefix below. + python-version: "~3.14.10" - name: Set pre-commit cache location run: echo "PRE_COMMIT_HOME=${RUNNER_TEMP}/pre-commit" >> "${GITHUB_ENV}"