{"record":{"id":"012a04b9d133d56e","repo":"abhigyanpatwari/GitNexus","slug":"sandbox-copy-file-changed-while-snapshotting-rel","errorCode":null,"errorMessage":"sandbox_copy file changed while snapshotting: {relative}","messagePattern":"sandbox_copy file changed while snapshotting: (.+?)","errorType":"exception","errorClass":"SandboxError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/task_assets.py","lineNumber":448,"sourceCode":"        copied = 0\n        try:\n            while True:\n                chunk = _read_source_chunk(descriptor, COPY_CHUNK_BYTES)\n                if not chunk:\n                    break\n                copied += len(chunk)\n                if self.budget.total_bytes + copied > MAX_TASK_ASSET_BYTES:\n                    raise SandboxError(\"sandbox_copy exceeds the total byte limit\")\n                digest.update(chunk)\n                _write_all(output, chunk)\n            captured_mode = stat.S_IMODE(before.st_mode) if self.preserve_modes else 0\n            frozen_mode = 0o400 | (0o100 if self.preserve_modes and captured_mode & 0o111 else 0)\n            os.fchmod(output, frozen_mode)\n        finally:\n            os.close(output)\n        after = os.fstat(descriptor)\n        if copied != before.st_size or _mutation_identity(before) != _mutation_identity(after):\n            raise SandboxError(f\"sandbox_copy file changed while snapshotting: {relative}\")\n        self.total_bytes += copied\n        self.budget.total_bytes += copied\n        self._record(\n            AssetManifestEntry(\n                path=relative,\n                kind=\"file\",\n                size=copied,\n                sha256=digest.hexdigest(),\n                mode=captured_mode,\n            )\n        )\n\n    def _copy_symlink(\n        self,\n        parent_descriptor: int,\n        name: str,\n        relative: PurePosixPath,\n        before: os.stat_result,","sourceCodeStart":430,"sourceCodeEnd":466,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/task_assets.py#L430-L466","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Capture the snapshot against a clean, quiescent checkout of resolved_sha — stop all writers (indexers, watchers, formatters) before prepare().","Use `git stash` or a fresh `git worktree add` at the resolved SHA so no unrelated process can mutate the tree.","If a daemon is unavoidable, pause it for the duration of TaskAssetCache.prepare and resume after.","Re-run once the working tree is stable; this is a transient race, not a declaration bug."],"exampleFix":"# before — snapshotting a tree a formatter is touching\nblack --check repo/ &  # may rewrite files\nsnapshot = cache.prepare(task, repo=repo, resolved_sha=sha)\n\n# after — quiesce first\nwait $(jobs -p)\nworktree=$(mktemp -d)\ngit -C \"$repo\" worktree add \"$worktree\" \"$sha\"\nsnapshot = cache.prepare(task, repo=Path(worktree), resolved_sha=sha)","handlingStrategy":"validation","validationCode":"from pathlib import Path\nimport os, time\n\ndef assert_tree_stable(repo: Path, declarations: list[str], gap: float = 0.5) -> None:\n    def fingerprint():\n        fp = {}\n        for raw in declarations:\n            for current, _, files in os.walk(repo / raw, followlinks=False):\n                for name in files:\n                    p = Path(current) / name\n                    st = p.lstat()\n                    fp[str(p)] = (st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns)\n        return fp\n    a = fingerprint()\n    time.sleep(gap)\n    b = fingerprint()\n    if a != b:\n        changed = [k for k in a if a.get(k) != b.get(k)]\n        raise ValueError(f\"tree not stable before snapshot: {changed[:5]}\")\n\nassert_tree_stable(repo_path, task[\"sandbox_copy\"])","typeGuard":null,"tryCatchPattern":"from eval.workflow_bench.propposer_sandbox import SandboxError\n\ntry:\n    snapshot = cache.prepare(task, repo=repo, resolved_sha=sha)\nexcept SandboxError as exc:\n    if \"changed while snapshotting\" in str(exc):\n        # transient race — quiesce writers and retry once; do NOT retry unchanged\n        raise\n    raise","preventionTips":["Snapshot a dedicated clean worktree at resolved_sha rather than a live working tree.","Stop formatters, linters, indexers, and watch modes for the duration of prepare.","Treat 'changed while snapshotting' as a signal of concurrent mutation, not a declaration bug."],"tags":["sandbox","filesystem","concurrency","toctou","integrity"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}