abhigyanpatwari/GitNexus · error · ValueError

{label} contains an unsafe path component: {relative}

Error message

{label} contains an unsafe path component: {relative}

What it means

Thrown by _require_directory_chain() in eval/workflow_bench/evolution.py when iterating relative.parts and finding a component that is '', '.', or '..'. These components are rejected outright because they enable path traversal or no-op confusion when constructing paths inside the sandbox, without needing to resolve links.

Source

Thrown at eval/workflow_bench/evolution.py:81


def _require_real_directory(path: Path, *, label: str) -> None:
    try:
        metadata = path.lstat()
    except OSError as exc:
        raise ValueError(f"{label} is unavailable: {path}: {exc}") from exc
    if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
        raise ValueError(f"{label} must be a real non-symlink directory: {path}")


def _require_directory_chain(root: Path, relative: Path, *, label: str) -> None:
    """Validate each lexical directory without erasing links via resolve()."""

    _require_real_directory(root, label=label)
    current = root
    for part in relative.parts:
        if part in {"", ".", ".."}:
            raise ValueError(f"{label} contains an unsafe path component: {relative}")
        current /= part
        _require_real_directory(current, label=label)


def _bounded_regular_bytes(path: Path, *, limit: int, label: str) -> bytes:
    """Read one bounded regular file without following its leaf link."""

    try:
        before = path.lstat()
    except OSError as exc:
        raise ValueError(f"{label} is unreadable: {path}: {exc}") from exc
    if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):
        raise ValueError(f"{label} must be a regular non-symlink file: {path}")
    if before.st_size > limit:
        raise ValueError(f"{label} exceeds the bounded evidence limit")

    descriptor = os.open(path, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0))
    try:

View on GitHub (pinned to d540b00184)

Solutions

  1. Rebuild the overlay so every path is a clean relative path with no '.'/'..'/empty segments.
  2. Audit the candidate generator to normalize paths (PurePosixPath and reject non-normal).
  3. Re-extract tarballs defensively and drop any entry whose relative path has unsafe parts.
  4. Treat this as a security signal if the source is untrusted — investigate provenance.

Example fix

# before
files = ['../secret.yaml', 'skill/x.yaml']
# ValueError: candidate overlay directory contains an unsafe path component: ../secret.yaml

# after
files = ['skill/x.yaml']  # traversal entries dropped at generation time
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import PurePosixPath
def safe_relative(rel: str) -> PurePosixPath:
    parts = PurePosixPath(rel).parts
    if any(c in {'', '.', '..'} for c in parts):
        raise SystemExit(f'unsafe overlay path component: {rel}')
    return PurePosixPath(rel)
# validate every candidate path before passing to candidate_overlay_payload

Type guard

def is_unsafe_path_error(exc: ValueError) -> bool:
    return 'contains an unsafe path component' in str(exc)

Try / catch

try:
    candidate_overlay_payload(overlay)
except ValueError as e:
    if is_unsafe_path_error(e):
        # drop offending entries, regenerate overlay, retry
        raise
    raise

Prevention

When it happens

Trigger: A candidate overlay file whose path relative to the overlay root contains '..' or '.' or an empty segment — e.g. ../escape.yaml, ./x.yaml, or a//b.yaml. The check fires during candidate_overlay_payload() walking each file's parent chain.

Common situations: A hand-crafted or tarball-extracted candidate that includes traversal segments to escape the overlay root; buggy candidate-generation code joining paths with leading '/'; an empty path part from string splitting.

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@d540b00184 (2026-08-12). Data as JSON: /api/errors/718ab598b4f6d42f. Report an issue: GitHub.