Skip to content

Harden program-cache permissions, binary-path resolution, and coredump helper lifetime#2399

Open
rparolin wants to merge 14 commits into
NVIDIA:mainfrom
rparolin:hardening/cache-perms-loader-coredump
Open

Harden program-cache permissions, binary-path resolution, and coredump helper lifetime#2399
rparolin wants to merge 14 commits into
NVIDIA:mainfrom
rparolin:hardening/cache-perms-loader-coredump

Conversation

@rparolin

@rparolin rparolin commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Small safety fixes across three areas, each with a test. All hardening — no intentional behavior change.

What changed

cuda.core

  • The program cache writes compiled GPU code to disk. The tmp/ staging area (where in-flight writes land before the atomic rename into entries/) is now created owner-only (0o700) so other local users can't read or replace device code mid-write. Per review, root/ and entries/ intentionally inherit the umask / any pre-existing permissions so a deliberately shared kernel cache (e.g. group-writable on a compute cluster) keeps working — an existing directory is used as-is, never re-tightened.

cuda.pathfinder

  • find_nvidia_binary_utility now always returns a full absolute path, as the docs promise (it could return a relative one before).
  • On Windows, if AddDllDirectory fails, we now warn instead of failing silently.

cuda.bindings

  • Fix two memory bugs in the coredump-settings helper (_HelperCUcoredumpSettings): it kept a pointer to bytes it didn't own (dangling pointer), and never freed a 1 KB buffer (leak).

Security scope

This started as "make the whole cache tree owner-only regardless of umask." Per review it was narrowed to keep shared caches working, so the residual posture is:

  • Confidentiality — closed in practice. Committed entries are written via mkstemp (0o600) and os.replace preserves that mode, so cached kernel contents stay owner-readable even though entries/ inherits the umask. Other local users can list entry filenames (key hashes) but can't read the code.
  • Tampering on a normal single-user box — closed. Only the owner can write entries/, so no one can plant a kernel this process later loads.
  • Tampering in a deliberately shared (group-writable) cache — accepted trade-off, NOT closed. A group member with write access to entries/ could replace a cached kernel. Tamper-proofing that path needs load-time integrity checks (a separate change), so it's out of scope here. Flagging for sign-off.

Tests

Added tests for the tmp/ permissions (and that a shared root is preserved), the absolute path, and the coredump helper. Directory-permission logic verified locally; full suite on RTX 5880.

Reviewers: @leofang (core/bindings), @rwgk (pathfinder).

…er lifetime

- cuda.core: create the on-disk program cache tree owner-only (0o700) and
  re-assert restrictive perms on POSIX, so cached device code cannot be read
  or planted by other local users regardless of the inherited umask.
- cuda.pathfinder: absolutize the resolved binary-utility path to honor the
  documented absolute-path contract; surface AddDllDirectory failures on
  Windows with GetLastError instead of silently swallowing them.
- cuda.bindings: retain the caller's bytes in _HelperCUcoredumpSettings so the
  borrowed pointer cannot outlive its backing buffer, and free the getter's
  1 KiB buffer in __dealloc__; clarify get_buffer_pointer's lifetime contract.

Adds regression tests for the cache permissions, path absolutization, and
coredump helper lifetime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rparolin rparolin added this to the cuda.core next milestone Jul 21, 2026
@rparolin rparolin added bug Something isn't working cuda.bindings Everything related to the cuda.bindings module cuda.core Everything related to the cuda.core module cuda.pathfinder Everything related to the cuda.pathfinder module labels Jul 21, 2026
@copy-pr-bot

copy-pr-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@rparolin
rparolin requested review from leofang and rwgk July 21, 2026 20:40
@rparolin
rparolin marked this pull request as ready for review July 21, 2026 20:41
rparolin and others added 3 commits July 21, 2026 13:47
Shorten the explanatory comments to a line or two each and drop the internal
issue-number references (which don't resolve in this repo). No code changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It didn't actually guard the two lifetime fixes: the driver copies the path
during the set call (so the NVIDIA#379 latent UAF can't trigger), and a missing free
leaks silently (so NVIDIA#381 wouldn't fail the test either).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rparolin rparolin self-assigned this Jul 21, 2026
The test pre-creates a 0o777 cache dir to verify the loader tightens it to
0o700; the permissive mask is the point of the test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

rparolin and others added 4 commits July 21, 2026 15:34
The abspath change makes find_nvidia_binary_utility return a drive-qualified
path on Windows (C:\... ), so the three search-order tests must compare against
os.path.abspath(expected). No-op on Linux; fixes the Windows CI failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ting

The c_int/c_byte param feeders silently narrowed an out-of-range Python int
(e.g. 2**32+5 -> 5). Range-check in the feeder and raise OverflowError; declare
feed() as 'except? -1' so Cython propagates the exception instead of swallowing
it. Addresses Glasswing V9.1 (leofang chose the raise option over matching
ctypes' silent wrap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per Leo's review: on 3.13+ PyLong_AsInt converts and range-checks in one call
(raising OverflowError itself), so gate the manual INT_MIN/INT_MAX check to
pre-3.13 only. c_byte keeps the manual check (no 8-bit CPython equivalent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…flow

Per Leo's follow-up: use a single code path on all supported Pythons instead of
version-gating PyLong_AsInt. PyLong_AsLongAndOverflow flags out-of-long values
via its overflow out-param (no exception set), then we bounds-check the 32-bit
(c_int) / 8-bit (c_byte) target range and raise OverflowError. Drops the
PY_VERSION_HEX gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#2402)

The out-of-range c_int/c_byte -> OverflowError change is a distinct behavior
change; moved to a standalone PR so it can be reviewed separately from this
hardening batch. No functional change to the remaining hardening work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread cuda_bindings/cuda/bindings/_internal/utils.pyx Outdated
Comment thread cuda_core/cuda/core/utils/_program_cache/_file_stream.py Outdated
rparolin and others added 2 commits July 22, 2026 08:58
Per @leofang's review on PR NVIDIA#2399:
- _file_stream.py: only the tmp/ staging dir is created owner-only (0o700).
  root/ and entries/ now inherit the umask / any pre-existing permissions so a
  deliberately shared kernel cache (e.g. group-writable on a compute cluster)
  keeps working. Dropped the post-mkdir chmod that would have re-tightened an
  existing shared directory. 0o700 in mkdir mode needs no chmod: umask only
  clears bits.
- _internal/utils.pyx: revert the get_buffer_pointer docstring change; that is
  cybind code and is out of scope for this PR.
- Tests updated to match: assert only tmp is 0o700, and that a pre-existing
  0o777 shared root is used as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Record the residual risk from narrowing the cache-permission hardening
(glasswing NVIDIA#359/NVIDIA#375) per PR NVIDIA#2399 review: committed entry files stay 0o600
(contents owner-only via mkstemp+os.replace), tmp/ is owner-only to protect
in-flight writes, but root/entries inherit the umask so shared caches keep
working. In a group-writable shared cache a group member could replace a
cached kernel; tamper-proofing that path needs load-time integrity checks and
is out of scope here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rparolin

rparolin commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up on the security posture after narrowing the cache-permission fix (details in the updated PR description under Security scope):

  • Cached kernel contents stay owner-only (0o600 via mkstemp+os.replace), and tmp/ is owner-only, so confidentiality and in-flight tampering are covered.
  • On a normal single-user box, planting a kernel into entries/ isn't possible either.
  • In a deliberately shared, group-writable cache, a group member could replace a cached entry that this process later loads. That's the accepted trade-off for keeping shared caches working — permissions alone can't both share-write and lock out writers; closing it would need load-time integrity checks (separate change).

@leofang flagging for sign-off on accepting that residual risk (mitigated for single-user; shared-cache tampering accepted).

rparolin and others added 2 commits July 22, 2026 10:18
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rparolin

Copy link
Copy Markdown
Collaborator Author

Ready for another look — pushed changes addressing the review:

@leofang (cuda.core / cuda.bindings)

  • Cache permissions narrowed to your guidance: only tmp/ is created owner-only (0o700); root/ and entries/ inherit the umask / any pre-existing perms, so a deliberately shared cache is used as-is and never re-tightened. Dropped the post-mkdir chmod.
  • Reverted the get_buffer_pointer docstring change in _internal/utils.pyx — that's cybind, left untouched.
  • The _HelperCUcoredumpSettings dangling-pointer + 1 KB leak fix is unchanged.
  • Please also sign off on the documented residual risk under Security scope (shared group-writable cache tampering — accepted trade-off).

@rwgk (cuda.pathfinder)

  • find_nvidia_binary_utility now always returns an absolute path (honors the documented contract).
  • Windows AddDllDirectory failures now warn instead of being swallowed.

Thanks!

@rparolin
rparolin requested a review from leofang July 22, 2026 17:20
Comment on lines 73 to +86
# 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,
)

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.

Q: Why wouldn't we just call os.add_dll_directory()? What is (was, apparently it's already there) the reason that we call win32 API directly?

@leofang

leofang commented Jul 22, 2026

Copy link
Copy Markdown
Member

cuda.core LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cuda.bindings Everything related to the cuda.bindings module cuda.core Everything related to the cuda.core module cuda.pathfinder Everything related to the cuda.pathfinder module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants