{"record":{"id":"b68bb939429ce536","repo":"abhigyanpatwari/GitNexus","slug":"sandbox-copy-directory-changed-while-snapshotting","errorCode":null,"errorMessage":"sandbox_copy directory changed while snapshotting: {relative}","messagePattern":"sandbox_copy directory changed while snapshotting: (.+?)","errorType":"exception","errorClass":"SandboxError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/task_assets.py","lineNumber":410,"sourceCode":"                names = sorted(os.listdir(descriptor))\n            except OSError as exc:\n                raise SandboxError(f\"sandbox_copy directory is unreadable: {relative}: {exc}\") from exc\n            for name in names:\n                child_relative = relative / name\n                child_metadata = os.stat(name, dir_fd=descriptor, follow_symlinks=False)\n                if stat.S_ISLNK(child_metadata.st_mode):\n                    if not self.allow_symlinks:\n                        raise SandboxError(f\"sandbox_copy must not traverse a symlink: {child_relative}\")\n                    self._copy_symlink(descriptor, name, child_relative, child_metadata)\n                    continue\n                child = _open_child(descriptor, name, child_relative)\n                try:\n                    self.copy_descriptor(child, child_relative)\n                finally:\n                    os.close(child)\n            after = os.fstat(descriptor)\n            if _mutation_identity(before) != _mutation_identity(after):\n                raise SandboxError(f\"sandbox_copy directory changed while snapshotting: {relative}\")\n            return\n        if not stat.S_ISREG(before.st_mode):\n            raise SandboxError(f\"sandbox_copy accepts only regular files and directories: {relative}\")\n        self._copy_file(descriptor, relative, before)\n\n    def _record_directory(self, relative: PurePosixPath) -> None:\n        self._ensure_parents(relative.parent)\n        self._record(AssetManifestEntry(path=relative, kind=\"directory\"))\n        destination = self.destination / Path(*relative.parts)\n        destination.mkdir(mode=0o700, exist_ok=True)\n\n    def _copy_file(self, descriptor: int, relative: PurePosixPath, before: os.stat_result) -> None:\n        self._ensure_parents(relative.parent)\n        if self.budget.total_bytes + before.st_size > MAX_TASK_ASSET_BYTES:\n            raise SandboxError(\"sandbox_copy exceeds the total byte limit\")\n        destination = self.destination / Path(*relative.parts)\n        flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, \"O_CLOEXEC\", 0)\n        output = os.open(destination, flags, 0o600)","sourceCodeStart":392,"sourceCodeEnd":428,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/task_assets.py#L392-L428","documentation":"TOCTOU guard in the recursive copier: after listing and descending into children, a final os.fstat of the directory differs in _mutation_identity (dev, ino, mode, size, mtime_ns, ctime_ns) from the value taken before the listing. The directory was mutated during capture.","triggerScenarios":"Concurrent modification of a sandbox_copy source directory (entries added/removed/renamed/mode-changed) while TaskAssetCache.prepare is recursing through it.","commonSituations":"A build/install writing into the source tree during snapshot; git operations on the repo; an indexer/IDE touching files; a shared worktree across arms.","solutions":["Quiesce the source repo for the duration of prepare (no writers).","Capture from a read-only checkout or a git worktree dedicated to snapshotting.","Retry prepare after the writer stops."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import fcntl, os\n\ndef lock_source_tree(repo):\n    \"\"\"Hold an exclusive flock on the repo root for the duration of prepare.\"\"\"\n    fd = os.open(repo, os.O_RDONLY | os.O_DIRECTORY)\n    fcntl.flock(fd, fcntl.LOCK_EX)\n    return fd","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Quiesce the source repo (no writers) for the duration of TaskAssetCache.prepare.","Capture from a read-only checkout or a dedicated git worktree.","Retry prepare after stopping any concurrent writer."],"tags":["toctou","filesystem","workflow-bench","sandbox"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}