{"record":{"id":"7e59034b35408db8","repo":"unslothai/unsloth","slug":"dataset-cleaned-escapes-the-studio-datasets-di","errorCode":null,"errorMessage":"Dataset '{cleaned}' escapes the Studio datasets directory.","messagePattern":"Dataset '(.+?)' escapes the Studio datasets directory\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"studio/backend/routes/training.py","lineNumber":3625,"sourceCode":"    \"\"\"Validate ``name`` (single component, no traversal) and resolve it under the Studio\n    datasets root. 404 when a read target is missing.\"\"\"\n    from utils.paths import datasets_root\n\n    cleaned = _clean_diffusion_dataset_name(name)\n    root = datasets_root().resolve()\n    folder = root / cleaned\n    # 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.\n    if folder.is_symlink():\n        raise HTTPException(\n            status_code = 400,\n            detail = f\"Dataset '{cleaned}' must not be a symbolic link.\",\n        )\n    if must_exist and not folder.is_dir():\n        raise HTTPException(status_code = 404, detail = f\"Dataset '{cleaned}' not found.\")\n    try:\n        folder.resolve(strict = must_exist).relative_to(root)\n    except (OSError, ValueError):\n        raise HTTPException(\n            status_code = 400,\n            detail = f\"Dataset '{cleaned}' escapes the Studio datasets directory.\",\n        )\n    return folder\n\n\n# Match diffusion's 4096px decoded-image limit.\n_MAX_TRAINING_IMAGE_SIDE = 4096\n\n\ndef _validate_uploaded_training_image(path: Path, original_name: str) -> None:\n    \"\"\"Reject an uploaded training image whose decoded dimensions exceed the per-side limit.\n\n    Reads only the header (never img.load()), so a small-payload / huge-dimension file is caught\n    before it spikes memory. Bytes PIL cannot identify are left as-is (the upload contract accepts\n    arbitrary bytes under an image extension), so only oversized real images change behaviour.\"\"\"\n    from PIL import Image, UnidentifiedImageError\n","sourceCodeStart":3607,"sourceCodeEnd":3643,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/training.py#L3607-L3643","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","solutions":["Remove any symlinks in the datasets root's path chain (both the root location and inside it) so folder.resolve() lands under root.resolve().","Point the datasets-root configuration at a stable, non-symlinked physical path and restart Studio.","Re-create the dataset as a plain directory under the root."],"exampleFix":"# find symlinks inside the datasets root and in its own path\nfind -L /studio/datasets -maxdepth 2 -type l\nreadlink -f /studio/datasets   # confirm root resolves where expected\n# replace offending links with real dirs","handlingStrategy":"validation","validationCode":"from pathlib import Path\nfrom utils.paths import datasets_root\n\ndef resolves_under_root(name: str) -> bool:\n    root = datasets_root().resolve()\n    try:\n        (root / name).resolve().relative_to(root)\n        return True\n    except (OSError, ValueError):\n        return False","typeGuard":"def is_escape_error(exc: HTTPException) -> bool:\n    return exc.status_code == 400 and 'escapes' in exc.detail","tryCatchPattern":"try:\n    folder = _resolve_dataset_folder(name)\nexcept HTTPException as e:\n    if e.status_code == 400 and 'escapes' in e.detail:\n        log_security_event(name)  # potential tampering with datasets root\n    raise","preventionTips":["Keep the datasets root on a plain physical path; avoid symlinked homes or NAS junction points in its ancestry.","Periodically audit for symlinks inside the datasets root.","Treat this error as a security signal, not a user error — investigate who created the link."],"tags":["security","path-traversal","symlink","fastapi","http-400"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}