abhigyanpatwari/GitNexus · error · SandboxError

sandbox_copy file changed while snapshotting: {relative}

Error message

sandbox_copy file changed while snapshotting: {relative}

What it means

Raised by _copy_file after the read loop completes: if the bytes actually copied differ from the st_size recorded before the copy, or if the file's mutation identity (dev, ino, mode, size, mtime_ns, ctime_ns) changed between the pre-copy fstat and a post-copy fstat, the snapshot is rejected as inconsistent. This is a TOCTOU guard guaranteeing the captured bytes match the recorded manifest exactly.

Source

Thrown at eval/workflow_bench/task_assets.py:448

        copied = 0
        try:
            while True:
                chunk = _read_source_chunk(descriptor, COPY_CHUNK_BYTES)
                if not chunk:
                    break
                copied += len(chunk)
                if self.budget.total_bytes + copied > MAX_TASK_ASSET_BYTES:
                    raise SandboxError("sandbox_copy exceeds the total byte limit")
                digest.update(chunk)
                _write_all(output, chunk)
            captured_mode = stat.S_IMODE(before.st_mode) if self.preserve_modes else 0
            frozen_mode = 0o400 | (0o100 if self.preserve_modes and captured_mode & 0o111 else 0)
            os.fchmod(output, frozen_mode)
        finally:
            os.close(output)
        after = os.fstat(descriptor)
        if copied != before.st_size or _mutation_identity(before) != _mutation_identity(after):
            raise SandboxError(f"sandbox_copy file changed while snapshotting: {relative}")
        self.total_bytes += copied
        self.budget.total_bytes += copied
        self._record(
            AssetManifestEntry(
                path=relative,
                kind="file",
                size=copied,
                sha256=digest.hexdigest(),
                mode=captured_mode,
            )
        )

    def _copy_symlink(
        self,
        parent_descriptor: int,
        name: str,
        relative: PurePosixPath,
        before: os.stat_result,

View on GitHub (pinned to d540b00184)

Solutions

  1. Capture the snapshot against a clean, quiescent checkout of resolved_sha — stop all writers (indexers, watchers, formatters) before prepare().
  2. Use `git stash` or a fresh `git worktree add` at the resolved SHA so no unrelated process can mutate the tree.
  3. If a daemon is unavoidable, pause it for the duration of TaskAssetCache.prepare and resume after.
  4. Re-run once the working tree is stable; this is a transient race, not a declaration bug.

Example fix

# before — snapshotting a tree a formatter is touching
black --check repo/ &  # may rewrite files
snapshot = cache.prepare(task, repo=repo, resolved_sha=sha)

# after — quiesce first
wait $(jobs -p)
worktree=$(mktemp -d)
git -C "$repo" worktree add "$worktree" "$sha"
snapshot = cache.prepare(task, repo=Path(worktree), resolved_sha=sha)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
import os, time

def assert_tree_stable(repo: Path, declarations: list[str], gap: float = 0.5) -> None:
    def fingerprint():
        fp = {}
        for raw in declarations:
            for current, _, files in os.walk(repo / raw, followlinks=False):
                for name in files:
                    p = Path(current) / name
                    st = p.lstat()
                    fp[str(p)] = (st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns)
        return fp
    a = fingerprint()
    time.sleep(gap)
    b = fingerprint()
    if a != b:
        changed = [k for k in a if a.get(k) != b.get(k)]
        raise ValueError(f"tree not stable before snapshot: {changed[:5]}")

assert_tree_stable(repo_path, task["sandbox_copy"])

Try / catch

from eval.workflow_bench.propposer_sandbox import SandboxError

try:
    snapshot = cache.prepare(task, repo=repo, resolved_sha=sha)
except SandboxError as exc:
    if "changed while snapshotting" in str(exc):
        # transient race — quiesce writers and retry once; do NOT retry unchanged
        raise
    raise

Prevention

When it happens

Trigger: Any concurrent mutation of the source file between the initial fstat (captured in copy_descriptor as `before`) and the post-copy fstat: truncation, append, rewrite via atomic replace (which changes ino), or a chmod that alters mode. The check `copied != before.st_size` also catches a short read that ended early without the file growing.

Common situations: A build tool rewrites a config file mid-snapshot. A linter formats a source file. An indexer updates a database file. git operations (checkout, reset) touch files. Running the snapshot against a live working tree instead of a clean checked-out revision.

Related errors


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