{"record":{"id":"bc23de8827109439","repo":"unslothai/unsloth","slug":"dataset-cleaned-must-not-be-a-symbolic-link","errorCode":null,"errorMessage":"Dataset '{cleaned}' must not be a symbolic link.","messagePattern":"Dataset '(.+?)' must not be a symbolic link\\.","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"studio/backend/routes/training.py","lineNumber":3616,"sourceCode":"\n\n# ── Dataset labeling (per-image caption editing) + one-click example imports ──\n# Thumbnails live in a hidden subdir so they never appear in dataset listings or the trainer's image discovery.\n_THUMBS_DIRNAME = \".thumbs\"\n_MAX_CAPTION_CHARS = 2000\n\n\ndef _resolve_dataset_folder(name: str, *, must_exist: bool = True) -> Path:\n    \"\"\"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","sourceCodeStart":3598,"sourceCodeEnd":3634,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/training.py#L3598-L3634","documentation":"HTTP 400 from _resolve_dataset_folder: the dataset directory under the Studio datasets root is itself a symbolic link. The check exists because the sibling containment check (resolve().relative_to(root)) is supplemented by an explicit symlink refusal for the folder — _safe_dataset_image_path only validates each image path, not the dataset directory. This blocks replacing a dataset with a link to arbitrary locations (e.g. /etc, another user's data).","triggerScenarios":"Any dataset route called with a dataset name whose directory under the datasets root is a symlink: GET/POST image listing, image serving (?thumb=), caption update, image delete, dataset summary. E.g. ln -s /mnt/nas/photos <datasets_root>/myset then GET /training/diffusion/dataset/myset.","commonSituations":"Users symlinking a NAS mount or another training output into the datasets directory to save disk; restoring a dataset from a backup tool that recreates directories as links; shared/multi-user setups where one user linked another's dataset.","solutions":["Replace the symlink with a real directory: remove the link, mkdir the folder, and move/copy the actual image files into it (or upload them through the API).","If the data lives on another volume, bind-mount it at the datasets root (or move the datasets root setting to that volume) instead of symlinking an individual dataset.","Recreate the dataset by uploading through the Studio upload endpoint so all files physically live under the root."],"exampleFix":"// shell fix\nmv /studio/datasets/myset /tmp/myset-link-target  # the symlink\nmkdir /studio/datasets/myset\n# copy real content in\ncp -L /tmp/myset-link-target/* /studio/datasets/myset/","handlingStrategy":"validation","validationCode":"from pathlib import Path\nfrom utils.paths import datasets_root\n\ndef dataset_is_safe(name: str) -> bool:\n    folder = datasets_root() / name\n    return folder.exists() and not folder.is_symlink() and folder.is_dir()","typeGuard":"def is_symlink_dataset_error(exc: HTTPException) -> bool:\n    return exc.status_code == 400 and 'symbolic link' in exc.detail","tryCatchPattern":"try:\n    folder = _resolve_dataset_folder(name)\nexcept HTTPException as e:\n    if e.status_code == 400 and 'symbolic link' in e.detail:\n        # tell the user to materialize the dataset; do not retry blindly\n        raise DatasetLinkError(name) from e\n    raise","preventionTips":["Never symlink external data into the datasets root; copy it or relocate the root.","Audit the datasets root after restores/migrations: find <root> -maxdepth 1 -type l.","Keep datasets managed exclusively through Studio's upload/import endpoints."],"tags":["security","symlink","path-traversal","fastapi","http-400"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}