Skip to content
Draft
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: 13 additions & 1 deletion .github/actions/harden-pip/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
79 changes: 79 additions & 0 deletions .github/actions/harden-pip/check_python_patched.py
Original file line number Diff line number Diff line change
@@ -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())
83 changes: 83 additions & 0 deletions ci/tools/tests/test_check_python_patched.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
Loading