Skip to content

Commit db347c9

Browse files
aryanputtarwgk
andauthored
feat(pathfinder): add CTK-root canary fallback for NVIDIA binaries (#2196)
* fix(pathfinder): make find_nvidia_binary_utility deterministic, never search CWD find_nvidia_binary_utility assembled a bounded list of trusted directories (NVIDIA wheel bin/, CONDA_PREFIX, CUDA_HOME/CUDA_PATH) and then delegated to shutil.which(name, path=trusted_dirs). On Windows shutil.which prepends the process current working directory to the search even when an explicit path= is supplied, so a binary located in an arbitrary (possibly attacker-writable) CWD could be returned in preference to the trusted CUDA / Conda / wheel binary. That violates the pathfinder contract of a deterministic lookup over a documented, bounded set of trusted roots. Replace the shutil.which delegation with an explicit resolver that searches only the trusted directories, in order, returning the first executable match. The current working directory and ambient PATH are never consulted. POSIX execute-bit (X_OK) and Windows extension semantics are preserved, so behavior is unchanged except for removing the CWD/PATH leakage. Names resolved in the existing trusted dirs return exactly as before. Rewrites the search-path tests to assert the deterministic probe order and adds TestResolveInTrustedDirs covering CWD isolation, first-match-wins, empty/duplicate dir skipping, and POSIX non-executable rejection. Fixes #2119 * feat(pathfinder): add CTK-root canary fallback to find_nvidia_binary_utility After the deterministic search over the explicit trusted directories (NVIDIA wheel bin/, CONDA_PREFIX, CUDA_HOME/CUDA_PATH) misses, fall back to a CTK-root canary probe: resolve cudart through the OS dynamic loader, which honors LD_LIBRARY_PATH on Linux and the native DLL search on Windows, derive the CUDA Toolkit root from its absolute path, and search that root's bin layout. This addresses the concern raised on #2196: users who follow the CUDA Linux installation guide set LD_LIBRARY_PATH for libraries and PATH for executables. The bounded finder alone would stop finding the utility for them because PATH is intentionally never consulted. The canary fallback recovers that case through LD_LIBRARY_PATH instead of PATH. LD_LIBRARY_PATH is still an attack vector, but a significantly harder one to exploit than PATH, and the ambient PATH and process CWD remain unused. The canary runs only after the explicit trusted dirs miss, so the common wheel/conda/CUDA_HOME cases never spawn the resolver subprocess. The canary -> CTK-root resolution is factored into a shared resolve_ctk_root_via_canary helper reused by the dynamic-library CTK-root canary flow. Adds tests for the fallback (found, ordering, Windows bin layout, not consulted when found earlier, cached) and for resolve_ctk_root_via_canary. Adds 1.6.0 release notes for the minor version bump. * fix(pathfinder): satisfy mypy no-any-return in canary helpers pre-commit.ci mypy flagged returning Any from resolve_ctk_root_via_canary and _resolve_ctk_root_via_canary (both declared -> str | None), because derive_ctk_root resolves to Any under the pathfinder mypy config. Bind the result to an annotated local before returning, matching the pattern used elsewhere in the package. * fix(pathfinder): address binary finder review * docs(pathfinder): prepare 1.6.0 release notes * refactor(pathfinder): apply review feedback on binary finder - Rename _is_executable_file to _is_executable_candidate; the helper marks a return candidate rather than proving OS executability. Drop its docstring now that the name is self-explanatory, and update the test patch target. - Drop the 'ambient PATH is never consulted' sentence from resolve_ctk_root_via_canary; it is not universally true. Signed-off-by: Aryan <aryansputta@gmail.com> --------- Signed-off-by: Aryan <aryansputta@gmail.com> Co-authored-by: Ralf W. Grosse-Kunstleve <rgrossekunst@nvidia.com>
1 parent 270be80 commit db347c9

9 files changed

Lines changed: 370 additions & 42 deletions

File tree

cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
import functools
55
import os
6-
import shutil
76

87
from cuda.pathfinder._binaries import supported_nvidia_binaries
8+
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
99
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
1010
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
1111
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS
@@ -28,6 +28,45 @@ def _normalize_utility_name(utility_name: str) -> str:
2828
return utility_name
2929

3030

31+
def _is_executable_candidate(path: str) -> bool:
32+
if not os.path.isfile(path):
33+
return False
34+
if IS_WINDOWS:
35+
return True
36+
return os.access(path, os.X_OK)
37+
38+
39+
def _ctk_bin_subdirs(root: str) -> list[str]:
40+
if IS_WINDOWS:
41+
return [
42+
os.path.join(root, "bin", "x64"),
43+
os.path.join(root, "bin", "x86_64"),
44+
os.path.join(root, "bin"),
45+
]
46+
return [os.path.join(root, "bin")]
47+
48+
49+
def _resolve_ctk_root_via_canary() -> str | None:
50+
from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import resolve_ctk_root_via_canary
51+
52+
ctk_root: str | None = resolve_ctk_root_via_canary(CTK_ROOT_CANARY_ANCHOR_LIBNAMES[0])
53+
return ctk_root
54+
55+
56+
def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | None:
57+
"""Resolve ``normalized_name`` against ``dirs`` in order."""
58+
seen: set[str] = set()
59+
for directory in dirs:
60+
if directory in seen:
61+
continue
62+
assert directory
63+
seen.add(directory)
64+
candidate = os.path.join(directory, normalized_name)
65+
if _is_executable_candidate(candidate):
66+
return candidate
67+
return None
68+
69+
3170
@functools.cache
3271
def find_nvidia_binary_utility(utility_name: str) -> str | None:
3372
"""Locate a CUDA binary utility executable.
@@ -65,6 +104,12 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
65104
``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows,
66105
or just ``bin`` on Linux.
67106
107+
4. **CTK-root canary fallback**
108+
109+
- Only when steps 1-3 miss: resolve the ``cudart`` library through the
110+
OS dynamic loader, derive the CUDA Toolkit root from it, and search
111+
that root's bin layout.
112+
68113
Note:
69114
Results are cached using ``@functools.cache`` for performance. The cache
70115
persists for the lifetime of the process.
@@ -73,6 +118,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
73118
(``.exe``, ``.bat``, ``.cmd``). On Unix-like systems, executables
74119
are identified by the ``X_OK`` (execute) permission bit.
75120
121+
Lookup is restricted to the trusted directories and the canary-derived
122+
CTK root listed above.
123+
76124
Example:
77125
>>> from cuda.pathfinder import find_nvidia_binary_utility
78126
>>> nvdisasm = find_nvidia_binary_utility("nvdisasm")
@@ -98,10 +146,15 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
98146

99147
# 3. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH)
100148
if (cuda_home := get_cuda_path_or_home()) is not None:
101-
if IS_WINDOWS:
102-
dirs.append(os.path.join(cuda_home, "bin", "x64"))
103-
dirs.append(os.path.join(cuda_home, "bin", "x86_64"))
104-
dirs.append(os.path.join(cuda_home, "bin"))
149+
dirs.extend(_ctk_bin_subdirs(cuda_home))
105150

106151
normalized_name = _normalize_utility_name(utility_name)
107-
return shutil.which(normalized_name, path=os.pathsep.join(dirs))
152+
found = _resolve_in_trusted_dirs(normalized_name, dirs)
153+
if found is not None:
154+
return found
155+
156+
# 4. CTK-root canary fallback.
157+
ctk_root = _resolve_ctk_root_via_canary()
158+
if ctk_root is not None:
159+
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root))
160+
return None

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/descriptor_catalog.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from dataclasses import dataclass
99
from typing import Literal
1010

11+
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
12+
1113
PackagedWith = Literal["ctk", "other", "driver"]
1214

1315

@@ -73,7 +75,7 @@ class DescriptorSpec:
7375
site_packages_windows=("nvidia/cu13/bin/x86_64", "nvidia/cuda_nvcc/nvvm/bin"),
7476
anchor_rel_dirs_linux=("nvvm/lib64",),
7577
anchor_rel_dirs_windows=("nvvm/bin/*", "nvvm/bin"),
76-
ctk_root_canary_anchor_libnames=("cudart",),
78+
ctk_root_canary_anchor_libnames=CTK_ROOT_CANARY_ANCHOR_LIBNAMES,
7779
),
7880
DescriptorSpec(
7981
name="cublas",
@@ -292,7 +294,7 @@ class DescriptorSpec:
292294
site_packages_windows=("nvidia/cu13/bin/x86_64", "nvidia/cuda_cupti/bin"),
293295
anchor_rel_dirs_linux=("extras/CUPTI/lib64", "lib"),
294296
anchor_rel_dirs_windows=("extras/CUPTI/lib64", "bin"),
295-
ctk_root_canary_anchor_libnames=("cudart",),
297+
ctk_root_canary_anchor_libnames=CTK_ROOT_CANARY_ANCHOR_LIBNAMES,
296298
),
297299
DescriptorSpec(
298300
name="cudla",

cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_nvidia_dynamic_lib.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,13 +136,26 @@ def _loadable_via_canary_subprocess(libname: str, *, timeout: float = _CANARY_PR
136136
return _resolve_system_loaded_abs_path_in_subprocess(libname, timeout=timeout) is not None
137137

138138

139+
def resolve_ctk_root_via_canary(canary_libname: str) -> str | None:
140+
"""Resolve the CUDA Toolkit root from a system-loadable canary library.
141+
142+
The canary library's absolute path is resolved by the OS dynamic loader in
143+
an isolated subprocess, which honors ``LD_LIBRARY_PATH`` on Linux and the
144+
native DLL search on Windows. The toolkit root is then derived from that
145+
path. Returns ``None`` if the canary cannot be resolved or no root can be
146+
derived.
147+
"""
148+
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname)
149+
if canary_abs_path is None:
150+
return None
151+
ctk_root: str | None = derive_ctk_root(canary_abs_path)
152+
return ctk_root
153+
154+
139155
def _try_ctk_root_canary(ctx: SearchContext) -> str | None:
140156
"""Try CTK-root canary fallback for descriptor-configured libraries."""
141157
for canary_libname in ctx.desc.ctk_root_canary_anchor_libnames:
142-
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname)
143-
if canary_abs_path is None:
144-
continue
145-
ctk_root = derive_ctk_root(canary_abs_path)
158+
ctk_root = resolve_ctk_root_via_canary(canary_libname)
146159
if ctk_root is None:
147160
continue
148161
find = find_via_ctk_root(ctx, ctk_root)

cuda_pathfinder/cuda/pathfinder/_headers/find_nvidia_headers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
platform_include_subdirs,
2020
resolve_conda_anchor,
2121
)
22+
from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES
2223
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
2324
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
2425

@@ -121,7 +122,7 @@ def find_via_ctk_root_canary(desc: HeaderDescriptor) -> LocatedHeaderDir | None:
121122
"""
122123
if not desc.use_ctk_root_canary:
123124
return None
124-
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess("cudart")
125+
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(CTK_ROOT_CANARY_ANCHOR_LIBNAMES[0])
125126
if canary_abs_path is None:
126127
return None
127128
ctk_root = derive_ctk_root(canary_abs_path)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
CTK_ROOT_CANARY_ANCHOR_LIBNAMES = ("cudart",)

cuda_pathfinder/docs/nv-versions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
"version": "latest",
44
"url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/latest/"
55
},
6+
{
7+
"version": "1.6.0",
8+
"url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.6.0/"
9+
},
610
{
711
"version": "1.5.6",
812
"url": "https://nvidia.github.io/cuda-python/cuda-pathfinder/1.5.6/"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
.. SPDX-License-Identifier: Apache-2.0
3+
4+
.. py:currentmodule:: cuda.pathfinder
5+
6+
``cuda-pathfinder`` 1.6.0 Release notes
7+
=======================================
8+
9+
Highlights
10+
----------
11+
12+
* :func:`find_nvidia_binary_utility` now probes configured binary directories
13+
directly instead of delegating to ``shutil.which``, avoiding implicit
14+
current-working-directory lookup on Windows. If those locations miss, a new
15+
CTK-root canary fallback resolves ``cudart`` through the OS dynamic loader,
16+
derives its CUDA Toolkit root, and searches that root's ``bin`` layout. With
17+
multiple visible toolkits, the result follows the ``cudart`` selected by the
18+
dynamic loader and may differ from the executable selected by a shell.
19+
(`PR #2196 <https://github.com/NVIDIA/cuda-python/pull/2196>`_)
20+
21+
* Add dynamic-library loading and header discovery for the cuQuantum libraries
22+
``cudensitymat``, ``cupauliprop``, ``cutensornet``, ``custabilizer``, and
23+
``custatevec`` on Linux.
24+
(`PR #2376 <https://github.com/NVIDIA/cuda-python/pull/2376>`_)
25+
26+
* Add dynamic-library loading support for ``cutensorMp`` on Linux, including
27+
preloading the packaged CUDA runtime in CUDA 12 wheel-only environments.
28+
(`PR #2374 <https://github.com/NVIDIA/cuda-python/pull/2374>`_)
29+
30+
* Add Windows Arm64 discovery for ``cudla.dll``, enabling
31+
``load_nvidia_dynamic_lib("cudla")`` to find cuDLA in the CUDA Toolkit's
32+
architecture-specific ``bin`` directories.
33+
(`PR #2274 <https://github.com/NVIDIA/cuda-python/pull/2274>`_)
34+
35+
Internal maintenance
36+
--------------------
37+
38+
* Clean up dead and misleading error-handling code in the Linux and Windows
39+
dynamic-library loaders; runtime behavior is unchanged.
40+
(`PR #2239 <https://github.com/NVIDIA/cuda-python/pull/2239>`_)

cuda_pathfinder/tests/test_ctk_root_discovery.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
_load_lib_no_cache,
1818
_resolve_system_loaded_abs_path_in_subprocess,
1919
_try_ctk_root_canary,
20+
resolve_ctk_root_via_canary,
2021
)
2122
from cuda.pathfinder._dynamic_libs.search_steps import (
2223
SearchContext,
@@ -369,6 +370,35 @@ def test_canary_skips_when_abs_path_none(mocker):
369370
assert _try_ctk_root_canary(_ctx("nvvm")) is None
370371

371372

373+
# ---------------------------------------------------------------------------
374+
# resolve_ctk_root_via_canary (shared by lib and binary discovery)
375+
# ---------------------------------------------------------------------------
376+
377+
378+
def test_resolve_ctk_root_via_canary_returns_root(tmp_path, mocker):
379+
ctk_root = tmp_path / "cuda-13"
380+
_create_cudart_in_ctk(ctk_root)
381+
probe = mocker.patch(
382+
f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess",
383+
return_value=_fake_canary_path(ctk_root),
384+
)
385+
assert resolve_ctk_root_via_canary("cudart") == str(ctk_root)
386+
probe.assert_called_once_with("cudart")
387+
388+
389+
def test_resolve_ctk_root_via_canary_none_when_probe_fails(mocker):
390+
mocker.patch(f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess", return_value=None)
391+
assert resolve_ctk_root_via_canary("cudart") is None
392+
393+
394+
def test_resolve_ctk_root_via_canary_none_when_unrecognized(mocker):
395+
mocker.patch(
396+
f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess",
397+
return_value=os.path.join(os.sep, "weird", "path", "libcudart.so.13"),
398+
)
399+
assert resolve_ctk_root_via_canary("cudart") is None
400+
401+
372402
# ---------------------------------------------------------------------------
373403
# _load_lib_no_cache search-order
374404
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)