unslothai/unsloth · error · RuntimeError
File escaped the linked folder
Error message
File escaped the linked folder
What it means
_snapshot() opens each scanned file for copy into the RAG uploads area; before copying it resolves realpath(source) and requires the result to stay inside the linked root via _is_within. Because _scan already skips symlinked entries, an escape means the file was swapped to or overlaid by a symlink between scan and snapshot — a defense against TOCTOU symlink attacks and files moved outside the tree.
Source
Thrown at studio/backend/core/rag/folder_sync.py:1019
"inode": st.st_ino,
# A recovered identity is comparable to the next scan's, but not to os.fstat's:
# shared-folder and WebDAV drivers report different ids for the two call paths.
"identity_from_path": from_path,
}
if config.FOLDER_MAX_FILES and len(found) > config.FOLDER_MAX_FILES:
raise RuntimeError(
f"Folder contains more than the {config.FOLDER_MAX_FILES} supported files limit"
)
if _root_identity(root) != identity:
raise RuntimeError("Linked folder root identity changed during scan")
return found, identity
def _snapshot(root: str, metadata: dict) -> str:
source = metadata["path"]
resolved = os.path.realpath(source)
if not _is_within(root, resolved):
raise RuntimeError("File escaped the linked folder")
# os.fdopen already forces this descriptor binary on Windows; O_BINARY only guards a raw os.read.
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_BINARY", 0)
fd = os.open(source, flags)
target = None
try:
ext = os.path.splitext(source)[1].lower()
target = ensure_dir(rag_uploads_root()) / f"linked-{uuid.uuid4().hex}{ext}"
before = os.fstat(fd)
if not stat.S_ISREG(before.st_mode):
raise RuntimeError("Linked source is not a regular file")
expected = (
metadata["size_bytes"],
metadata["mtime_ns"],
metadata["device"],
metadata["inode"],
)
actual = (before.st_size, before.st_mtime_ns, before.st_dev, before.st_ino)
# Only an identity os.fstat can reproduce is comparable here: os.scandir reports none at allView on GitHub (pinned to 203007d190)
Solutions
- Re-run the sync when the tree is quiescent; the next scan will record the new (symlink-free) state or skip the symlinked file.
- Stop whatever is replacing files with symlinks during sync windows (schedule migrations around syncs).
- Treat repeated occurrences as a security signal: audit what is mutating the linked folder.
Defensive patterns
Strategy: validation
Validate before calling
import os
def snapshot_target_safe(root: str, source: str) -> bool:
if os.path.islink(source):
return False
resolved = os.path.realpath(source)
return os.path.normcase(os.path.commonpath([root, resolved])) == os.path.normcase(root) Try / catch
try:
ingest_folder(folder_id)
except RuntimeError as e:
if "escaped the linked folder" in str(e):
quarantine_and_alert(folder_id) # possible symlink attack; do not auto-retry
else:
raise Prevention
- Never replace files inside a linked folder with symlinks while syncing.
- Audit automated tooling that rewrites files as symlinks in shared data dirs.
- Treat repeated escapes as a security event, not a flaky failure.
When it happens
Trigger: Between _scan recording a file and _snapshot processing it, the file is replaced by a symlink pointing outside the root (classic symlink race), or the whole subtree is symlinked during an atomic swap; also hit when a bind-mounted file resolves outside on platforms where realpath crosses bind mounts.
Common situations: Malicious or clumsy automation rewriting files into symlinks while a sync runs; partial directory swaps mid-sync; overlay filesystems whose realpath escapes the root.
Related errors
- Linked source is not a regular file
- Linked folder no longer resolves to its registered path
- Linked folder changed after it was selected
- Linked folder root identity changed during scan
- Linked source changed during reconciliation
AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15).
Data as JSON: /api/errors/d7d7655c4aaa31f5.
Report an issue: GitHub.