e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
396 lines
15 KiB
Python
396 lines
15 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Verification for Tauri native path signed grants.
|
|
|
|
Rust signs compact ``base64url(payload_json).base64url(hmac)`` grants. The
|
|
frontend can see and forward the grant, but cannot change it without breaking
|
|
the HMAC. The backend verifies the original payload segment bytes, then
|
|
re-stats the path before any native read.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import binascii
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import os
|
|
import stat as _stat_module
|
|
import threading
|
|
import time
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Iterable, Iterator, Mapping
|
|
|
|
LEASE_SECRET_ENV = "UNSLOTH_STUDIO_NATIVE_PATH_LEASE_SECRET"
|
|
_MAX_NATIVE_PATH_REDACTIONS = 100
|
|
_MAX_NATIVE_PATH_LABELS = 10_000
|
|
_MIN_LEASE_SECRET_BYTES = 32
|
|
|
|
_REPLAY_LOCK = threading.Lock()
|
|
_USED_NONCES: dict[str, int] = {}
|
|
_REDACTION_LOCK = threading.Lock()
|
|
_NATIVE_PATH_REDACTIONS: list[str] = []
|
|
_NATIVE_PATH_LABELS: dict[str, str] = {}
|
|
_NATIVE_PATH_ENV_LOCK = threading.Lock()
|
|
_SECRET_INIT_LOCK = threading.Lock()
|
|
_CACHED_LEASE_SECRET: bytes | None = None
|
|
_SCRUB_REFCOUNT = 0
|
|
_SCRUB_SAVED_SECRET: str | None = None
|
|
|
|
|
|
class NativePathLeaseError(ValueError):
|
|
"""Raised when a native path grant is missing, invalid, or unsafe."""
|
|
|
|
|
|
@dataclass(frozen = True)
|
|
class NativePathGrant:
|
|
operation: str
|
|
canonical_path: Path
|
|
path_kind: str
|
|
path_type: str
|
|
source_kind: str
|
|
token_id_hash: str
|
|
display_label: str
|
|
expires_at_ms: int
|
|
size_bytes: int | None
|
|
modified_ms: int | None
|
|
|
|
|
|
def native_path_leases_supported() -> bool:
|
|
try:
|
|
_decode_secret()
|
|
except NativePathLeaseError:
|
|
return False
|
|
return True
|
|
|
|
|
|
def child_env_without_native_path_secret(env: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
"""Return a child-process env with the native path lease secret removed."""
|
|
|
|
if env is None:
|
|
with _NATIVE_PATH_ENV_LOCK:
|
|
cleaned = dict(os.environ)
|
|
else:
|
|
cleaned = dict(env)
|
|
cleaned.pop(LEASE_SECRET_ENV, None)
|
|
return cleaned
|
|
|
|
|
|
def run_without_native_path_secret(target: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
|
"""Run a multiprocessing child target without the native path lease secret."""
|
|
|
|
# Runs in the spawned child: bind it to the parent's death (Linux), since
|
|
# multiprocessing children cannot be given a preexec_fn by the parent. Shared
|
|
# entrypoint for the inference/export/training/data-recipe workers.
|
|
try:
|
|
from utils.process_lifetime import bind_current_process_to_parent_lifetime
|
|
bind_current_process_to_parent_lifetime()
|
|
except Exception:
|
|
pass
|
|
|
|
global _CACHED_LEASE_SECRET, _SCRUB_SAVED_SECRET
|
|
os.environ.pop(LEASE_SECRET_ENV, None)
|
|
_CACHED_LEASE_SECRET = None
|
|
_SCRUB_SAVED_SECRET = None
|
|
return target(*args, **kwargs)
|
|
|
|
|
|
@contextmanager
|
|
def native_path_secret_removed_for_child_start() -> Iterator[None]:
|
|
global _SCRUB_REFCOUNT, _SCRUB_SAVED_SECRET, _CACHED_LEASE_SECRET
|
|
with _NATIVE_PATH_ENV_LOCK:
|
|
if _SCRUB_REFCOUNT == 0:
|
|
_SCRUB_SAVED_SECRET = os.environ.pop(LEASE_SECRET_ENV, None)
|
|
_CACHED_LEASE_SECRET = None
|
|
_SCRUB_REFCOUNT += 1
|
|
try:
|
|
yield
|
|
finally:
|
|
with _NATIVE_PATH_ENV_LOCK:
|
|
_SCRUB_REFCOUNT -= 1
|
|
if _SCRUB_REFCOUNT == 0 and _SCRUB_SAVED_SECRET is not None:
|
|
os.environ[LEASE_SECRET_ENV] = _SCRUB_SAVED_SECRET
|
|
_SCRUB_SAVED_SECRET = None
|
|
|
|
|
|
def verify_native_path_lease(
|
|
lease: str | None,
|
|
*,
|
|
operation: str,
|
|
expected_kind: str | None = None,
|
|
expected_path_type: str | None = None,
|
|
allowed_suffixes: Iterable[str] | None = None,
|
|
) -> NativePathGrant:
|
|
if not lease:
|
|
raise NativePathLeaseError("Native path grant is required.")
|
|
|
|
secret = _decode_secret()
|
|
payload_b64, signature_b64 = _split_lease(lease)
|
|
expected_signature = hmac.new(
|
|
secret,
|
|
payload_b64.encode("ascii"),
|
|
hashlib.sha256,
|
|
).digest()
|
|
supplied_signature = _b64decode(signature_b64)
|
|
if not hmac.compare_digest(expected_signature, supplied_signature):
|
|
raise NativePathLeaseError("Native path grant signature is invalid.")
|
|
|
|
payload = _decode_payload(payload_b64)
|
|
_validate_payload(payload, operation = operation, expected_kind = expected_kind)
|
|
|
|
path = Path(str(payload["canonical_path"]))
|
|
_reject_network_or_device_path(path)
|
|
try:
|
|
signed_lstat = os.lstat(path)
|
|
except OSError as exc:
|
|
raise NativePathLeaseError("Native path is no longer accessible.") from exc
|
|
if _stat_module.S_ISLNK(signed_lstat.st_mode):
|
|
raise NativePathLeaseError("Native path is no longer a regular file.")
|
|
try:
|
|
resolved = path.resolve(strict = True)
|
|
except OSError as exc:
|
|
raise NativePathLeaseError("Native path is no longer accessible.") from exc
|
|
_reject_network_or_device_path(resolved)
|
|
if not _same_native_path(resolved, path):
|
|
raise NativePathLeaseError("Native path grant no longer resolves to the selected path.")
|
|
|
|
grant = NativePathGrant(
|
|
operation = str(payload["operation"]),
|
|
canonical_path = resolved,
|
|
path_kind = str(payload["path_kind"]),
|
|
path_type = str(payload["path_type"]),
|
|
source_kind = str(payload["source_kind"]),
|
|
token_id_hash = str(payload["token_id_hash"]),
|
|
display_label = str(payload.get("display_label") or resolved.name),
|
|
expires_at_ms = _required_int(payload, "expires_at_ms"),
|
|
size_bytes = _optional_int(payload.get("size_bytes")),
|
|
modified_ms = _optional_int(payload.get("modified_ms")),
|
|
)
|
|
|
|
if expected_path_type and grant.path_type != expected_path_type:
|
|
raise NativePathLeaseError("Native path grant has the wrong path type.")
|
|
suffixes = tuple(s.lower() for s in (allowed_suffixes or ()))
|
|
if suffixes and resolved.suffix.lower() not in suffixes:
|
|
raise NativePathLeaseError("Native path grant has an unsupported file type.")
|
|
|
|
_validate_current_stat(grant)
|
|
_consume_nonce(str(payload["nonce"]), grant.expires_at_ms)
|
|
_remember_native_path_for_redaction(str(resolved), grant.display_label)
|
|
return grant
|
|
|
|
|
|
def display_label_for_native_path(value: str | None) -> str | None:
|
|
if not value:
|
|
return value
|
|
with _REDACTION_LOCK:
|
|
return _NATIVE_PATH_LABELS.get(value, value)
|
|
|
|
|
|
def is_registered_native_path_label(path_value: str | None, label: str | None) -> bool:
|
|
if not path_value or not label:
|
|
return False
|
|
with _REDACTION_LOCK:
|
|
return _NATIVE_PATH_LABELS.get(path_value) == label
|
|
|
|
|
|
def redact_native_paths(value: str) -> str:
|
|
with _REDACTION_LOCK:
|
|
paths = sorted(_NATIVE_PATH_REDACTIONS, key = len, reverse = True)
|
|
redacted = value
|
|
for path in paths:
|
|
for variant in {path, path.replace("/", "\\"), path.replace("\\", "/")}:
|
|
if variant:
|
|
redacted = redacted.replace(variant, "<native_path>")
|
|
return redacted
|
|
|
|
|
|
def _decode_secret() -> bytes:
|
|
global _CACHED_LEASE_SECRET
|
|
if _CACHED_LEASE_SECRET is not None:
|
|
return _CACHED_LEASE_SECRET
|
|
with _SECRET_INIT_LOCK:
|
|
if _CACHED_LEASE_SECRET is not None:
|
|
return _CACHED_LEASE_SECRET
|
|
with _NATIVE_PATH_ENV_LOCK:
|
|
encoded = os.environ.get(LEASE_SECRET_ENV)
|
|
if encoded is None and _SCRUB_SAVED_SECRET is not None:
|
|
encoded = _SCRUB_SAVED_SECRET
|
|
if not encoded:
|
|
raise NativePathLeaseError("Native path grants require the managed desktop backend.")
|
|
try:
|
|
secret = _b64decode(encoded)
|
|
except Exception as exc:
|
|
raise NativePathLeaseError("Native path grant secret is invalid.") from exc
|
|
if len(secret) < _MIN_LEASE_SECRET_BYTES:
|
|
raise NativePathLeaseError("Native path grant secret is invalid.")
|
|
_CACHED_LEASE_SECRET = secret
|
|
return secret
|
|
|
|
|
|
def _split_lease(lease: str) -> tuple[str, str]:
|
|
if not isinstance(lease, str):
|
|
raise NativePathLeaseError("Native path grant has an invalid format.")
|
|
try:
|
|
lease.encode("ascii")
|
|
except UnicodeEncodeError as exc:
|
|
raise NativePathLeaseError("Native path grant has an invalid format.") from exc
|
|
parts = lease.split(".")
|
|
if len(parts) != 2 or not parts[0] or not parts[1]:
|
|
raise NativePathLeaseError("Native path grant has an invalid format.")
|
|
return parts[0], parts[1]
|
|
|
|
|
|
def _decode_payload(payload_b64: str) -> dict[str, Any]:
|
|
try:
|
|
payload = json.loads(_b64decode(payload_b64).decode("utf-8"))
|
|
except Exception as exc:
|
|
raise NativePathLeaseError("Native path grant payload is invalid.") from exc
|
|
if not isinstance(payload, dict):
|
|
raise NativePathLeaseError("Native path grant payload is invalid.")
|
|
return payload
|
|
|
|
|
|
def _validate_payload(
|
|
payload: dict[str, Any], *, operation: str, expected_kind: str | None
|
|
) -> None:
|
|
required = (
|
|
"version",
|
|
"operation",
|
|
"canonical_path",
|
|
"path_kind",
|
|
"path_type",
|
|
"source_kind",
|
|
"token_id_hash",
|
|
"issued_at_ms",
|
|
"expires_at_ms",
|
|
"nonce",
|
|
)
|
|
missing = [key for key in required if key not in payload]
|
|
if missing:
|
|
raise NativePathLeaseError("Native path grant payload is missing required fields.")
|
|
if _required_int(payload, "version") != 1:
|
|
raise NativePathLeaseError("Native path grant version is unsupported.")
|
|
if payload["operation"] != operation:
|
|
raise NativePathLeaseError("Native path grant operation is invalid.")
|
|
if expected_kind and payload["path_kind"] != expected_kind:
|
|
raise NativePathLeaseError("Native path grant kind is invalid.")
|
|
now_ms = int(time.time() * 1000)
|
|
issued_at_ms = _required_int(payload, "issued_at_ms")
|
|
expires_at_ms = _required_int(payload, "expires_at_ms")
|
|
if issued_at_ms >= expires_at_ms:
|
|
raise NativePathLeaseError("Native path grant timestamps are inconsistent.")
|
|
if expires_at_ms <= now_ms:
|
|
raise NativePathLeaseError("Native path grant has expired.")
|
|
if issued_at_ms > now_ms + 30_000:
|
|
raise NativePathLeaseError("Native path grant issue time is invalid.")
|
|
for key in ("canonical_path", "nonce", "token_id_hash", "display_label"):
|
|
raw = payload.get(key)
|
|
if raw is None:
|
|
continue
|
|
if "\x00" in str(raw):
|
|
raise NativePathLeaseError("Native path grant contains invalid characters.")
|
|
|
|
|
|
def _validate_current_stat(grant: NativePathGrant) -> None:
|
|
try:
|
|
st = os.lstat(grant.canonical_path)
|
|
except OSError as exc:
|
|
raise NativePathLeaseError("Native path is no longer accessible.") from exc
|
|
if _stat_module.S_ISLNK(st.st_mode):
|
|
raise NativePathLeaseError("Native path is no longer a regular file.")
|
|
if grant.path_type == "file":
|
|
if not _stat_module.S_ISREG(st.st_mode):
|
|
raise NativePathLeaseError("Native path is no longer a regular file.")
|
|
elif grant.path_type == "directory":
|
|
if not _stat_module.S_ISDIR(st.st_mode):
|
|
raise NativePathLeaseError("Native path is no longer a directory.")
|
|
else:
|
|
raise NativePathLeaseError("Native path grant has an unsupported path type.")
|
|
|
|
if grant.size_bytes is not None and st.st_size != grant.size_bytes:
|
|
raise NativePathLeaseError("Native path changed after it was selected.")
|
|
current_modified_ms = int(st.st_mtime_ns // 1_000_000)
|
|
if grant.modified_ms is not None and current_modified_ms != grant.modified_ms:
|
|
raise NativePathLeaseError("Native path changed after it was selected.")
|
|
|
|
|
|
def _consume_nonce(nonce: str, expires_at_ms: int) -> None:
|
|
now_ms = int(time.time() * 1000)
|
|
with _REPLAY_LOCK:
|
|
for key, expiry in list(_USED_NONCES.items()):
|
|
if expiry <= now_ms:
|
|
_USED_NONCES.pop(key, None)
|
|
if nonce in _USED_NONCES:
|
|
raise NativePathLeaseError("Native path grant was already used.")
|
|
_USED_NONCES[nonce] = expires_at_ms
|
|
|
|
|
|
def _remember_native_path_for_redaction(path: str, display_label: str) -> None:
|
|
with _REDACTION_LOCK:
|
|
_NATIVE_PATH_LABELS[path] = display_label
|
|
if len(_NATIVE_PATH_LABELS) > _MAX_NATIVE_PATH_LABELS:
|
|
excess = len(_NATIVE_PATH_LABELS) - _MAX_NATIVE_PATH_LABELS
|
|
for stale_path in list(_NATIVE_PATH_LABELS.keys())[:excess]:
|
|
_NATIVE_PATH_LABELS.pop(stale_path, None)
|
|
if path in _NATIVE_PATH_REDACTIONS:
|
|
return
|
|
_NATIVE_PATH_REDACTIONS.append(path)
|
|
del _NATIVE_PATH_REDACTIONS[:-_MAX_NATIVE_PATH_REDACTIONS]
|
|
|
|
|
|
def _reject_network_or_device_path(path: Path) -> None:
|
|
text = str(path)
|
|
if os.name == "nt":
|
|
normalized = text.replace("/", "\\").lower()
|
|
if normalized.startswith("\\\\?\\"):
|
|
rest = normalized[4:]
|
|
is_local_drive = len(rest) >= 3 and rest[0].isalpha() and rest[1:3] == ":\\"
|
|
if not is_local_drive:
|
|
raise NativePathLeaseError("Network paths are not supported for native grants.")
|
|
elif normalized.startswith("\\\\"):
|
|
raise NativePathLeaseError("Network paths are not supported for native grants.")
|
|
if os.name != "nt":
|
|
for root in ("/dev", "/proc", "/sys"):
|
|
if path.is_relative_to(root):
|
|
raise NativePathLeaseError("Device and virtual filesystem paths are not supported.")
|
|
if "\x00" in text:
|
|
raise NativePathLeaseError("Native path contains invalid characters.")
|
|
|
|
|
|
def _b64decode(value: str) -> bytes:
|
|
try:
|
|
padding = "=" * (-len(value) % 4)
|
|
return base64.urlsafe_b64decode((value + padding).encode("ascii"))
|
|
except (UnicodeEncodeError, binascii.Error, ValueError) as exc:
|
|
raise NativePathLeaseError("Native path grant has an invalid format.") from exc
|
|
|
|
|
|
def _same_native_path(resolved: Path, signed: Path) -> bool:
|
|
try:
|
|
return resolved.samefile(signed)
|
|
except OSError:
|
|
return os.path.normcase(str(resolved)) == os.path.normcase(str(signed))
|
|
|
|
|
|
def _optional_int(value: Any) -> int | None:
|
|
if value is None:
|
|
return None
|
|
try:
|
|
return int(value)
|
|
except (TypeError, ValueError) as exc:
|
|
raise NativePathLeaseError("Native path grant payload is invalid.") from exc
|
|
|
|
|
|
def _required_int(payload: dict[str, Any], key: str) -> int:
|
|
raw = payload.get(key)
|
|
if raw is None:
|
|
raise NativePathLeaseError("Native path grant payload is missing required fields.")
|
|
try:
|
|
return int(raw)
|
|
except (TypeError, ValueError) as exc:
|
|
raise NativePathLeaseError("Native path grant payload is invalid.") from exc
|