unslothai/unsloth · error · HTTPException

Dataset '{cleaned}' escapes the Studio datasets directory.

Error message

Dataset '{cleaned}' escapes the Studio datasets directory.

What it means

HTTP 400 from _resolve_dataset_folder: the resolved dataset folder does not stay under the resolved datasets root (resolve(strict=...).relative_to(root) raised ValueError) or resolution itself failed (OSError). Despite the name passing the single-component cleaning and not being a symlink, its real path escapes the root — e.g. an intermediate component is a symlink, or the root itself moved between resolve() calls.

Source

Thrown at studio/backend/routes/training.py:3625

    """Validate ``name`` (single component, no traversal) and resolve it under the Studio
    datasets root. 404 when a read target is missing."""
    from utils.paths import datasets_root

    cleaned = _clean_diffusion_dataset_name(name)
    root = datasets_root().resolve()
    folder = root / cleaned
    # Reject a symlinked dataset directory and prove the resolved folder stays under root: _safe_dataset_image_path only checks each image path, not the folder.
    if folder.is_symlink():
        raise HTTPException(
            status_code = 400,
            detail = f"Dataset '{cleaned}' must not be a symbolic link.",
        )
    if must_exist and not folder.is_dir():
        raise HTTPException(status_code = 404, detail = f"Dataset '{cleaned}' not found.")
    try:
        folder.resolve(strict = must_exist).relative_to(root)
    except (OSError, ValueError):
        raise HTTPException(
            status_code = 400,
            detail = f"Dataset '{cleaned}' escapes the Studio datasets directory.",
        )
    return folder


# Match diffusion's 4096px decoded-image limit.
_MAX_TRAINING_IMAGE_SIDE = 4096


def _validate_uploaded_training_image(path: Path, original_name: str) -> None:
    """Reject an uploaded training image whose decoded dimensions exceed the per-side limit.

    Reads only the header (never img.load()), so a small-payload / huge-dimension file is caught
    before it spikes memory. Bytes PIL cannot identify are left as-is (the upload contract accepts
    arbitrary bytes under an image extension), so only oversized real images change behaviour."""
    from PIL import Image, UnidentifiedImageError

View on GitHub (pinned to 203007d190)

Solutions

  1. Remove any symlinks in the datasets root's path chain (both the root location and inside it) so folder.resolve() lands under root.resolve().
  2. Point the datasets-root configuration at a stable, non-symlinked physical path and restart Studio.
  3. Re-create the dataset as a plain directory under the root.

Example fix

# find symlinks inside the datasets root and in its own path
find -L /studio/datasets -maxdepth 2 -type l
readlink -f /studio/datasets   # confirm root resolves where expected
# replace offending links with real dirs
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
from utils.paths import datasets_root

def resolves_under_root(name: str) -> bool:
    root = datasets_root().resolve()
    try:
        (root / name).resolve().relative_to(root)
        return True
    except (OSError, ValueError):
        return False

Type guard

def is_escape_error(exc: HTTPException) -> bool:
    return exc.status_code == 400 and 'escapes' in exc.detail

Try / catch

try:
    folder = _resolve_dataset_folder(name)
except HTTPException as e:
    if e.status_code == 400 and 'escapes' in e.detail:
        log_security_event(name)  # potential tampering with datasets root
    raise

Prevention

When it happens

Trigger: A dataset name that survives _clean_diffusion_dataset_name but whose folder, once resolved, is not under the resolved root. Realistic triggers: a parent inside the datasets root is a symlink pointing elsewhere; the datasets root path itself contains a symlink so root was resolved to a different prefix than folder.resolve(); TOCTOU where the folder is swapped for a link between the is_symlink() check and resolve().

Common situations: Datasets root placed under a symlinked home dir (e.g. /var/www -> /srv/www) so prefix comparison mismatches; NAS-managed directories replaced by links mid-session; unusual mount layouts after container volume remapping.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/7e59034b35408db8. Report an issue: GitHub.