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
8 changes: 7 additions & 1 deletion cuda_bindings/cuda/bindings/_internal/utils.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ cdef bint is_nested_sequence(data):


cdef void* get_buffer_pointer(buf, Py_ssize_t size, readonly=True) except*:
"""The caller must ensure ``buf`` is alive when the returned pointer is in use."""
"""Return a raw pointer into ``buf``.

The caller must keep ``buf`` alive AND not resize it while the pointer is in
use. The buffer export is released before this returns, so resizing a mutable
buffer (e.g. ``bytearray``) during a ``nogil`` call is a use-after-free.
Prefer immutable ``bytes``.
"""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is part of cybind (which @mdboom is improving). Let's not touch it.

cdef void* bufPtr
cdef int flags = cpython.PyBUF_ANY_CONTIGUOUS
if not readonly:
Expand Down
1 change: 1 addition & 0 deletions cuda_bindings/cuda/bindings/_lib/utils.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ cdef class _HelperCUcoredumpSettings:
cdef cydriver.CUcoredumpSettings_enum _attrib
cdef bint _is_getter
cdef size_t _size
cdef object _references # keeps caller bytes alive so _charstar stays valid

# Return values
cdef bint _bool
Expand Down
8 changes: 7 additions & 1 deletion cuda_bindings/cuda/bindings/_lib/utils.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,8 @@ cdef class _HelperCUcoredumpSettings:
self._cptr = <void*><void_ptr>self._charstar
self._size = 1024
else:
# Keep a reference so the borrowed _charstar buffer stays alive.
self._references = init_value
self._charstar = init_value
self._cptr = <void*><void_ptr>self._charstar
self._size = len(init_value)
Expand All @@ -680,7 +682,11 @@ cdef class _HelperCUcoredumpSettings:
raise TypeError('Unsupported attribute: {}'.format(attr.name))

def __dealloc__(self):
pass
# Only the getter path owns heap (the calloc'd 1024-byte buffer). The
# setter borrows caller bytes and the bool path points at &self._bool,
# so only free for the getter.
if self._is_getter:
free(self._charstar)

@property
def cptr(self):
Expand Down
13 changes: 10 additions & 3 deletions cuda_core/cuda/core/utils/_program_cache/_file_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,16 @@ def __init__(
self._entries = self._root / _ENTRIES_SUBDIR
self._tmp = self._root / _TMP_SUBDIR
self._max_size_bytes = max_size_bytes
self._root.mkdir(parents=True, exist_ok=True)
self._entries.mkdir(exist_ok=True)
self._tmp.mkdir(exist_ok=True)
# Cache holds compiled device code loaded via cuLibraryLoadData, so keep
# it owner-only (0o700) so other local users can't read or replace it.
# mkdir's mode is umask-masked and exist_ok keeps an existing dir's
# perms, so re-chmod on POSIX.
self._root.mkdir(parents=True, exist_ok=True, mode=0o700)
self._entries.mkdir(exist_ok=True, mode=0o700)
self._tmp.mkdir(exist_ok=True, mode=0o700)
if os.name != "nt":
for d in (self._root, self._entries, self._tmp):
os.chmod(d, 0o700)
Comment on lines +400 to +409

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure this is acceptable. One can imagine in a compute cluster a group of users want to share the same kernel cache. If the directory already exists, we should use it as-is without changing the permission.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self._tmp can certainly be made owner-only.

# Opportunistic startup sweep of orphaned temp files left by any
# crashed writers. Age-based so concurrent in-flight writes from
# other processes are preserved.
Expand Down
38 changes: 38 additions & 0 deletions cuda_core/tests/test_program_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# SPDX-License-Identifier: Apache-2.0

import abc
import os
import time

import pytest
Expand Down Expand Up @@ -2535,3 +2536,40 @@ def reader(tid: int) -> None:
# Internal accounting must agree with the cap and with __len__.
assert cache._total_bytes <= 4096
assert len(cache) == len(cache._entries) # no orphan entries


@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits only")
def test_program_cache_dir_created_owner_only(tmp_path):
"""#359/#375: the on-disk program cache stores raw executable device code,
so its directory tree must be created owner-only (0o700), independent of the
inherited umask, so other local principals can neither read cached kernels
nor plant a binary this process would later load."""
import stat

from cuda.core.utils._program_cache._file_stream import FileStreamProgramCache

root = tmp_path / "pc"
FileStreamProgramCache(path=root)

for d in (root, root / "entries", root / "tmp"):
mode = stat.S_IMODE(os.stat(d).st_mode)
assert mode == 0o700, f"{d} has mode {oct(mode)}"


@pytest.mark.skipif(os.name == "nt", reason="POSIX permission bits only")
def test_program_cache_dir_tightens_preexisting_world_writable(tmp_path):
"""#359: a pre-created world-writable cache root (the poisoning precondition)
must be re-tightened to 0o700; mkdir(exist_ok=True) alone would keep the
attacker-supplied permissions."""
import stat

from cuda.core.utils._program_cache._file_stream import FileStreamProgramCache

root = tmp_path / "pc"
root.mkdir()
# Simulate the attack precondition: a pre-existing world-writable cache dir.
os.chmod(root, 0o777) # noqa: S103

FileStreamProgramCache(path=root)

assert stat.S_IMODE(os.stat(root).st_mode) == 0o700
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ def _resolve_in_trusted_dirs(normalized_name: str, dirs: list[str]) -> str | Non
seen.add(directory)
candidate = os.path.join(directory, normalized_name)
if _is_executable_candidate(candidate):
return candidate
# Return an absolute path, as the docstring promises (a relative
# search dir would otherwise leak a relative result).
return os.path.abspath(candidate)
return None


Expand Down
13 changes: 12 additions & 1 deletion cuda_pathfinder/cuda/pathfinder/_dynamic_libs/load_dl_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import ctypes.wintypes
import os
import struct
import warnings
from typing import TYPE_CHECKING

from cuda.pathfinder._dynamic_libs.load_dl_common import LoadedDL
Expand Down Expand Up @@ -72,7 +73,17 @@ def add_dll_directory(dll_abs_path: str) -> None:
# Add the DLL directory to the native search path. AddDllDirectory only
# affects the LOAD_LIBRARY_SEARCH_USER_DIRS search; PATH is updated
# unconditionally below to also cover legacy dependent-DLL resolution.
kernel32.AddDllDirectory(dirpath)
cookie = kernel32.AddDllDirectory(dirpath)
if not cookie:
# Warn instead of failing silently; the PATH update below is a weaker
# fallback that newer loaders may ignore.
error_code = ctypes.GetLastError() # type: ignore[attr-defined]
warnings.warn(
f"AddDllDirectory({dirpath!r}) failed (error code: {error_code}); "
"falling back to process-global PATH mutation for dependent-DLL resolution.",
RuntimeWarning,
stacklevel=2,
)

# Update PATH as a fallback for dependent DLL resolution
curr_path = os.environ.get("PATH")
Expand Down
30 changes: 27 additions & 3 deletions cuda_pathfinder/tests/test_find_nvidia_binaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_find_binary_first_matching_dir_wins(monkeypatch, mocker):
result = find_nvidia_binary_utility("nvcc")

# Conda comes before CUDA_HOME, so the Conda hit wins and CUDA_HOME is never probed.
assert result == conda_nvcc
assert result == os.path.abspath(conda_nvcc)
assert checked == [os.path.join(site_dir, "nvcc"), conda_nvcc]


Expand All @@ -173,7 +173,7 @@ def test_find_binary_ctk_root_canary_fallback(monkeypatch, mocker):

result = find_nvidia_binary_utility("nvcc")

assert result == ctk_nvcc
assert result == os.path.abspath(ctk_nvcc)
canary_mock.assert_called_once_with()
# No earlier trusted dirs existed, so the only probe is the canary bin dir.
assert checked == [ctk_nvcc]
Expand Down Expand Up @@ -218,7 +218,7 @@ def test_find_binary_canary_not_consulted_when_found_earlier(monkeypatch, mocker

result = find_nvidia_binary_utility("nvcc")

assert result == conda_nvcc
assert result == os.path.abspath(conda_nvcc)
canary_mock.assert_not_called()


Expand Down Expand Up @@ -370,3 +370,27 @@ def test_caching_per_utility():
# them is None)
if nvdisasm1 is not None and nvcc1 is not None:
assert nvdisasm1 != nvcc1


def test_resolve_in_trusted_dirs_returns_absolute_path(tmp_path, monkeypatch, mocker):
"""#374: a match found under a relative search dir must be absolutized.

``find_nvidia_binary_utility`` documents an absolute, separator-resolved
result. A relative search dir (e.g. a relative ``CUDA_HOME``) previously
leaked a relative path that would re-resolve against a possibly different
CWD at execution time.
"""
rel_dir = os.path.join("some", "relative", "bin")
candidate = os.path.join(rel_dir, "nvcc")
mocker.patch.object(
binary_finder_module,
"_is_executable_candidate",
side_effect=lambda path: path == candidate,
)

# Anchor CWD so os.path.abspath is deterministic for the assertion.
monkeypatch.chdir(tmp_path)
result = binary_finder_module._resolve_in_trusted_dirs("nvcc", [rel_dir])

assert os.path.isabs(result)
assert result == os.path.abspath(os.path.join(str(tmp_path), candidate))
Loading