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
19 changes: 18 additions & 1 deletion cuda_core/cuda/core/utils/_program_cache/_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,7 +669,8 @@ def make_program_cache_key(
``use_pch``, ``pch_dir``) -- the cache cannot read those files on
the caller's behalf, so the caller must fingerprint the header /
PCH surface and pass it here. Callers may pass this for other
inputs too (embedded kernels, generated sources, etc.).
inputs too (embedded kernels, generated sources, etc.). Must be
``bytes``, ``bytearray``, ``memoryview``, or ``None``.

Returns
-------
Expand All @@ -686,6 +687,8 @@ def make_program_cache_key(
If ``extra_digest`` is ``None`` while ``options`` sets any option
whose compilation effect depends on external file content that the
key cannot otherwise observe.
TypeError
If ``extra_digest`` is neither ``None`` nor a bytes-like object.

Examples
--------
Expand Down Expand Up @@ -751,6 +754,20 @@ def make_program_cache_key(
# init and target_type at the top of compile); a caller that passes
# "PTX" or "C++" must get the same routing and the same cache key as
# the lowercase form.
# Check the digest's type before anything else: every guard below asks
# only whether it ``is not None``, so a non-bytes value satisfies the
# requirement to supply a digest for options that read external files
# while contributing nothing to the key. ``extra_digest=0`` is the worst
# shape -- ``bytes(0)`` is empty, so the key is byte-identical to one
# built with ``extra_digest=b""`` and is completely blind to the header
# content the caller was asked to fingerprint.
if extra_digest is not None and not isinstance(extra_digest, bytes | bytearray | memoryview):
raise TypeError(
f"extra_digest must be bytes, bytearray, memoryview, or None; got "
f"{type(extra_digest).__name__}. It satisfies the guards that require a "
f"digest for options reading external files, but contributes no digest bytes."
)

code_type = code_type.lower() if isinstance(code_type, str) else code_type
target_type = target_type.lower() if isinstance(target_type, str) else target_type
check_str_enum(code_type, SourceCodeType)
Expand Down
7 changes: 7 additions & 0 deletions cuda_core/docs/source/release/1.2.0-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@ Fixes and enhancements
Windows, both ``ctypes.CFUNCTYPE`` and ``ctypes.WINFUNCTYPE`` are accepted.
(`#2439 <https://github.com/NVIDIA/cuda-python/issues/2439>`__)

- :func:`~cuda.core.utils.make_program_cache_key` now requires ``extra_digest``
to be bytes-like. The guards that demand a digest for options reading
external files only asked whether it was not ``None``, so a non-bytes value
such as ``0`` satisfied them while contributing nothing to the key --
``bytes(0)`` is empty, so the resulting key was blind to the header contents
it was supposed to fingerprint.

Deprecation Notices
-------------------

Expand Down
54 changes: 54 additions & 0 deletions cuda_core/tests/test_program_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,60 @@ def test_make_program_cache_key_rejects(kwargs, exc_type, match):
_make_key(**kwargs)


@pytest.mark.agent_authored(model="claude-opus-5")
@pytest.mark.parametrize(
"bad",
[
# 0 is the sharp one: it satisfies every `extra_digest is not None`
# guard, and bytes(0) is empty, so the key it produces is identical to
# one built with b"" and carries no fingerprint at all.
pytest.param(0, id="int-zero"),
pytest.param(5, id="int-nonzero"),
pytest.param(-1, id="int-negative"),
pytest.param(True, id="bool"),
pytest.param("abcd", id="str"),
pytest.param(1.5, id="float"),
pytest.param(["abcd"], id="list"),
],
)
def test_make_program_cache_key_rejects_non_bytes_extra_digest(bad):
with pytest.raises(TypeError, match="extra_digest must be bytes"):
_make_key(extra_digest=bad)


@pytest.mark.agent_authored(model="claude-opus-5")
@pytest.mark.parametrize(
"digest",
[
pytest.param(b"\x01" * 32, id="bytes"),
pytest.param(bytearray(b"\x01" * 32), id="bytearray"),
pytest.param(memoryview(b"\x01" * 32), id="memoryview"),
],
)
def test_make_program_cache_key_accepts_bytes_like_extra_digest(digest):
"""All three bytes-like spellings are accepted and hash identically."""
assert _make_key(extra_digest=digest) == _make_key(extra_digest=b"\x01" * 32)


@pytest.mark.agent_authored(model="claude-opus-5")
def test_non_bytes_extra_digest_cannot_bypass_the_external_content_guard():
"""The external-content guard asks only whether extra_digest `is not None`.

A caller passing 0 -- a plausible "no digest yet" sentinel -- used to slip
past it and get a persistent key that is blind to the header contents it
was supposed to fingerprint, which is exactly what the guard exists to
prevent.
"""
with pytest.raises(ValueError, match="without an extra_digest"):
_make_key(options=_opts(include_path="/my/headers"))

with pytest.raises(TypeError, match="extra_digest must be bytes"):
_make_key(options=_opts(include_path="/my/headers"), extra_digest=0)

# A real digest is still the way through.
assert isinstance(_make_key(options=_opts(include_path="/my/headers"), extra_digest=b"\x01" * 32), bytes)


@pytest.mark.parametrize(
"code_type, code, target_type",
[
Expand Down
Loading