{"record":{"id":"9f8fe7495facf453","repo":"abhigyanpatwari/GitNexus","slug":"workspace-file-changed-while-opening-entry-path","errorCode":null,"errorMessage":"workspace file changed while opening: {entry.path}","messagePattern":"workspace file changed while opening: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/runner_artifacts.py","lineNumber":175,"sourceCode":"                continue\n            if stat.S_ISLNK(metadata.st_mode):\n                snapshot[relative.as_posix()] = f\"l:{permissions:o}:{os.readlink(entry.path)}\"\n                continue\n            if not stat.S_ISREG(metadata.st_mode):\n                snapshot[relative.as_posix()] = f\"s:{metadata.st_mode}\"\n                continue\n            file_bytes += metadata.st_size\n            if file_bytes > MAX_WORKSPACE_SNAPSHOT_FILE_BYTES:\n                raise ValueError(\"workspace snapshot exceeds its bounded file-byte limit\")\n            descriptor = os.open(entry.path, os.O_RDONLY | nofollow)\n            try:\n                opened = os.fstat(descriptor)\n                if (\n                    not stat.S_ISREG(opened.st_mode)\n                    or opened.st_dev != metadata.st_dev\n                    or opened.st_ino != metadata.st_ino\n                ):\n                    raise ValueError(f\"workspace file changed while opening: {entry.path}\")\n                digest = hashlib.sha256()\n                while chunk := os.read(descriptor, 64 * 1024):\n                    digest.update(chunk)\n                after = os.fstat(descriptor)\n                if (opened.st_size, opened.st_mtime_ns) != (after.st_size, after.st_mtime_ns):\n                    raise ValueError(f\"workspace file changed while hashing: {entry.path}\")\n            finally:\n                os.close(descriptor)\n            snapshot[relative.as_posix()] = f\"f:{permissions:o}:{metadata.st_size}:{digest.hexdigest()}\"\n    return snapshot\n\n\ndef enforce_phase_workspace(\n    worktree: Path,\n    before: dict[str, str],\n    *,\n    allowed_artifact: Path,\n) -> None:","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/runner_artifacts.py#L157-L193","documentation":"Raised in workspace_snapshot (runner_artifacts.py:175) right after opening a file with O_NOFOLLOW: os.fstat on the descriptor is compared to the earlier entry.stat(), and if the type, device, or inode changed the file is considered to have been swapped between stat and open. This TOCTOU guard prevents hashing the wrong content if a symlink race replaces the file.","triggerScenarios":"Between entry.stat(follow_symlinks=False) and os.open(...|O_NOFOLLOW) the file is replaced: an attacker/model renames a new file over the path, or the OS recycles the inode. The opened descriptor's st_dev/st_ino no longer match metadata, or the opened type is not a regular file.","commonSituations":"A model or setup step rewriting files concurrently with the phase-boundary snapshot; a build tool that atomically replaces files (write temp + rename); running on a filesystem where inode reuse is aggressive.","solutions":["Re-run the snapshot when no model/build process is active (phase boundaries are meant to be quiescent — if a background process is still writing, it violates the phase contract).","Investigate the named {entry.path} to see which process rewrote it; gate the phase with a process supervisor that waits for the arm to exit before snapshotting.","If legitimate atomic replacements are frequent, take the snapshot from a filesystem freeze/snapshot (e.g. btrfs subvolume snapshot) rather than the live tree."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    snapshot = workspace_snapshot(root)\nexcept ValueError as exc:\n    if \"changed while opening\" in str(exc):\n        # A file was swapped between stat and open — a writer is active.\n        # Wait for quiescence (e.g. join the model/build process), then retry.\n        wait_for_quiescence(root)\n        snapshot = workspace_snapshot(root)\n    else:\n        raise","preventionTips":["Only snapshot when the phase is quiescent — no model/build process writing files.","Use a process supervisor that waits for child exit before the boundary check.","Avoid worktrees on filesystems where inode recycling makes swap-races common."],"tags":["toctou","integrity","workspace-snapshot"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}