From 2b1b87215ba9a7e115efc8bf33bd82883e5a41bb Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Thu, 5 Nov 2026 15:14:04 -0800 Subject: [PATCH 1/6] CI: Use the matrix CTK's compute-sanitizer instead of the latest release Fetch cuda_sanitizer_api from the same pinned redistrib manifest as the rest of the mini CTK, rather than from whatever release ci/tools/guess_latest.sh scrapes from the redist directory listing. Remove guess_latest.sh, which has no other users. --- .github/workflows/test-wheel-linux.yml | 4 +-- ci/tools/compute_ci_plan.py | 1 - ci/tools/env-vars | 1 - ci/tools/guess_latest.sh | 42 -------------------------- ci/tools/tests/test_compute_ci_plan.py | 1 - 5 files changed, 2 insertions(+), 47 deletions(-) delete mode 100644 ci/tools/guess_latest.sh diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index c2c7523b68c..4b35c689a05 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -361,13 +361,13 @@ jobs: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ matrix.CUDA_VER }} - - name: Set up latest cuda_sanitizer_api + - name: Set up cuda_sanitizer_api if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true') && env.SETUP_SANITIZER == '1' }} uses: ./.github/actions/fetch_ctk continue-on-error: false with: host-platform: ${{ inputs.host-platform }} - cuda-version: ${{ env.LATEST_CUDA_VERSION }} + cuda-version: ${{ matrix.CUDA_VER }} cuda-components: "cuda_sanitizer_api" - name: Set up compute-sanitizer diff --git a/ci/tools/compute_ci_plan.py b/ci/tools/compute_ci_plan.py index 51493044569..1ab4b2961b9 100644 --- a/ci/tools/compute_ci_plan.py +++ b/ci/tools/compute_ci_plan.py @@ -56,7 +56,6 @@ ".github/workflows/test-wheel-linux.yml": "linux", ".github/workflows/test-wheel-windows.yml": "windows", "ci/tools/configure_driver_mode.ps1": "windows", - "ci/tools/guess_latest.sh": "linux", "ci/tools/install_gpu_driver.ps1": "windows", "ci/tools/install_gpu_driver.sh": "linux", "ci/tools/setup-sanitizer": "linux", diff --git a/ci/tools/env-vars b/ci/tools/env-vars index 262de423065..9946ad549de 100755 --- a/ci/tools/env-vars +++ b/ci/tools/env-vars @@ -119,7 +119,6 @@ elif [[ "${1}" == "test" ]]; then # We only test compute-sanitizer on python 3.12 arbitrarily; we don't need to use sanitizer on the entire matrix # Only local ctk installs have compute-sanitizer; there is no wheel for it if [[ "${PY_VER}" == "3.12" && "${CUDA_VER}" != "11.8.0" && "${LOCAL_CTK}" == 1 && "${HOST_PLATFORM}" == linux* ]]; then - echo "LATEST_CUDA_VERSION=$(bash ci/tools/guess_latest.sh $TEST_CUDA_MAJOR)" >> $GITHUB_ENV SETUP_SANITIZER=1 else SETUP_SANITIZER=0 diff --git a/ci/tools/guess_latest.sh b/ci/tools/guess_latest.sh deleted file mode 100644 index f9d383f2094..00000000000 --- a/ci/tools/guess_latest.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# -# SPDX-License-Identifier: Apache-2.0 - -# URL to search -URL="https://developer.download.nvidia.com/compute/cuda/redist/" - -# Ensure exactly one argument is provided -if [ "$#" -ne 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -# Accept major version as the first argument -MAJOR_VERSION="$1" - -# Fetch the directory listing and extract the latest version number -get_latest_version() { - # Get the HTML content of the page - local html_content=$(wget -q -O - "$URL") - - # Extract links matching the pattern redistrib_?.?.?.json - local files=$(echo "$html_content" | grep -oP "redistrib_${MAJOR_VERSION}\.[0-9]+\.[0-9]+\.json" | cut -d'"' -f2) - - # If files were found, extract the version numbers and find the latest - if [ -n "$files" ]; then - # Extract just the version numbers using regex - local versions=$(echo "$files" | grep -oP "redistrib_\K${MAJOR_VERSION}\.[0-9]+\.[0-9]+(?=\.json)") - - # Sort the versions and get the latest - local latest_version=$(echo "$versions" | sort -V | tail -n 1) - echo "$latest_version" - else - echo "No files matching the pattern were found." - return 1 - fi -} - -# Call the function and store the result -latest_version=$(get_latest_version) -echo $latest_version diff --git a/ci/tools/tests/test_compute_ci_plan.py b/ci/tools/tests/test_compute_ci_plan.py index b87326420ee..c08eddb6dcd 100644 --- a/ci/tools/tests/test_compute_ci_plan.py +++ b/ci/tools/tests/test_compute_ci_plan.py @@ -85,7 +85,6 @@ def test_test_infrastructure_platforms(self) -> None: ".github/workflows/test-wheel-linux.yml": {"linux"}, ".github/workflows/test-wheel-windows.yml": {"windows"}, "ci/tools/configure_driver_mode.ps1": {"windows"}, - "ci/tools/guess_latest.sh": {"linux"}, "ci/tools/install_gpu_driver.ps1": {"windows"}, "ci/tools/install_gpu_driver.sh": {"linux"}, "ci/tools/setup-sanitizer": {"linux"}, From 241ef09518d1d0aab9e3c8de046cecf6065f24d9 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Thu, 5 Nov 2026 15:31:04 -0800 Subject: [PATCH 2/6] CI: Fail closed on unpinned redistrib manifests verify-metadata used to warn and pass when a public-server CUDA version had no pinned manifest hash, so the 13.4.2 -> 13.5.0 bump ran unpinned. Fail closed instead, with an explicit fetch_ctk allow-unpinned input (and --allow-unpinned flag) as the opt-out; it never overrides a mismatch against an existing pin. Manifests from an alternate redist server stay warn-only. Pin 13.5.0 and add a test that every CUDA version in ci/versions.yml and every LOCAL_CTK row of ci/test-matrix.yml is pinned. --- .github/actions/fetch_ctk/action.yml | 15 ++++- ci/tools/fetch_ctk_redistrib.py | 36 ++++++++---- ci/tools/tests/test_fetch_ctk_redistrib.py | 64 ++++++++++++++++++++-- 3 files changed, 99 insertions(+), 16 deletions(-) diff --git a/.github/actions/fetch_ctk/action.yml b/.github/actions/fetch_ctk/action.yml index 65b05a66acf..9931403f8b0 100644 --- a/.github/actions/fetch_ctk/action.yml +++ b/.github/actions/fetch_ctk/action.yml @@ -26,6 +26,13 @@ inputs: Override to install from an alternate redistrib server. required: false default: "https://developer.download.nvidia.com/compute/cuda/redist" + allow-unpinned: + description: > + Set to "true" to accept a public-server CUDA version whose manifest hash is + not pinned in ci/tools/fetch_ctk_redistrib.py (warn instead of fail). + A mismatch against an existing pin always fails. + required: false + default: "false" runs: using: composite @@ -63,10 +70,16 @@ runs: else METADATA_SOURCE=custom fi + case "${{ inputs.allow-unpinned }}" in + true) ALLOW_UNPINNED_FLAG=--allow-unpinned ;; + false) ALLOW_UNPINNED_FLAG= ;; + *) echo "ERROR: allow-unpinned must be 'true' or 'false'" >&2; exit 1 ;; + esac python "$CTK_REDIST_TOOL" verify-metadata \ --cuda-version "${{ inputs.cuda-version }}" \ --metadata-path "$CTK_JSON_FILE" \ - --metadata-source "$METADATA_SOURCE" + --metadata-source "$METADATA_SOURCE" \ + ${ALLOW_UNPINNED_FLAG} # Pre-process the component list to ensure hash uniqueness CTK_CACHE_COMPONENTS="$(python "$CTK_REDIST_TOOL" filter-components \ diff --git a/ci/tools/fetch_ctk_redistrib.py b/ci/tools/fetch_ctk_redistrib.py index 1a75ed471a8..a6ad3e4116e 100644 --- a/ci/tools/fetch_ctk_redistrib.py +++ b/ci/tools/fetch_ctk_redistrib.py @@ -29,10 +29,11 @@ # Known-good sha256 of the redistrib metadata JSON, keyed by CUDA version. # Per-archive verification is only trustworthy if the JSON that carries the -# archive hashes is itself trusted, so we pin the metadata too. A version that -# is absent here is not blocked -- failing closed would break CI on every new -# CTK -- but `verify-metadata` emits a warning; add the version's hash below as -# it enters the matrix to pin it. Regenerate with: +# archive hashes is itself trusted, so we pin the metadata too. `verify-metadata` +# fails on a public-server version that is absent here unless the caller opts +# out with `--allow-unpinned`. Add a version's hash below in the same change +# that adds it to ci/versions.yml or a LOCAL_CTK row of ci/test-matrix.yml; +# tests/test_fetch_ctk_redistrib.py enforces this. Regenerate with: # curl -fsSL \ # https://developer.download.nvidia.com/compute/cuda/redist/redistrib_.json \ # | sha256sum @@ -41,6 +42,7 @@ "12.9.1": "8335301010b0023ee1ff61eb11e2600ca62002d76780de4089011ad77e0c7630", "13.0.2": "fce66717a81c510ffeb89ecc3e79849ab34af3b80139f750876d9033e31d71c2", "13.4.2": "015b97aa3977d4d0bade1631b226b6d4cbe48751cc841aad9193295df7f9047a", + "13.5.0": "401529e951b845788177f999c0756c520c4d0db1cd4cd05c0a295e331884b596", } @@ -146,7 +148,9 @@ def compute_file_sha256(path: str) -> str: return digest.hexdigest() -def verify_metadata_sha256(*, cuda_version: str, metadata_path: str, metadata_source: str = "public") -> int: +def verify_metadata_sha256( + *, cuda_version: str, metadata_path: str, metadata_source: str = "public", allow_unpinned: bool = False +) -> int: """Verify the redistrib JSON against the pinned hash for its CUDA version. The pins describe the public developer.download.nvidia.com manifests. A @@ -154,8 +158,10 @@ def verify_metadata_sha256(*, cuda_version: str, metadata_path: str, metadata_so manifest under the same version number, so it is never checked against them; it only gets a warning. - Returns a process exit code: 1 on a mismatch (fail closed), 0 when the hash - matches, the version is not pinned yet, or the source is custom (warn only). + Returns a process exit code: 1 on a mismatch or an unpinned public version + (fail closed), 0 when the hash matches, the source is custom, or the version + is unpinned and ``allow_unpinned`` is set (warn only). ``allow_unpinned`` + never overrides a mismatch against an existing pin. """ actual = compute_file_sha256(metadata_path) if metadata_source == "custom": @@ -167,12 +173,20 @@ def verify_metadata_sha256(*, cuda_version: str, metadata_path: str, metadata_so return 0 expected = KNOWN_REDISTRIB_SHA256.get(cuda_version) if expected is None: + if allow_unpinned: + print( + f"WARNING: no pinned redistrib sha256 for CUDA {cuda_version!r}; got {actual}. " + "Continuing because unpinned versions are explicitly allowed.", + file=sys.stderr, + ) + return 0 print( - f"WARNING: no pinned redistrib sha256 for CUDA {cuda_version!r}; got {actual}. " - f"Add it to KNOWN_REDISTRIB_SHA256 in {Path(__file__).name} to pin it.", + f"ERROR: no pinned redistrib sha256 for CUDA {cuda_version!r}; got {actual}. " + f"Add it to KNOWN_REDISTRIB_SHA256 in {Path(__file__).name}, " + "or pass --allow-unpinned (fetch_ctk input allow-unpinned) to accept it unverified.", file=sys.stderr, ) - return 0 + return 1 if actual != expected: print( f"ERROR: redistrib metadata sha256 mismatch for CUDA {cuda_version!r}: expected {expected}, got {actual}", @@ -207,6 +221,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: verify_parser.add_argument("--cuda-version", required=True) verify_parser.add_argument("--metadata-path", required=True) verify_parser.add_argument("--metadata-source", choices=("public", "custom"), default="public") + verify_parser.add_argument("--allow-unpinned", action="store_true") return parser.parse_args(argv) @@ -221,6 +236,7 @@ def main(argv: list[str] | None = None) -> int: cuda_version=args.cuda_version, metadata_path=args.metadata_path, metadata_source=args.metadata_source, + allow_unpinned=args.allow_unpinned, ) metadata = load_metadata(args.metadata_path) diff --git a/ci/tools/tests/test_fetch_ctk_redistrib.py b/ci/tools/tests/test_fetch_ctk_redistrib.py index dad75da1a1b..5a06c2c9c37 100644 --- a/ci/tools/tests/test_fetch_ctk_redistrib.py +++ b/ci/tools/tests/test_fetch_ctk_redistrib.py @@ -4,13 +4,16 @@ from __future__ import annotations import json +import re import sys from pathlib import Path import pytest +import yaml sys.path.insert(0, str(Path(__file__).parent.parent)) from fetch_ctk_redistrib import ( + KNOWN_REDISTRIB_SHA256, compute_file_sha256, get_component_relative_path, get_component_sha256, @@ -18,6 +21,9 @@ verify_metadata_sha256, ) +CI_DIR = Path(__file__).resolve().parents[2] +SHA256_RE = re.compile(r"^[0-9a-f]{64}$") + # Minimal redistrib-shaped metadata. "cccl" exercises the cuda_cccl -> cccl # alias; "no_sha" lacks the sha256 field. FAKE_METADATA = { @@ -101,16 +107,64 @@ def test_verify_metadata_mismatch_fails_closed(tmp_path, monkeypatch): assert verify_metadata_sha256(cuda_version="1.2.3", metadata_path=str(f)) == 1 -@pytest.mark.agent_authored(model="claude-opus-4.8") -def test_verify_metadata_unknown_version_warns(tmp_path, capsys): +@pytest.mark.agent_authored(model="claude-opus-5.5") +def test_verify_metadata_unknown_version_fails_closed(tmp_path, capsys): + f = tmp_path / "redistrib.json" + f.write_bytes(b"whatever") + assert verify_metadata_sha256(cuda_version="99.9.9", metadata_path=str(f)) == 1 + assert "ERROR" in capsys.readouterr().err + + +@pytest.mark.agent_authored(model="claude-opus-5.5") +def test_verify_metadata_unknown_version_allowed_warns(tmp_path, capsys): + f = tmp_path / "redistrib.json" + f.write_bytes(b"whatever") + assert verify_metadata_sha256(cuda_version="99.9.9", metadata_path=str(f), allow_unpinned=True) == 0 + assert "WARNING" in capsys.readouterr().err + + +@pytest.mark.agent_authored(model="claude-opus-5.5") +def test_verify_metadata_allow_unpinned_does_not_bypass_mismatch(tmp_path, monkeypatch): + f = tmp_path / "redistrib.json" + f.write_bytes(b"tampered-metadata") + monkeypatch.setitem( + sys.modules["fetch_ctk_redistrib"].KNOWN_REDISTRIB_SHA256, + "1.2.3", + "f" * 64, + ) + assert verify_metadata_sha256(cuda_version="1.2.3", metadata_path=str(f), allow_unpinned=True) == 1 + + +@pytest.mark.agent_authored(model="claude-opus-5.5") +def test_main_verify_metadata_allow_unpinned_flag(tmp_path, capsys): f = tmp_path / "redistrib.json" f.write_bytes(b"whatever") - # An unpinned version must not fail closed (it would break CI on every new - # CTK), but it must emit a warning. - assert verify_metadata_sha256(cuda_version="99.9.9", metadata_path=str(f)) == 0 + assert main(["verify-metadata", "--cuda-version", "99.9.9", "--metadata-path", str(f)]) == 1 + rc = main(["verify-metadata", "--cuda-version", "99.9.9", "--metadata-path", str(f), "--allow-unpinned"]) + assert rc == 0 assert "WARNING" in capsys.readouterr().err +@pytest.mark.agent_authored(model="claude-opus-5.5") +def test_known_redistrib_sha256_covers_ci_cuda_versions(): + # Every CUDA version CI fetches from the public server must be pinned, so a + # version bump fails here instead of in the fetch_ctk step. + versions = yaml.safe_load((CI_DIR / "versions.yml").read_text(encoding="utf-8")) + matrix = yaml.safe_load((CI_DIR / "test-matrix.yml").read_text(encoding="utf-8")) + required = {str(entry["version"]) for entry in versions["cuda"].values()} + required |= { + str(row["CUDA_VER"]) + for platform in ("linux", "windows") + for rows in matrix[platform].values() + for row in rows + if str(row.get("LOCAL_CTK")) == "1" + } + assert required + assert sorted(required - KNOWN_REDISTRIB_SHA256.keys()) == [] + for version, sha256 in KNOWN_REDISTRIB_SHA256.items(): + assert SHA256_RE.fullmatch(sha256), f"invalid pinned sha256 for CUDA {version}" + + @pytest.mark.agent_authored(model="claude-opus-4.8") def test_main_component_sha256(tmp_path, capsys): f = tmp_path / "redistrib.json" From baa5ab69a51587712deedd4113abb92ea4960c49 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Thu, 5 Nov 2026 15:34:30 -0800 Subject: [PATCH 3/6] CI: Drop the allow-unpinned escape hatch from fetch_ctk No caller sets it, and pinning a new CUDA version is a one-line addition to KNOWN_REDISTRIB_SHA256 that the error message points to. Remove the fetch_ctk input and the --allow-unpinned flag; fetch_ctk/action.yml is back to its main-branch form. --- .github/actions/fetch_ctk/action.yml | 15 +---------- ci/tools/fetch_ctk_redistrib.py | 29 +++++---------------- ci/tools/tests/test_fetch_ctk_redistrib.py | 30 ---------------------- 3 files changed, 8 insertions(+), 66 deletions(-) diff --git a/.github/actions/fetch_ctk/action.yml b/.github/actions/fetch_ctk/action.yml index 9931403f8b0..65b05a66acf 100644 --- a/.github/actions/fetch_ctk/action.yml +++ b/.github/actions/fetch_ctk/action.yml @@ -26,13 +26,6 @@ inputs: Override to install from an alternate redistrib server. required: false default: "https://developer.download.nvidia.com/compute/cuda/redist" - allow-unpinned: - description: > - Set to "true" to accept a public-server CUDA version whose manifest hash is - not pinned in ci/tools/fetch_ctk_redistrib.py (warn instead of fail). - A mismatch against an existing pin always fails. - required: false - default: "false" runs: using: composite @@ -70,16 +63,10 @@ runs: else METADATA_SOURCE=custom fi - case "${{ inputs.allow-unpinned }}" in - true) ALLOW_UNPINNED_FLAG=--allow-unpinned ;; - false) ALLOW_UNPINNED_FLAG= ;; - *) echo "ERROR: allow-unpinned must be 'true' or 'false'" >&2; exit 1 ;; - esac python "$CTK_REDIST_TOOL" verify-metadata \ --cuda-version "${{ inputs.cuda-version }}" \ --metadata-path "$CTK_JSON_FILE" \ - --metadata-source "$METADATA_SOURCE" \ - ${ALLOW_UNPINNED_FLAG} + --metadata-source "$METADATA_SOURCE" # Pre-process the component list to ensure hash uniqueness CTK_CACHE_COMPONENTS="$(python "$CTK_REDIST_TOOL" filter-components \ diff --git a/ci/tools/fetch_ctk_redistrib.py b/ci/tools/fetch_ctk_redistrib.py index a6ad3e4116e..6217dad42da 100644 --- a/ci/tools/fetch_ctk_redistrib.py +++ b/ci/tools/fetch_ctk_redistrib.py @@ -30,10 +30,10 @@ # Known-good sha256 of the redistrib metadata JSON, keyed by CUDA version. # Per-archive verification is only trustworthy if the JSON that carries the # archive hashes is itself trusted, so we pin the metadata too. `verify-metadata` -# fails on a public-server version that is absent here unless the caller opts -# out with `--allow-unpinned`. Add a version's hash below in the same change -# that adds it to ci/versions.yml or a LOCAL_CTK row of ci/test-matrix.yml; -# tests/test_fetch_ctk_redistrib.py enforces this. Regenerate with: +# fails on a public-server version that is absent here, so add a version's hash +# below in the same change that adds it to ci/versions.yml or a LOCAL_CTK row of +# ci/test-matrix.yml; tests/test_fetch_ctk_redistrib.py enforces this. +# Regenerate with: # curl -fsSL \ # https://developer.download.nvidia.com/compute/cuda/redist/redistrib_.json \ # | sha256sum @@ -148,9 +148,7 @@ def compute_file_sha256(path: str) -> str: return digest.hexdigest() -def verify_metadata_sha256( - *, cuda_version: str, metadata_path: str, metadata_source: str = "public", allow_unpinned: bool = False -) -> int: +def verify_metadata_sha256(*, cuda_version: str, metadata_path: str, metadata_source: str = "public") -> int: """Verify the redistrib JSON against the pinned hash for its CUDA version. The pins describe the public developer.download.nvidia.com manifests. A @@ -159,9 +157,7 @@ def verify_metadata_sha256( them; it only gets a warning. Returns a process exit code: 1 on a mismatch or an unpinned public version - (fail closed), 0 when the hash matches, the source is custom, or the version - is unpinned and ``allow_unpinned`` is set (warn only). ``allow_unpinned`` - never overrides a mismatch against an existing pin. + (fail closed), 0 when the hash matches or the source is custom (warn only). """ actual = compute_file_sha256(metadata_path) if metadata_source == "custom": @@ -173,17 +169,9 @@ def verify_metadata_sha256( return 0 expected = KNOWN_REDISTRIB_SHA256.get(cuda_version) if expected is None: - if allow_unpinned: - print( - f"WARNING: no pinned redistrib sha256 for CUDA {cuda_version!r}; got {actual}. " - "Continuing because unpinned versions are explicitly allowed.", - file=sys.stderr, - ) - return 0 print( f"ERROR: no pinned redistrib sha256 for CUDA {cuda_version!r}; got {actual}. " - f"Add it to KNOWN_REDISTRIB_SHA256 in {Path(__file__).name}, " - "or pass --allow-unpinned (fetch_ctk input allow-unpinned) to accept it unverified.", + f"Add it to KNOWN_REDISTRIB_SHA256 in {Path(__file__).name} to pin it.", file=sys.stderr, ) return 1 @@ -221,8 +209,6 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: verify_parser.add_argument("--cuda-version", required=True) verify_parser.add_argument("--metadata-path", required=True) verify_parser.add_argument("--metadata-source", choices=("public", "custom"), default="public") - verify_parser.add_argument("--allow-unpinned", action="store_true") - return parser.parse_args(argv) @@ -236,7 +222,6 @@ def main(argv: list[str] | None = None) -> int: cuda_version=args.cuda_version, metadata_path=args.metadata_path, metadata_source=args.metadata_source, - allow_unpinned=args.allow_unpinned, ) metadata = load_metadata(args.metadata_path) diff --git a/ci/tools/tests/test_fetch_ctk_redistrib.py b/ci/tools/tests/test_fetch_ctk_redistrib.py index 5a06c2c9c37..59a7cadfba9 100644 --- a/ci/tools/tests/test_fetch_ctk_redistrib.py +++ b/ci/tools/tests/test_fetch_ctk_redistrib.py @@ -115,36 +115,6 @@ def test_verify_metadata_unknown_version_fails_closed(tmp_path, capsys): assert "ERROR" in capsys.readouterr().err -@pytest.mark.agent_authored(model="claude-opus-5.5") -def test_verify_metadata_unknown_version_allowed_warns(tmp_path, capsys): - f = tmp_path / "redistrib.json" - f.write_bytes(b"whatever") - assert verify_metadata_sha256(cuda_version="99.9.9", metadata_path=str(f), allow_unpinned=True) == 0 - assert "WARNING" in capsys.readouterr().err - - -@pytest.mark.agent_authored(model="claude-opus-5.5") -def test_verify_metadata_allow_unpinned_does_not_bypass_mismatch(tmp_path, monkeypatch): - f = tmp_path / "redistrib.json" - f.write_bytes(b"tampered-metadata") - monkeypatch.setitem( - sys.modules["fetch_ctk_redistrib"].KNOWN_REDISTRIB_SHA256, - "1.2.3", - "f" * 64, - ) - assert verify_metadata_sha256(cuda_version="1.2.3", metadata_path=str(f), allow_unpinned=True) == 1 - - -@pytest.mark.agent_authored(model="claude-opus-5.5") -def test_main_verify_metadata_allow_unpinned_flag(tmp_path, capsys): - f = tmp_path / "redistrib.json" - f.write_bytes(b"whatever") - assert main(["verify-metadata", "--cuda-version", "99.9.9", "--metadata-path", str(f)]) == 1 - rc = main(["verify-metadata", "--cuda-version", "99.9.9", "--metadata-path", str(f), "--allow-unpinned"]) - assert rc == 0 - assert "WARNING" in capsys.readouterr().err - - @pytest.mark.agent_authored(model="claude-opus-5.5") def test_known_redistrib_sha256_covers_ci_cuda_versions(): # Every CUDA version CI fetches from the public server must be pinned, so a From bc927090690aade55899edf803009e8eb0b1bc56 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Thu, 5 Nov 2026 15:39:10 -0800 Subject: [PATCH 4/6] CI: Point CTK version bumps at the redistrib manifest pins Note in ci/versions.yml that each CUDA version there and in LOCAL_CTK matrix rows needs a pin in KNOWN_REDISTRIB_SHA256, and make the coverage test's failure message say where to add the pin and how to compute it. --- ci/tools/tests/test_fetch_ctk_redistrib.py | 7 ++++++- ci/versions.yml | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ci/tools/tests/test_fetch_ctk_redistrib.py b/ci/tools/tests/test_fetch_ctk_redistrib.py index 59a7cadfba9..03f9a29df50 100644 --- a/ci/tools/tests/test_fetch_ctk_redistrib.py +++ b/ci/tools/tests/test_fetch_ctk_redistrib.py @@ -130,7 +130,12 @@ def test_known_redistrib_sha256_covers_ci_cuda_versions(): if str(row.get("LOCAL_CTK")) == "1" } assert required - assert sorted(required - KNOWN_REDISTRIB_SHA256.keys()) == [] + missing = sorted(required - KNOWN_REDISTRIB_SHA256.keys()) + assert not missing, ( + f"CUDA versions used by CI have no pinned redistrib manifest hash: {missing}. " + "Add them to KNOWN_REDISTRIB_SHA256 in ci/tools/fetch_ctk_redistrib.py; compute each hash with " + "`curl -fsSL https://developer.download.nvidia.com/compute/cuda/redist/redistrib_.json | sha256sum`." + ) for version, sha256 in KNOWN_REDISTRIB_SHA256.items(): assert SHA256_RE.fullmatch(sha256), f"invalid pinned sha256 for CUDA {version}" diff --git a/ci/versions.yml b/ci/versions.yml index 2a48f83497c..de50338604d 100644 --- a/ci/versions.yml +++ b/ci/versions.yml @@ -6,6 +6,9 @@ backport_branch: "12.9.x" # keep in sync with target-branch in .github/dependab # This controls the version of the upstream CTK to install to build the wheels. # It is not the version of cuda_bindings/cuda_python being released, which is # solely determined by git tags via setuptools_scm. +# Each version here, and in every LOCAL_CTK: '1' row of ci/test-matrix.yml, +# needs a pinned manifest hash in KNOWN_REDISTRIB_SHA256 in +# ci/tools/fetch_ctk_redistrib.py; fetch_ctk fails without one. cuda: build: version: "13.5.0" From 8fbed1191863b3906843c2bd2144d184ee551c57 Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Fri, 6 Nov 2026 09:24:39 -0800 Subject: [PATCH 5/6] CI: Fix compute-sanitizer setup wiping the mini CTK #2437 made fetch_ctk empty cuda-path before every install. The sanitizer step runs fetch_ctk a second time for cuda_sanitizer_api alone, so it unintentionally deleted the mini CTK installed just before it, and the instrumented test runs failed before any test ran. #2437 then disabled the only matrix rows that run the sanitizer. Add an install-mode input. "add" installs on top of the existing mini CTK at the same cuda-path and keeps the environment the first call exported; the sanitizer step uses it. --- .github/actions/fetch_ctk/action.yml | 45 ++++++++++++++++++++++++-- .github/workflows/test-wheel-linux.yml | 1 + 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/.github/actions/fetch_ctk/action.yml b/.github/actions/fetch_ctk/action.yml index 65b05a66acf..034f3c416f8 100644 --- a/.github/actions/fetch_ctk/action.yml +++ b/.github/actions/fetch_ctk/action.yml @@ -19,6 +19,14 @@ inputs: description: "where the CTK components will be installed to, relative to $PWD" required: false default: "./cuda_toolkit" + install-mode: + description: > + "replace" (default) empties cuda-path before installing. + "add" installs the components on top of the mini CTK that an earlier + fetch_ctk call installed at the same cuda-path, and leaves the + environment that call exported unchanged. + required: false + default: "replace" redist-base-url: description: > Base URL of a redistrib server (manifest and component archives). @@ -43,6 +51,34 @@ runs: with: dependent_exes: "curl unzip tar sha256sum cygpath" + - name: Check install mode + shell: bash --noprofile --norc -xeuo pipefail {0} + run: | + case "${{ inputs.install-mode }}" in + replace) + ;; + add) + # The environment exports are skipped in this mode, so the target + # must be the CUDA_PATH an earlier fetch_ctk call exported. + if [[ -z "${CUDA_PATH:-}" ]]; then + echo "::error::install-mode 'add' requires CUDA_PATH from an earlier fetch_ctk call" + exit 1 + fi + EXISTING_PATH="$CUDA_PATH" + if [[ "${{ inputs.host-platform }}" == win* ]]; then + EXISTING_PATH="$(cygpath -u "$EXISTING_PATH")" + fi + if [[ "$(realpath -e "${{ inputs.cuda-path }}")" != "$(realpath -e "$EXISTING_PATH")" ]]; then + echo "::error::install-mode 'add' requires cuda-path to be the existing CUDA_PATH ($CUDA_PATH)" + exit 1 + fi + ;; + *) + echo "::error::install-mode must be 'replace' or 'add', got '${{ inputs.install-mode }}'" + exit 1 + ;; + esac + - name: Set up CTK cache variable shell: bash --noprofile --norc -xeuo pipefail {0} run: | @@ -170,7 +206,9 @@ runs: # symlinks from a prerelease CTK at ./cuda_toolkit/include etc., causing # "cp: cannot overwrite non-directory" failures on the next restore). CUDA_PATH="${{ inputs.cuda-path }}" - rm -rf $CUDA_PATH + if [[ "${{ inputs.install-mode }}" == replace ]]; then + rm -rf $CUDA_PATH + fi mkdir -p $CUDA_PATH # Unfortunately we cannot use "rsync -av $CACHE_TMP_DIR/ $CUDA_PATH" because # not all runners have rsync pre-installed (or even installable, such as @@ -194,7 +232,9 @@ runs: ls -l CACHE_TMP_DIR="./cache_tmp_dir" CUDA_PATH="${{ inputs.cuda-path }}" - rm -rf $CUDA_PATH + if [[ "${{ inputs.install-mode }}" == replace ]]; then + rm -rf $CUDA_PATH + fi mkdir -p $CUDA_PATH tar -xzvf $CTK_CACHE_FILENAME # Can't use rsync here, see above @@ -203,6 +243,7 @@ runs: ls -l $CUDA_PATH - name: Set output environment variables + if: ${{ inputs.install-mode == 'replace' }} shell: bash --noprofile --norc -xeuo pipefail {0} run: | # mimics actual CTK installation diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 4b35c689a05..38e117c96db 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -369,6 +369,7 @@ jobs: host-platform: ${{ inputs.host-platform }} cuda-version: ${{ matrix.CUDA_VER }} cuda-components: "cuda_sanitizer_api" + install-mode: add - name: Set up compute-sanitizer if: ${{ env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' }} From d14706c2573e33837eb9f7f37794332e59cf538d Mon Sep 17 00:00:00 2001 From: Ralf Juengling Date: Fri, 6 Nov 2026 11:05:26 -0800 Subject: [PATCH 6/6] CI: Use the build CTK's compute-sanitizer in all sanitizer runs With the matrix CTK's own sanitizer, the 12.9.1 and 13.0.2 rows segfaulted partway through the cuda.core tests. With the 13.5.0 sanitizer, both ran the full suite (#3400). This keeps the intent of #594 (newer sanitizer fixes for older toolkits) without the run-time scrape: the version comes from cuda.build.version in ci/versions.yml, which is already pinned. --- .github/workflows/test-wheel-linux.yml | 4 +++- ci/versions.yml | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 38e117c96db..e4ba9683cb5 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -367,7 +367,9 @@ jobs: continue-on-error: false with: host-platform: ${{ inputs.host-platform }} - cuda-version: ${{ matrix.CUDA_VER }} + # The build CTK's sanitizer, not the matrix CTK's: older sanitizers + # crash on the cuda.core tests, and newer ones carry the fixes. + cuda-version: ${{ inputs.build-ctk-ver }} cuda-components: "cuda_sanitizer_api" install-mode: add diff --git a/ci/versions.yml b/ci/versions.yml index de50338604d..e1238c28756 100644 --- a/ci/versions.yml +++ b/ci/versions.yml @@ -3,7 +3,8 @@ backport_branch: "12.9.x" # keep in sync with target-branch in .github/dependabot.yml -# This controls the version of the upstream CTK to install to build the wheels. +# This controls the version of the upstream CTK to install to build the wheels, +# and the compute-sanitizer version used in all sanitizer test runs. # It is not the version of cuda_bindings/cuda_python being released, which is # solely determined by git tags via setuptools_scm. # Each version here, and in every LOCAL_CTK: '1' row of ci/test-matrix.yml,