Skip to content

fix(core): require extra_digest to be bytes-like in make_program_cache_key - #2555

Open
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:core-program-cache-key-extra-digest
Open

fix(core): require extra_digest to be bytes-like in make_program_cache_key#2555
LeSingh1 wants to merge 1 commit into
NVIDIA:mainfrom
LeSingh1:core-program-cache-key-extra-digest

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Problem

make_program_cache_key requires an extra_digest whenever the options pull in file content the key cannot otherwise observe (include_path, pre_include, pch, use_pch, pch_dir; use_libdevice=True on NVVM; an NVRTC options.name with a directory component). Every one of those guards asks only whether it is not None:

if extra_digest is None:
    external = [n for n in _EXTERNAL_CONTENT_OPTIONS if _option_is_set(options, n)]
    if external:
        raise ValueError("... refuses to build a key ... without an extra_digest ...")

and the value is consumed with a bare coercion:

if extra_digest is not None:
    _update("extra_digest", bytes(extra_digest))

So a non-bytes value satisfies the requirement to supply a digest without supplying one. extra_digest=0 is the worst shape, because bytes(0) is b"":

>>> make_program_cache_key(code=src, code_type="c++", target_type="cubin",
...                        options=ProgramOptions(arch="sm_80", include_path="/my/headers"),
...                        extra_digest=0)
b'...'                                            # accepted — guard bypassed

>>> key(extra_digest=0) == key(extra_digest=b"")
True                                              # contributes nothing

The caller ends up with a persistent cache key that is completely blind to the header contents it was asked to fingerprint — precisely the failure the ValueError exists to prevent. Every edit under /my/headers is then served a stale cubin. 0 is a plausible "no digest yet" sentinel, and an int-returning hash helper (hash(...), zlib.crc32(...)) lands here just as easily.

Verified against main, with the baseline include_path case for contrast:

call result on main
include_path=..., extra_digest=None ValueError: ... without an extra_digest (correct)
include_path=..., extra_digest=0 accepted, key built
key(extra_digest=0) vs key(extra_digest=b"") identical
key(extra_digest=5) vs key(extra_digest=b"\x00"*5) identical (collision)
extra_digest=-1 ValueError: negative count
extra_digest="abc" TypeError: string argument without an encoding
extra_digest=1.5 TypeError: cannot convert 'float' object to bytes

The last three surface from inside the hasher rather than from an argument check.

Fix

Check the type up front, before any guard consults it. bytes, bytearray, memoryview, and None are accepted — the documented contract — and anything else raises TypeError, matching how this module already reports a bad code or name_expressions element type. The docstring's parameter description and Raises section are updated.

No behavior change for any bytes-like or None digest.

Tests

Added to cuda_core/tests/test_program_cache.py, beside the existing test_make_program_cache_key_rejects:

  • test_make_program_cache_key_rejects_non_bytes_extra_digest over 0, 5, -1, True, "abcd", 1.5, ["abcd"];
  • test_make_program_cache_key_accepts_bytes_like_extra_digestbytes / bytearray / memoryview are accepted and hash identically;
  • test_non_bytes_extra_digest_cannot_bypass_the_external_content_guard — pins the whole story: None still raises the ValueError, 0 now raises TypeError, and a real digest still gets through.

Verification I could and could not do

  • _keys.py needs ProgramOptions, cuda_utils, and cuda.core.typing, so I loaded it by path with stubs for those three (the version probes are already exception-tolerant via _hash_probe_failure, so a stub that raises is fine) and ran every case above against both upstream/main and the fix. The table above is from the upstream/main run; with the fix all seven rejected values raise TypeError, all three bytes-like spellings are accepted and hash identically, None is unchanged, and the include_path guard is no longer bypassable.
  • ruff check compared against an upstream/main baseline of the same file: no new findings (the file has 4 pre-existing UP038 reports under my local ruff 0.12.11; the repo pins v0.15.9 where that rule no longer exists — I used the X | Y form so the count stays at 4 either way). ruff format --check and python -m py_compile clean.
  • Not run: pytest cuda_core/tests/test_program_cache.py itself — it imports cuda.core, which is not importable here (no CUDA driver, no built extension modules). The stub run exercises the same code path the new tests do. Please treat CI as the first real run.

Related

Independent of #2553 (constructor validation in _in_memory.py / _file_stream.py) — different file, no overlap.

…e_key

Every guard around extra_digest asks only whether it ``is not None``:

    if extra_digest is None:
        external = [n for n in _EXTERNAL_CONTENT_OPTIONS if _option_is_set(options, n)]
        if external:
            raise ValueError("... refuses to build a key ... without an extra_digest ...")

and the value is consumed with a bare coercion:

    if extra_digest is not None:
        _update("extra_digest", bytes(extra_digest))

So a non-bytes value satisfies the requirement to supply a digest without
supplying one. ``extra_digest=0`` -- a plausible "no digest yet" sentinel, or
the result of an int-returning hash helper -- is the worst shape, because
``bytes(0)`` is empty:

    make_program_cache_key(..., options=ProgramOptions(include_path="/my/headers"),
                           extra_digest=0)          # accepted

    key(extra_digest=0) == key(extra_digest=b"")    # True

The caller gets a persistent cache key that is completely blind to the header
contents it was asked to fingerprint -- exactly the failure the ValueError
exists to prevent. Every edit under /my/headers is then served a stale cubin.

Related, from the same missing type check:

    key(extra_digest=5) == key(extra_digest=b"\x00" * 5)   # True -- collision
    extra_digest=-1     -> ValueError: negative count
    extra_digest="abc"  -> TypeError: string argument without an encoding
    extra_digest=1.5    -> TypeError: cannot convert 'float' object to bytes

all surfacing from inside the hasher rather than from the argument check.

Check the type up front, before any guard consults it. bytes, bytearray,
memoryview, and None are accepted, as documented; anything else raises
TypeError, matching how this module already reports a bad ``code`` or
``name_expressions`` element type.
@copy-pr-bot

copy-pr-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the cuda.core Everything related to the cuda.core module label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cuda.core Everything related to the cuda.core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant