Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@

import functools
import os
import shutil

from cuda.pathfinder._binaries import supported_nvidia_binaries
from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home
from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages
from cuda.pathfinder._utils.platform_aware import IS_WINDOWS

# CUDA Toolkit canary library used to derive the toolkit root when it is only
# visible through the dynamic loader. ``cudart`` always ships with the CTK and
# matches the anchor used by the dynamic-library CTK-root canary flow.
_CTK_ROOT_CANARY_ANCHOR_LIBNAME = "cudart"


class UnsupportedBinaryError(Exception):
def __init__(self, utility: str) -> None:
Expand All @@ -28,6 +32,71 @@ def _normalize_utility_name(utility_name: str) -> str:
return utility_name


def _is_executable_file(path: str) -> bool:
"""Return True if ``path`` is a file the OS would run as an executable.

On Windows executability is determined by the file extension (the
candidate name already carries one), so existence is sufficient. On POSIX
the execute permission bit must be set, matching ``shutil.which``.
"""
if not os.path.isfile(path):
return False
if IS_WINDOWS:
return True
return os.access(path, os.X_OK)


def _ctk_bin_subdirs(root: str) -> list[str]:
"""Return the bin directories to search under a CUDA Toolkit ``root``.

On Windows the CTK ships binaries under ``bin/x64`` (CTK 13), ``bin/x86_64``,
and ``bin`` (CTK 12); on Linux they live in ``bin``.
"""
if IS_WINDOWS:
return [
os.path.join(root, "bin", "x64"),
os.path.join(root, "bin", "x86_64"),
os.path.join(root, "bin"),
]
return [os.path.join(root, "bin")]


def _resolve_ctk_root_via_canary() -> str | None:
"""Derive the CUDA Toolkit root from the ``cudart`` canary library.

``cudart`` is resolved by the OS dynamic loader, which honors
``LD_LIBRARY_PATH`` on Linux and the native DLL search on Windows, and the
toolkit root is derived from its absolute path. The ambient ``PATH`` is
never consulted. The loader module is imported lazily to avoid pulling the
dynamic-library machinery in at import time.
"""
from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import resolve_ctk_root_via_canary

ctk_root: str | None = resolve_ctk_root_via_canary(_CTK_ROOT_CANARY_ANCHOR_LIBNAME)
return ctk_root


def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | None:
"""Resolve ``normalized_name`` against ``dirs`` only, in order.

Unlike ``shutil.which``, this never consults the current working directory
or the ambient ``PATH``. On Windows ``shutil.which`` prepends the process
CWD to the search even when an explicit ``path=`` is supplied, which lets a
binary sitting in an arbitrary CWD shadow the trusted CUDA / Conda / wheel
binary that pathfinder is contracted to discover. Searching the trusted
directories explicitly keeps the lookup deterministic and bounded.
"""
seen: set[str] = set()
for directory in dirs:
if not directory or directory in seen:
continue
seen.add(directory)
candidate = os.path.join(directory, normalized_name)
if _is_executable_file(candidate):
return candidate
return None


@functools.cache
def find_nvidia_binary_utility(utility_name: str) -> str | None:
"""Locate a CUDA binary utility executable.
Expand Down Expand Up @@ -65,6 +134,15 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
``bin/x64``, ``bin/x86_64``, and ``bin`` subdirectories on Windows,
or just ``bin`` on Linux.

4. **CTK-root canary fallback**

- Only when steps 1-3 miss: resolve the ``cudart`` library 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 it,
and search that root's bin layout. This finds the utility for users
who follow the CUDA install guide and set ``LD_LIBRARY_PATH`` for
libraries without also setting ``CUDA_HOME`` / ``CUDA_PATH``.

Note:
Results are cached using ``@functools.cache`` for performance. The cache
persists for the lifetime of the process.
Expand All @@ -73,6 +151,10 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:
(``.exe``, ``.bat``, ``.cmd``). On Unix-like systems, executables
are identified by the ``X_OK`` (execute) permission bit.

Lookup is restricted to the trusted directories and the canary-derived
CTK root listed above; the process working directory and the ambient
``PATH`` are never consulted.

Example:
>>> from cuda.pathfinder import find_nvidia_binary_utility
>>> nvdisasm = find_nvidia_binary_utility("nvdisasm")
Expand All @@ -98,10 +180,17 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None:

# 3. Search in CUDA Toolkit (CUDA_HOME/CUDA_PATH)
if (cuda_home := get_cuda_path_or_home()) is not None:
if IS_WINDOWS:
dirs.append(os.path.join(cuda_home, "bin", "x64"))
dirs.append(os.path.join(cuda_home, "bin", "x86_64"))
dirs.append(os.path.join(cuda_home, "bin"))
dirs.extend(_ctk_bin_subdirs(cuda_home))

normalized_name = _normalize_utility_name(utility_name)
return shutil.which(normalized_name, path=os.pathsep.join(dirs))
found = _resolve_in_trusted_dirs(normalized_name, dirs)
if found is not None:
return found

# 4. CTK-root canary fallback: only when the explicit trusted dirs above
# miss. Resolve cudart via the dynamic loader (honors LD_LIBRARY_PATH),
# derive the toolkit root, and search its bin layout. PATH is never used.
ctk_root = _resolve_ctk_root_via_canary()
if ctk_root is not None:
return _resolve_in_trusted_dirs(normalized_name, _ctk_bin_subdirs(ctk_root))
return None
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,26 @@ def _loadable_via_canary_subprocess(libname: str, *, timeout: float = _CANARY_PR
return _resolve_system_loaded_abs_path_in_subprocess(libname, timeout=timeout) is not None


def resolve_ctk_root_via_canary(canary_libname: str) -> str | None:
"""Resolve the CUDA Toolkit root from a system-loadable canary library.

The canary library's absolute path is resolved by the OS dynamic loader in
an isolated subprocess, which honors ``LD_LIBRARY_PATH`` on Linux and the
native DLL search on Windows. The toolkit root is then derived from that
path. Returns ``None`` if the canary cannot be resolved or no root can be
derived. The ambient ``PATH`` is never consulted.
"""
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname)
if canary_abs_path is None:
return None
ctk_root: str | None = derive_ctk_root(canary_abs_path)
return ctk_root


def _try_ctk_root_canary(ctx: SearchContext) -> str | None:
"""Try CTK-root canary fallback for descriptor-configured libraries."""
for canary_libname in ctx.desc.ctk_root_canary_anchor_libnames:
canary_abs_path = _resolve_system_loaded_abs_path_in_subprocess(canary_libname)
if canary_abs_path is None:
continue
ctk_root = derive_ctk_root(canary_abs_path)
ctk_root = resolve_ctk_root_via_canary(canary_libname)
if ctk_root is None:
continue
find = find_via_ctk_root(ctx, ctk_root)
Expand Down
27 changes: 27 additions & 0 deletions cuda_pathfinder/docs/source/release/1.6.0-notes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0

.. py:currentmodule:: cuda.pathfinder

``cuda-pathfinder`` 1.6.0 Release notes
=======================================

Highlights
----------

* :func:`find_nvidia_binary_utility` now resolves binaries through a bounded,
deterministic search of trusted directories instead of ``shutil.which``. The
process working directory and the ambient ``PATH`` are never consulted, which
closes a lookup ambiguity on Windows where ``shutil.which`` prepends the CWD
even when an explicit search path is supplied.
(`PR #2196 <https://github.com/NVIDIA/cuda-python/pull/2196>`_)

* :func:`find_nvidia_binary_utility` gains a CTK-root canary fallback. When the
NVIDIA wheel, ``CONDA_PREFIX``, and ``CUDA_HOME`` / ``CUDA_PATH`` directories
all miss, ``cudart`` is resolved through the OS dynamic loader, which honors
``LD_LIBRARY_PATH`` on Linux and the native DLL search on Windows. The CUDA
Toolkit root is derived from that path and its ``bin`` layout is searched.
This locates the utility for users who follow the CUDA installation guide and
set ``LD_LIBRARY_PATH`` for libraries without also setting ``CUDA_HOME`` /
``CUDA_PATH``, while still never falling back to ``PATH``.
(`PR #2196 <https://github.com/NVIDIA/cuda-python/pull/2196>`_)
30 changes: 30 additions & 0 deletions cuda_pathfinder/tests/test_ctk_root_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
_load_lib_no_cache,
_resolve_system_loaded_abs_path_in_subprocess,
_try_ctk_root_canary,
resolve_ctk_root_via_canary,
)
from cuda.pathfinder._dynamic_libs.search_steps import (
SearchContext,
Expand Down Expand Up @@ -369,6 +370,35 @@ def test_canary_skips_when_abs_path_none(mocker):
assert _try_ctk_root_canary(_ctx("nvvm")) is None


# ---------------------------------------------------------------------------
# resolve_ctk_root_via_canary (shared by lib and binary discovery)
# ---------------------------------------------------------------------------


def test_resolve_ctk_root_via_canary_returns_root(tmp_path, mocker):
ctk_root = tmp_path / "cuda-13"
_create_cudart_in_ctk(ctk_root)
probe = mocker.patch(
f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess",
return_value=_fake_canary_path(ctk_root),
)
assert resolve_ctk_root_via_canary("cudart") == str(ctk_root)
probe.assert_called_once_with("cudart")


def test_resolve_ctk_root_via_canary_none_when_probe_fails(mocker):
mocker.patch(f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess", return_value=None)
assert resolve_ctk_root_via_canary("cudart") is None


def test_resolve_ctk_root_via_canary_none_when_unrecognized(mocker):
mocker.patch(
f"{_MODULE}._resolve_system_loaded_abs_path_in_subprocess",
return_value=os.path.join(os.sep, "weird", "path", "libcudart.so.13"),
)
assert resolve_ctk_root_via_canary("cudart") is None


# ---------------------------------------------------------------------------
# _load_lib_no_cache search-order
# ---------------------------------------------------------------------------
Expand Down
Loading