{"record":{"id":"3bf138b5c54a5f33","repo":"unslothai/unsloth","slug":"model-path-is-outside-unsloth-storage","errorCode":null,"errorMessage":"Model path is outside Unsloth storage","messagePattern":"Model path is outside Unsloth storage","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"studio/backend/routes/models.py","lineNumber":2932,"sourceCode":"    raw_path = Path(model_path).expanduser()\n    if source == \"training\":\n        target_path = raw_path\n        allowed_root = outputs_root()\n    else:\n        allowed_root = exports_root()\n        target_path = (\n            raw_path.parent\n            if export_type == \"gguf\" and raw_path.suffix.lower() == \".gguf\"\n            else raw_path\n        )\n\n    allowed_root = allowed_root.resolve()\n    delete_path = Path(os.path.abspath(str(target_path)))\n    delete_path_is_symlink = delete_path.is_symlink()\n\n    if delete_path_is_symlink:\n        if not _is_path_under_lexically(delete_path, allowed_root):\n            raise HTTPException(\n                status_code = 400,\n                detail = \"Model path is outside Unsloth storage\",\n            )\n        if export_type == \"gguf\" and gguf_variant:\n            target_path = delete_path.resolve()\n            if not _is_path_under(target_path, allowed_root):\n                raise HTTPException(\n                    status_code = 400,\n                    detail = \"Model path is outside Unsloth storage\",\n                )\n        else:\n            target_path = delete_path\n    else:\n        target_path = target_path.resolve()\n\n    should_check_resolved_path = not delete_path_is_symlink or (\n        export_type == \"gguf\" and gguf_variant\n    )","sourceCodeStart":2914,"sourceCodeEnd":2950,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/routes/models.py#L2914-L2950","documentation":"Fires when the path to delete is a symlink and even its unresolved (lexical) absolute form does not live under the allowed storage root — outputs_root() for source=\"training\", exports_root() for source=\"exported\" (studio/backend/utils/paths/storage_roots.py:90-96). The endpoint only ever deletes inside Unsloth-managed directories, and symlinks are checked lexically first (via _is_path_under_lexically, models.py:2749) so a link pointing at /etc or the user's home is rejected before resolution. This is a path-containment / anti-traversal guard.","triggerScenarios":"model_path = \"/etc/passwd\" with source=\"training\"; model_path = \"~/.cache/huggingface/...\"; a symlink like outputs/link -> ../../secrets whose abspath spelling itself escapes the root; source set to \"training\" while passing a path that lives under the exports root (wrong root for that source).","commonSituations":"Users hand-crafting model_path instead of copying it from the studio UI/scan results; moving or re-pointing an outputs directory with symlinks after a disk change; mixing up source=\"training\" vs \"exported\" so the wrong root is enforced; attempts to use the delete endpoint as a general file-deletion utility.","solutions":["Use the exact model_path shown by the studio models list for that row (it always comes from a scan of the correct root).","Check that source matches where the model lives: \"training\" -> outputs_root(), \"exported\" -> exports_root().","If you relocated storage, fix the roots (UNSLOTH_OUTPUTS_DIR / equivalent env or settings that outputs_root()/exports_root() read) rather than symlinking from outside.","Verify containment yourself before calling: Path(model_path).expanduser().absolute() must start with the matching root."],"exampleFix":"# before\nrequests.delete('/delete-finetuned', json={'model_path': '/etc/my-model', 'source': 'training'})\n# after\nroot = Path(os.environ['UNSLOTH_OUTPUTS_DIR']).resolve()\ntarget = Path(model_path).expanduser().absolute()\nassert target == root or root in target.parents, 'outside storage'\nrequests.delete('/delete-finetuned', json={'model_path': str(target), 'source': 'training'})","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef assert_delete_allowed(model_path: str, source: str, root: Path) -> None:\n    lex = Path(os.path.abspath(str(Path(model_path).expanduser())))\n    if lex != root and root not in lex.parents:\n        raise ValueError(f'{model_path} is outside {root} (lexical symlink check)')","typeGuard":"def is_inside_storage(model_path: str, root: Path) -> bool:\n    p = Path(os.path.abspath(str(Path(model_path).expanduser())))\n    return p == root or root in p.parents","tryCatchPattern":"try: api_delete(payload) except HTTPError as e: if e.response.status_code == 400 and 'outside Unsloth storage' in e.response.json()['detail']: log_and_reselect_path() else: raise","preventionTips":["Always source model_path from the studio scan endpoint for the matching source value.","Never symlink models into storage from elsewhere; move data or reconfigure the storage roots instead.","Client-side, mirror the lexical containment check before every delete call."],"tags":["fastapi","unsloth","path-traversal","symlink","security","filesystem","model-deletion"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}