unslothai/unsloth · error · ValueError
path escapes root: {resolved!s} -> {resolved_real!s} is not
Error message
path escapes root: {resolved!s} -> {resolved_real!s} is not under {root_real!s} What it means
Raised by _assert_contained in storage_roots.py:388-392 when the realpath of the resolved candidate does not live under the realpath of the root — the classic path-traversal containment check. Even though a relative path is joined onto root, symlinked directories inside the root can point outside, and realpath exposes that. The message shows resolved -> resolved_real vs root_real.
Source
Thrown at studio/backend/utils/paths/storage_roots.py:388
def _is_absolute_user_path(path: Path) -> bool:
expanded = str(path)
if os.name == "nt":
return path.is_absolute() and PureWindowsPath(expanded).is_absolute()
return path.is_absolute() and PurePosixPath(expanded).is_absolute()
def _assert_contained(resolved: Path, root: Path) -> None:
"""Raise ValueError if ``resolved`` realpaths outside ``root``."""
try:
resolved_real = Path(os.path.realpath(resolved))
root_real = Path(os.path.realpath(root))
except OSError as exc:
raise ValueError(f"path resolution failed: {exc}") from exc
try:
resolved_real.relative_to(root_real)
except ValueError as exc:
raise ValueError(
f"path escapes root: {resolved!s} -> {resolved_real!s} " f"is not under {root_real!s}"
) from exc
def resolve_under_root(
path_value: str | None,
*,
root: Path,
strip_prefixes: tuple[str, ...] = (),
) -> Path:
"""Resolve ``path_value`` and assert the result is under ``root``.
Absolutes are accepted only if already contained (so pre-resolved
internal paths re-enter idempotently); schemas reject absolutes upstream.
"""
if not path_value or not str(path_value).strip():
return root
View on GitHub (pinned to 203007d190)
Solutions
- Point the resolver at the realpath of the location: configure the root to the actual target of the symlink, or move data physically under the root
- If the user needs files on another volume, mount/bind that volume directly under the root rather than symlinking out
- For legitimate absolute paths, use resolve_export_write_dir which intentionally passes absolutes through
- Catch ValueError and surface a clear 'paths must stay under <root>' message to the user
Example fix
# before (root/models -> symlink to /data/models)
resolve_under_root("models/llama.gguf", root=Path("/srv/studio"))
# ValueError: path escapes root: /srv/studio/models/llama.gguf -> /data/models/llama.gguf ...
# after: mount the volume under the root, or configure the real target
resolve_under_root("llama.gguf", root=Path("/data/models").resolve()) Defensive patterns
Strategy: validation
Validate before calling
def contained_under(path_str: str, root) -> bool:
try:
p = Path(os.path.realpath(Path(root) / path_str))
r = Path(os.path.realpath(root))
p.relative_to(r)
return True
except (ValueError, OSError):
return False Try / catch
try:
path = resolve_under_root(value, root=root)
except ValueError as exc:
if "path escapes root" in str(exc):
raise HTTPException(400, f"paths must stay under {root}") from exc
raise Prevention
- Never symlink from inside a storage root to outside locations
- Configure roots by their realpath
- Use resolve_export_write_dir for legitimate cross-volume writes
- Treat this error as a blocked traversal attempt and log it
When it happens
Trigger: resolve_under_root("models/foo", root=...) where root/models is a symlink to /etc; absolute inputs that are inside string-wise but resolve elsewhere after realpath; Windows subst/junction mappings under the root; also a user-supplied absolute path outside the root passed to a resolver that accepts absolutes only when contained.
Common situations: Users symlink their model folder to another drive to save space and the containment check correctly blocks it; security scanners probing traversal; roots configured with symlinked children; docker volume mounts whose realpath differs from the configured root string.
Related errors
- local cache path must not contain '..' segments
- Directory not allowed
- Path is not in the browseable allowlist. Register it via POS
- Model path is outside Unsloth storage
- Linked folder is no longer a regular directory
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/96079d1dcf9c2c70.
Report an issue: GitHub.