diff --git a/docs/source/kernel-requirements.md b/docs/source/kernel-requirements.md index b9a7738a..14976c4e 100644 --- a/docs/source/kernel-requirements.md +++ b/docs/source/kernel-requirements.md @@ -18,7 +18,7 @@ maintain an older `model`-type kernel repository. `kernels` only loads kernels from a curated set of trusted publishers by default. Loading from any other publisher raises an error unless the caller -opts in with `trust_remote_code=True`: +opts in with `trust_remote_code=True` or explicitly allowlists the repository: ```python # Trusted publisher: works without opt-in. @@ -26,8 +26,18 @@ get_kernel("kernels-community/activation", version=1) # Untrusted publisher: must opt in explicitly. get_kernel("some-other-org/my-kernel", version=1, trust_remote_code=True) + +# Allow only specific repositories from untrusted publishers. +get_kernel( + "some-other-org/my-kernel", + version=1, + trust_remote_code=["some-other-org/my-kernel"], +) ``` +The repository IDs in the allowlist must match exactly. Other repositories +from the same publisher remain subject to the default trust check. + The Hub also exposes a `trustedKernelPublisher` flag on the kernel API and displays a corresponding badge in the UI. diff --git a/kernels/src/kernels/hf_hub.py b/kernels/src/kernels/hf_hub.py index 91dcc3c3..c61a74e6 100644 --- a/kernels/src/kernels/hf_hub.py +++ b/kernels/src/kernels/hf_hub.py @@ -102,22 +102,15 @@ def _check_trust_remote_code(repo_id: str, local_files_only: bool, trust_remote_ When ``trust_remote_code`` is ``True``, all repositories are allowed. - When ``trust_remote_code`` is a list of strings, it is treated as a list - of signing identities to verify against. Signing verification is not yet - implemented, so passing a list currently emits a warning and falls back - to the default trust check (i.e. only trusted publishers are allowed). + When ``trust_remote_code`` is a list of strings, it is treated as an + allowlist of repository IDs. Only repositories in the list and repositories + from trusted publishers are allowed. """ if trust_remote_code is True: return - if isinstance(trust_remote_code, list): - warnings.warn( - "Signing identity verification is not yet implemented. " - "The provided signing identities will be ignored and the " - "kernel will be treated as untrusted. Use trust_remote_code=True " - "to bypass trust checks.", - stacklevel=3, - ) + if isinstance(trust_remote_code, list) and repo_id in trust_remote_code: + return if local_files_only: # Publisher trust cannot be verified offline. The user opted into @@ -136,7 +129,8 @@ def _check_trust_remote_code(repo_id: str, local_files_only: bool, trust_remote_ except Exception: raise ValueError( f"Kernel repository '{repo_id}' could not verify publisher trust status. " - "Set trust_remote_code=True to allow loading kernels from untrusted sources." + "Set trust_remote_code=True or add the repository ID to the trust_remote_code allowlist " + "to allow loading kernels from untrusted sources." ) if getattr(info, "trustedKernelPublisher", False): @@ -144,5 +138,6 @@ def _check_trust_remote_code(repo_id: str, local_files_only: bool, trust_remote_ raise ValueError( f"Kernel repository '{repo_id}' is not from a trusted publisher. " - "Set trust_remote_code=True to allow loading kernels from untrusted sources." + "Set trust_remote_code=True or add the repository ID to the trust_remote_code allowlist " + "to allow loading kernels from untrusted sources." ) diff --git a/kernels/src/kernels/install.py b/kernels/src/kernels/install.py index 41bafe1a..3db327ea 100644 --- a/kernels/src/kernels/install.py +++ b/kernels/src/kernels/install.py @@ -42,9 +42,8 @@ def install_kernel( trust_remote_code (`bool | list[str]`, *optional*, defaults to `False`): Whether to allow loading kernels from untrusted organisations. When ``False``, only kernels from trusted organisations are allowed. When ``True``, all - repositories are allowed. A list of strings will be used to verify signing - identities in a future release; for now it emits a warning and falls - back to the default trust check. + repositories are allowed. A list of repository IDs allows only those + repositories in addition to repositories from trusted organisations. Returns: `Path`: The path to the variant directory. diff --git a/kernels/src/kernels/layer/func.py b/kernels/src/kernels/layer/func.py index dfd47c72..09c615ca 100644 --- a/kernels/src/kernels/layer/func.py +++ b/kernels/src/kernels/layer/func.py @@ -94,7 +94,9 @@ def __init__( self._repo_id = repo_id self.func_name = func_name - self._trust_remote_code = trust_remote_code + self._trust_remote_code = ( + trust_remote_code.copy() if isinstance(trust_remote_code, list) else trust_remote_code + ) # We are going to resolve these lazily, since we do not want # to do a network request for every registered FuncRepository. @@ -135,7 +137,9 @@ def __hash__(self): self._repo_id, self._revision, self._version, - self._trust_remote_code, + tuple(self._trust_remote_code) + if isinstance(self._trust_remote_code, list) + else self._trust_remote_code, ) ) @@ -304,7 +308,9 @@ def __init__( self._repo_id = repo_id self._lockfile = lockfile self.func_name = func_name - self._trust_remote_code = trust_remote_code + self._trust_remote_code = ( + trust_remote_code.copy() if isinstance(trust_remote_code, list) else trust_remote_code + ) kernel_locks, kernel_dep = self._get_lock() self.kernel_locks = kernel_locks self.kernel_dep = kernel_dep @@ -353,7 +359,9 @@ def __hash__(self): self._repo_id, self.kernel_dep, self.kernel_locks, - self._trust_remote_code, + tuple(self._trust_remote_code) + if isinstance(self._trust_remote_code, list) + else self._trust_remote_code, ) ) diff --git a/kernels/src/kernels/layer/layer.py b/kernels/src/kernels/layer/layer.py index 86d13e8f..9d1cd2cf 100644 --- a/kernels/src/kernels/layer/layer.py +++ b/kernels/src/kernels/layer/layer.py @@ -55,9 +55,10 @@ class LayerRepository: The kernel version to download. Cannot be used together with `revision`. Either `version` or `revision` must be specified. trust_remote_code (`bool | list[str]`, *optional*, defaults to `False`): - Whether to allow loading kernels from untrusted organisations. A list - of signing identities can be provided for future verification support; - until then it warns and falls back to the default trust check. + Whether to allow loading kernels from untrusted organisations. When `False`, + only kernels from trusted organisations are allowed. When `True`, all + repositories are allowed. A list of repository IDs allows only those + repositories in addition to repositories from trusted organisations. Example: ```python @@ -88,7 +89,9 @@ def __init__( self._repo_id = repo_id self.layer_name = layer_name - self._trust_remote_code = trust_remote_code + self._trust_remote_code = ( + trust_remote_code.copy() if isinstance(trust_remote_code, list) else trust_remote_code + ) # We are going to resolve these lazily, since we do not want # to do a network request for every registered LayerRepository. @@ -129,7 +132,9 @@ def __hash__(self): self._repo_id, self._revision, self._version, - self._trust_remote_code, + tuple(self._trust_remote_code) + if isinstance(self._trust_remote_code, list) + else self._trust_remote_code, ) ) @@ -213,7 +218,9 @@ def __init__( self._repo_id = repo_id self._lockfile = lockfile self.layer_name = layer_name - self._trust_remote_code = trust_remote_code + self._trust_remote_code = ( + trust_remote_code.copy() if isinstance(trust_remote_code, list) else trust_remote_code + ) kernel_locks, kernel_dep = self._get_lock() self.kernel_locks = kernel_locks self.kernel_dep = kernel_dep @@ -262,7 +269,9 @@ def __hash__(self): self._repo_id, self.kernel_dep, self.kernel_locks, - self._trust_remote_code, + tuple(self._trust_remote_code) + if isinstance(self._trust_remote_code, list) + else self._trust_remote_code, ) ) diff --git a/kernels/src/kernels/load.py b/kernels/src/kernels/load.py index a6ed7193..cf0bc087 100644 --- a/kernels/src/kernels/load.py +++ b/kernels/src/kernels/load.py @@ -125,9 +125,8 @@ def get_kernel( trust_remote_code (`bool | list[str]`, *optional*, defaults to `False`): Whether to allow loading kernels from untrusted organisations. When ``False``, only kernels from trusted organisations are allowed. When ``True``, all - repositories are allowed. A list of strings will be used to verify signing - identities in a future release; for now it emits a warning and falls - back to the default trust check. + repositories are allowed. A list of repository IDs allows only those + repositories in addition to repositories from trusted organisations. check_arch (`bool`, *optional*, defaults to `True`): Whether to check that the kernel build supports the architecture (e.g. CUDA compute capability) of the current device. Kernels can @@ -198,9 +197,8 @@ def get_local_kernel( trust_remote_code (`bool | list[str]`, *optional*, defaults to `False`): Whether to allow loading kernels from untrusted organisations. When ``False``, only kernels from trusted organisations are allowed. When ``True``, all - repositories are allowed. A list of strings will be used to verify signing - identities in a future release; for now it emits a warning and falls - back to the default trust check. + repositories are allowed. A list of repository IDs allows only those + repositories in addition to repositories from trusted organisations. @@ -257,9 +255,8 @@ def has_kernel( trust_remote_code (`bool | list[str]`, *optional*, defaults to `False`): Whether to allow loading kernels from untrusted organisations. When ``False``, only kernels from trusted organisations are allowed. When ``True``, all - repositories are allowed. A list of strings will be used to verify signing - identities in a future release; for now it emits a warning and falls - back to the default trust check. + repositories are allowed. A list of repository IDs allows only those + repositories in addition to repositories from trusted organisations. check_arch (`bool`, *optional*, defaults to `True`): Whether to check that the kernel build supports the architecture (e.g. CUDA compute capability) of the current device. Kernels can diff --git a/kernels/tests/test_basic.py b/kernels/tests/test_basic.py index 8f72480d..65df993d 100644 --- a/kernels/tests/test_basic.py +++ b/kernels/tests/test_basic.py @@ -256,6 +256,22 @@ def test_trust_remote_code_flag_allows_untrusted(): get_kernel("kernels-test-untrusted/ci-test-kernel", version=1, trust_remote_code=True) +def test_trust_remote_code_allowlist_allows_untrusted(): + """An allowlist should bypass the org check for repositories it contains.""" + repo_id = "kernels-test-untrusted/ci-test-kernel" + get_kernel(repo_id, version=1, trust_remote_code=[repo_id]) + + +def test_trust_remote_code_allowlist_blocks_unlisted(): + """A non-empty allowlist should not bypass the org check for other repositories.""" + with pytest.raises(ValueError, match=r"not from a trusted publisher"): + get_kernel( + "kernels-test-untrusted/not-a-trused-org-kernel", + version=1, + trust_remote_code=["kernels-test-untrusted/ci-test-kernel"], + ) + + def test_install_kernel_offline_with_revision(local_kernel_path): """install_kernel should resolve a cached snapshot when HF_HUB_OFFLINE=1.""" expected_path = local_kernel_path diff --git a/kernels/tests/test_layer.py b/kernels/tests/test_layer.py index 699c4a11..3857ad0d 100644 --- a/kernels/tests/test_layer.py +++ b/kernels/tests/test_layer.py @@ -594,6 +594,33 @@ def test_layer_repository_requires_version_or_revision(): LayerRepository(repo_id="kernels-test/silu-and-mul", layer_name="SiluAndMul") +def test_layer_repository_with_trust_remote_code_allowlist_is_hashable(): + allowlist = ["untrusted-org/allowed-kernel"] + repo = LayerRepository( + repo_id="untrusted-org/allowed-kernel", + layer_name="SiluAndMul", + revision="main", + trust_remote_code=allowlist, + ) + + original_hash = hash(repo) + allowlist.append("untrusted-org/another-kernel") + + assert hash(repo) == original_hash + + +def test_layer_repository_trust_remote_code_allowlist_blocks_unlisted(): + repo = LayerRepository( + repo_id="kernels-test-untrusted/not-a-trused-org-kernel", + layer_name="SiluAndMul", + revision="main", + trust_remote_code=["kernels-test-untrusted/ci-test-kernel"], + ) + + with pytest.raises(ValueError, match=r"not from a trusted publisher"): + repo.load() + + def test_validate_kernel_layer(): class BadLayer(nn.Module): def __init__(self, *args, **kwargs):