abhigyanpatwari/GitNexus · error · SandboxError
results directory must not traverse symlinks: {root}
Error message
results directory must not traverse symlinks: {root} What it means
Even when the leaf is a real directory, _real_results_root requires root.resolve(strict=True) == root — i.e. no parent component of the path may be a symlink. If resolving the path lands on a different inode, some ancestor traverses a symlink, which the trust boundary forbids because the symlink target could be swapped under the harness.
Source
Thrown at eval/workflow_bench/evolve.py:297
mode = path.lstat().st_mode
if path.is_symlink() or not stat.S_ISREG(mode):
raise SandboxError(f"evidence source must be a regular non-symlink file: {path}")
with path.open("rb") as handle:
if path.stat().st_size > limit:
handle.seek(-limit, os.SEEK_END)
return handle.read(limit).decode(errors="replace")
def _real_results_root(results_dir: Path) -> Path:
root = results_dir.expanduser().absolute()
try:
metadata = root.lstat()
except OSError as exc:
raise SandboxError(f"results directory is unavailable: {root}: {exc}") from exc
if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
raise SandboxError(f"results directory must be a real non-symlink directory: {root}")
if root.resolve(strict=True) != root:
raise SandboxError(f"results directory must not traverse symlinks: {root}")
return root
def _results_artifact_path(root: Path, relative_value: str, *, transcript: bool) -> Path:
relative = PurePosixPath(relative_value)
expected_parts = 2 if transcript else 1
if (
relative.is_absolute()
or len(relative.parts) != expected_parts
or any(part in {"", ".", ".."} for part in relative.parts)
or (transcript and relative.parts[0] != "transcripts")
):
raise SandboxError(f"unsafe results artifact path: {relative_value!r}")
current = root
for part in relative.parts[:-1]:
current /= part
try:
metadata = current.lstat()View on GitHub (pinned to d540b00184)
Solutions
- Resolve the path yourself first and pass the realpath: `Path(...).resolve(strict=True)`.
- Remove or replace the symlinked ancestor with a real directory, or move the results tree under a non-symlinked root.
- Document the no-symlink-traversal rule for operators picking the results location.
Example fix
# before
results_dir = Path("/opt/bench/results") # /opt/bench is a symlink
# after: caller resolves to the real inode first
results_dir = Path("/opt/bench/results").resolve(strict=True)
# or point directly at the real volume
results_dir = Path("/mnt/x/results") Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def results_dir_no_symlink_traversal(path: Path) -> bool:
root = path.expanduser().absolute()
try:
return root.resolve(strict=True) == root
except OSError:
return False
# use the realpath to avoid the guard
results_dir = Path(...).resolve(strict=True) Prevention
- Resolve the results path yourself first: Path(...).resolve(strict=True).
- Keep the results tree under a non-symlinked volume root.
- Audit install prefixes (/opt, /var/lib) for symlinks before pointing the harness at them.
When it happens
Trigger: A parent directory of results_dir is a symlink: e.g. /opt/bench -> /mnt/x, and results_dir=/opt/bench/results resolves to /mnt/x/results, which differs from the as-given path.
Common situations: Standard symlinked install prefixes (/opt, /var/lib); a workspace root that is a symlink to a volume; PATH-like convenience links used in the results dir argument.
Related errors
- evidence source must be a regular non-symlink file: {path}
- results directory must be a real non-symlink directory: {roo
- results artifact parent must be a real directory: {current}
- {label} must be a real directory: {root}
- pinned GitNexus runtime component must be a real {kind}: {so
AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12).
Data as JSON: /api/errors/df888de5164fb3af.
Report an issue: GitHub.