abhigyanpatwari/GitNexus · error · ValueError

{label} changed while opening: {path}

Error message

{label} changed while opening: {path}

What it means

Thrown by _bounded_regular_bytes() in eval/workflow_bench/evolution.py as a TOCTOU defense: after lstat() it opens the file with O_NOFOLLOW, then fstat()s the descriptor and compares st_dev and st_ino against the lstat result. If they differ, someone replaced the file (symlink swap, rename race) between lstat and open, and the reader refuses to return bytes that may now point somewhere unexpected.

Source

Thrown at eval/workflow_bench/evolution.py:102


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:
        opened = os.fstat(descriptor)
        if not stat.S_ISREG(opened.st_mode) or opened.st_dev != before.st_dev or opened.st_ino != before.st_ino:
            raise ValueError(f"{label} changed while opening: {path}")
        chunks: list[bytes] = []
        remaining = limit + 1
        while remaining > 0:
            chunk = os.read(descriptor, min(64 * 1024, remaining))
            if not chunk:
                break
            chunks.append(chunk)
            remaining -= len(chunk)
        content = b"".join(chunks)
        if len(content) > limit:
            raise ValueError(f"{label} exceeds the bounded evidence limit")
        after = os.fstat(descriptor)
        if (
            opened.st_dev,
            opened.st_ino,
            opened.st_size,
            opened.st_mtime_ns,
        ) != (

View on GitHub (pinned to d540b00184)

Solutions

  1. Freeze the overlay before reading: copy with `cp -rL` to a private location and `chmod -R a-w` so nothing can mutate it.
  2. Run the bench in an isolated sandbox with no other writers having access to the path.
  3. Treat this error as a security signal — investigate provenance and reject the candidate.
  4. Retry against a stable snapshot; persistent races indicate a buggy generator that must be fixed.

Example fix

# before: overlay mutated between lstat and open
# ValueError: candidate overlay file changed while opening: /tmp/ov/x.yaml

# after: stage an immutable snapshot first
cp -rL /volatile/overlay /tmp/ov && chmod -R a-w /tmp/ov
candidate_overlay_payload(Path('/tmp/ov'))
Defensive patterns

Strategy: validation

Validate before calling

# Prevent the race: stage an immutable snapshot before the bench reads it.
import subprocess, shutil
def freeze_overlay(src: Path, dst: Path) -> Path:
    if dst.exists(): shutil.rmtree(dst)
    subprocess.run(['cp', '-rL', str(src), str(dst)], check=True)
    subprocess.run(['chmod', '-R', 'a-w', str(dst)], check=True)
    return dst
# overlay = freeze_overlay(original, Path('/tmp/ov-frozen'))

Type guard

def is_toctu_error(exc: ValueError) -> bool:
    return 'changed while opening' in str(exc)

Try / catch

try:
    candidate_overlay_payload(overlay)
except ValueError as e:
    if is_toctu_error(e):
        overlay = freeze_overlay(original_overlay, Path('/tmp/ov-frozen'))
        candidate_overlay_payload(overlay)  # retry against immutable copy
    else:
        raise

Prevention

When it happens

Trigger: An attacker or buggy concurrent writer replaces the file at the path between the lstat() check and the os.open() call (classic TOCTOU): e.g. swap a regular file for a symlink to /etc/passwd, or rename another file over the path. The descriptor's fstat dev/ino no longer match the pre-open lstat.

Common situations: Untrusted candidate directories trying to escape the sandbox via a race; a concurrent build process rewriting files mid-read; CI artifact directory being actively written when the bench starts; malicious overlay attempting to substitute privileged content.

Related errors


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