{"record":{"id":"8ac4ed3a483190b5","repo":"unslothai/unsloth","slug":"dataset-path-must-be-relative-or-under-a-dataset-r-8ac4ed","errorCode":null,"errorMessage":"dataset path must be relative or under a dataset root: {raw!r}","messagePattern":"dataset path must be relative or under a dataset root: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"studio/backend/utils/paths/storage_roots.py","lineNumber":510,"sourceCode":"        strip_prefixes = (\"runs\", \"tensorboard\"),\n    )\n\n\ndef resolve_dataset_path(path_value: str) -> Path:\n    raw = str(path_value or \"\").strip()\n    if \"\\x00\" in raw:\n        raise ValueError(\"dataset path may not contain null bytes\")\n    path = Path(raw).expanduser()\n    if \"..\" in path.parts:\n        raise ValueError(f\"dataset path may not contain '..' segments: {raw!r}\")\n    if path.is_absolute():\n        for root_fn in (datasets_root, dataset_uploads_root, recipe_datasets_root):\n            try:\n                _assert_contained(path, root_fn())\n                return path\n            except ValueError:\n                continue\n        raise ValueError(f\"dataset path must be relative or under a dataset root: {raw!r}\")\n\n    parts = [part for part in Path(path_value).parts if part not in (\"\", \".\")]\n    if parts[:2] == [\"assets\", \"datasets\"]:\n        parts = parts[2:]\n    if parts and parts[0] == \"uploads\":\n        cleaned = Path(*parts[1:]) if len(parts) > 1 else Path()\n        return dataset_uploads_root() / cleaned\n    if parts and parts[0] == \"recipes\":\n        cleaned = Path(*parts[1:]) if len(parts) > 1 else Path()\n        return recipe_datasets_root() / cleaned\n\n    cleaned = Path(*parts) if parts else Path()\n    candidates = [\n        dataset_uploads_root() / cleaned,\n        recipe_datasets_root() / cleaned,\n        datasets_root() / cleaned,\n        dataset_uploads_root() / cleaned.name,\n        recipe_datasets_root() / cleaned.name,","sourceCodeStart":492,"sourceCodeEnd":528,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/utils/paths/storage_roots.py#L492-L528","documentation":"Raised by resolve_dataset_path() when the input is an absolute path that is not contained under any of the three allowed dataset roots (datasets_root(), dataset_uploads_root(), recipe_datasets_root()). The function only accepts relative paths (optionally prefixed with 'uploads/', 'recipes/', or 'assets/datasets/') or absolute paths already inside a managed root; anything else absolute is rejected to prevent reading arbitrary filesystem locations.","triggerScenarios":"Calling resolve_dataset_path('/home/user/my-data.csv') where that path is outside all configured dataset roots; also '/var/data/uploads/x' when dataset_uploads_root() points elsewhere (e.g. changed storage configuration), or an absolute path on a different drive/prefix spelling than the root (symlinked mount, trailing differences).","commonSituations":"Storage root configuration changed (moved datasets directory, different machine) while persisted absolute paths in the DB still point at the old location. Users pasting local absolute paths into a form. Case-sensitivity or symlink mismatches on macOS/Windows making a logically-inside path fail the containment assert.","solutions":["Send the path relative to the dataset root (or with the 'uploads/'/'recipes/' prefix) instead of absolute.","If the path should be valid, verify the configured dataset roots actually contain it: check datasets_root()/dataset_uploads_root()/recipe_datasets_root() return values and move/symlink the data or update configuration.","For data living outside the roots, copy or import it into a managed dataset root first, then reference it relatively."],"exampleFix":"# before\nresolve_dataset_path('/srv/old-storage/datasets/cats')  # roots moved to /srv/new-storage\n\n# after\nresolve_dataset_path('cats')  # relative to datasets_root()\n# or copy data under the new root first:\n#   shutil.copytree('/srv/old-storage/datasets/cats', datasets_root() / 'cats')","handlingStrategy":"validation","validationCode":"from utils.paths.storage_roots import datasets_root, dataset_uploads_root, recipe_datasets_root\n\ndef is_under_dataset_root(p) -> bool:\n    try:\n        p.relative_to(datasets_root()); return True\n    except ValueError: pass\n    for root in (dataset_uploads_root(), recipe_datasets_root()):\n        try:\n            p.relative_to(root); return True\n        except ValueError: pass\n    return False\n\nif Path(raw).is_absolute() and not is_under_dataset_root(Path(raw).expanduser()):\n    raise HTTPBadRequest('send dataset paths relative to the dataset root')","typeGuard":"def is_resolvable_dataset_path(raw: str) -> bool:\n    p = Path(str(raw or '').strip()).expanduser()\n    if '\\x00' in raw or '..' in p.parts:\n        return False\n    return not p.is_absolute() or is_under_dataset_root(p)","tryCatchPattern":"try:\n    path = resolve_dataset_path(raw)\nexcept ValueError as e:\n    if 'must be relative or under a dataset root' in str(e):\n        return bad_request('dataset path outside managed storage')\n    raise","preventionTips":["Persist dataset references relative to the dataset root, not absolute — storage roots move between machines.","After changing storage configuration, run a sweep over stored dataset paths to re-base any absolute ones."],"tags":["path-traversal","filesystem","configuration","validation","python"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}