Studio (#4237)
* Rebuild Studio branch on top of main * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix security and code quality issues for Studio PR #4237 - Validate models_dir query param against allowed directory roots to prevent path traversal in /api/models/local endpoint - Replace string startswith() with Path.is_relative_to() for frontend path traversal check in serve_frontend - Sanitize SSE error messages to not leak exception details to clients (4 locations in inference.py) - Bind port-discovery socket to 127.0.0.1 instead of all interfaces in llama_cpp backend - Import datasets_root and resolve_output_dir in embedding training function to fix NameError and use managed output directory - Remove stale .gitignore entries for package-lock.json and test directories so tests can be tracked in version control - Add venv-reexecution logic to ui CLI command matching the studio command behavior * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Move models_dir path validation before try/except block The HTTPException(403) was inside the try/except Exception handler, so it would be caught and re-raised as a 500. Moving the validation before the try block ensures the 403 is returned directly and also makes the control flow clearer for static analysis (path is validated before any filesystem operations). * Use os.path.realpath + startswith for models_dir validation CodeQL py/path-injection does not recognize Path.is_relative_to() as a sanitizer. Switched to os.path.realpath + str.startswith which is a recognized sanitizer pattern in CodeQL's taint analysis. The startswith check uses root_str + os.sep to prevent prefix collisions (e.g. /app/models_evil matching /app/models). * Never pass user input to Path constructor in models_dir validation CodeQL traces taint through Path(resolved) even after a startswith barrier guard. Fix: the user-supplied models_dir is only used as a string for comparison against allowed roots. The Path object passed to _scan_models_dir comes from the trusted allowed_roots list, not from user input. This fully breaks the taint chain. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
bced78373f
commit
f08aef1804
664 changed files with 103567 additions and 4 deletions
118
studio/backend/utils/utils.py
Normal file
118
studio/backend/utils/utils.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""
|
||||
Shared backend utilities
|
||||
"""
|
||||
|
||||
import os
|
||||
import structlog
|
||||
from loggers import get_logger
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def without_hf_auth():
|
||||
"""
|
||||
Context manager to temporarily disable HuggingFace authentication.
|
||||
|
||||
Usage:
|
||||
with without_hf_auth():
|
||||
# Code that should run without cached tokens
|
||||
model_info(model_name, token=None)
|
||||
"""
|
||||
# Save environment variables
|
||||
saved_env = {}
|
||||
env_vars = ["HF_TOKEN", "HUGGINGFACE_HUB_TOKEN", "HF_HOME"]
|
||||
for var in env_vars:
|
||||
if var in os.environ:
|
||||
saved_env[var] = os.environ[var]
|
||||
del os.environ[var]
|
||||
|
||||
# Save disable flag
|
||||
saved_disable = os.environ.get("HF_HUB_DISABLE_IMPLICIT_TOKEN")
|
||||
os.environ["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = "1"
|
||||
|
||||
# Move token files temporarily
|
||||
token_files = []
|
||||
token_locations = [
|
||||
Path.home() / ".cache" / "huggingface" / "token",
|
||||
Path.home() / ".huggingface" / "token",
|
||||
]
|
||||
|
||||
for token_loc in token_locations:
|
||||
if token_loc.exists():
|
||||
temp = tempfile.NamedTemporaryFile(delete = False)
|
||||
temp.close()
|
||||
shutil.move(str(token_loc), temp.name)
|
||||
token_files.append((token_loc, temp.name))
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Restore tokens
|
||||
for original, temp in token_files:
|
||||
try:
|
||||
original.parent.mkdir(parents = True, exist_ok = True)
|
||||
shutil.move(temp, str(original))
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to restore token {original}: {e}")
|
||||
|
||||
# Restore environment
|
||||
for var, value in saved_env.items():
|
||||
os.environ[var] = value
|
||||
|
||||
if saved_disable is not None:
|
||||
os.environ["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = saved_disable
|
||||
else:
|
||||
os.environ.pop("HF_HUB_DISABLE_IMPLICIT_TOKEN", None)
|
||||
|
||||
|
||||
def format_error_message(error: Exception, model_name: str) -> str:
|
||||
"""
|
||||
Format user-friendly error messages for common issues.
|
||||
|
||||
Args:
|
||||
error: The exception that occurred
|
||||
model_name: Name of the model being loaded
|
||||
|
||||
Returns:
|
||||
User-friendly error string
|
||||
"""
|
||||
error_str = str(error).lower()
|
||||
model_short = model_name.split("/")[-1] if "/" in model_name else model_name
|
||||
|
||||
if "repository not found" in error_str or "404" in error_str:
|
||||
return f"Model '{model_short}' not found. Check the model name."
|
||||
|
||||
if "401" in error_str or "unauthorized" in error_str:
|
||||
return f"Authentication failed for '{model_short}'. Please provide a valid HF token."
|
||||
|
||||
if "gated" in error_str or "access to model" in error_str:
|
||||
return f"Model '{model_short}' requires authentication. Please provide a valid HF token."
|
||||
|
||||
if "invalid user token" in error_str:
|
||||
return "Invalid HF token. Please check your token and try again."
|
||||
|
||||
if (
|
||||
"memory" in error_str
|
||||
or "cuda" in error_str
|
||||
or "mlx" in error_str
|
||||
or "out of memory" in error_str
|
||||
):
|
||||
from utils.hardware import get_device
|
||||
|
||||
device = get_device()
|
||||
device_label = {"cuda": "GPU", "mlx": "Apple Silicon GPU", "cpu": "system"}.get(
|
||||
device.value, "GPU"
|
||||
)
|
||||
return f"Not enough {device_label} memory to load '{model_short}'. Try a smaller model or free memory."
|
||||
|
||||
# Generic fallback
|
||||
return str(error)
|
||||
Loading…
Add table
Add a link
Reference in a new issue