{"record":{"id":"41bd9d15189923f3","repo":"abhigyanpatwari/GitNexus","slug":"compound-engineering-plugin-file-changed-during-va","errorCode":null,"errorMessage":"Compound Engineering plugin file changed during validation: {path}","messagePattern":"Compound Engineering plugin file changed during validation: (.+?)","errorType":"exception","errorClass":"SandboxError","httpStatus":null,"severity":"critical","filePath":"eval/workflow_bench/runtime_mounts.py","lineNumber":363,"sourceCode":"        yield from walk(directory, PurePosixPath(name))\n\n\ndef _bounded_plugin_bytes(path: Path) -> tuple[bytes, bool]:\n    \"\"\"Read one stable regular file without following a last-component symlink.\"\"\"\n\n    try:\n        before = path.lstat()\n    except OSError as exc:\n        raise SandboxError(f\"Compound Engineering plugin file is unreadable: {path}: {exc}\") from exc\n    if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):\n        raise SandboxError(f\"Compound Engineering plugin file must be regular and non-symlink: {path}\")\n    if before.st_size > MAX_CE_PLUGIN_FILE_BYTES:\n        raise SandboxError(f\"Compound Engineering plugin file exceeds the per-file limit: {path}\")\n    descriptor = os.open(path, os.O_RDONLY | getattr(os, \"O_NOFOLLOW\", 0))\n    try:\n        opened = os.fstat(descriptor)\n        if (opened.st_dev, opened.st_ino) != (before.st_dev, before.st_ino) or not stat.S_ISREG(opened.st_mode):\n            raise SandboxError(f\"Compound Engineering plugin file changed during validation: {path}\")\n        chunks: list[bytes] = []\n        remaining = MAX_CE_PLUGIN_FILE_BYTES + 1\n        while remaining > 0:\n            chunk = os.read(descriptor, min(64 * 1024, remaining))\n            if not chunk:\n                break\n            chunks.append(chunk)\n            remaining -= len(chunk)\n        payload = b\"\".join(chunks)\n        after = os.fstat(descriptor)\n    finally:\n        os.close(descriptor)\n    if len(payload) > MAX_CE_PLUGIN_FILE_BYTES:\n        raise SandboxError(f\"Compound Engineering plugin file exceeds the per-file limit: {path}\")\n    identity_before = (opened.st_dev, opened.st_ino, opened.st_size, opened.st_mtime_ns)\n    identity_after = (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns)\n    if identity_after != identity_before or len(payload) != after.st_size:\n        raise SandboxError(f\"Compound Engineering plugin file changed while being copied: {path}\")","sourceCodeStart":345,"sourceCodeEnd":381,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/runtime_mounts.py#L345-L381","documentation":"TOCTOU guard between lstat and the post-open fstat. After opening the path with O_RDONLY|O_NOFOLLOW, _bounded_plugin_bytes compares (st_dev, st_ino) and the regular-file flag of the opened descriptor against the pre-open lstat; any mismatch means the inode was swapped (classic symlink/replace race) and reading would be unsafe. On platforms without O_NOFOLLOW the open itself could have followed a link planted in the window.","triggerScenarios":"The plugin file was replaced, renamed, or turned into a symlink between the lstat and os.open; a concurrent writer (live build, editor autosave, package manager reinstall) mutated the tree mid-snapshot; a hostile plugin deliberately races the open.","commonSituations":"Building the plugin and running the benchmark from the same tree; a watcher (tsc --watch, vite) rewriting files; NFS or synced-folder filesystems with deferred inode stability; CI that checks out the plugin while the harness walks it.","solutions":["Freeze the plugin source before the run: produce a tarball or a fresh 'cp -a' checkout and point --ce-plugin-dir at the immutable copy.","Stop any file watcher, build daemon, or sync client that touches the plugin tree during the benchmark.","Re-run the snapshot step; transient races usually clear once the writer is quiescent.","On shared/networked filesystems, copy the plugin onto local disk (tmpfs ideal) first to remove cross-host inode jitter."],"exampleFix":"# before: build and bench share the tree\npnpm --filter ce-plugin build && wfbench run --ce-plugin-dir ./packages/ce-plugin\n# after: snapshot once, then bench the frozen copy\ncp -a ./packages/ce-plugin /tmp/ce-plugin-frozen && wfbench run --ce-plugin-dir /tmp/ce-plugin-frozen","handlingStrategy":"validation","validationCode":"from pathlib import Path\nimport shutil, os\n\n# Freeze the source into a read-only copy so no writer can race the snapshot.\nsrc = Path(\"ce-plugin\")\nfrozen = Path(\"/tmp/ce-plugin-frozen\")\nif frozen.exists():\n    shutil.rmtree(frozen)\nshutil.copytree(src, frozen, symlinks=False)\nfor root, dirs, files in os.walk(frozen):\n    Path(root).chmod(0o555)\n    for f in files:\n        Path(root, f).chmod(0o444)\n# pass frozen as --ce-plugin-dir","typeGuard":"import os, stat\nfrom pathlib import Path\n\ndef open_stable(path: Path):\n    before = path.lstat()\n    if stat.S_ISLNK(before.st_mode) or not stat.S_ISREG(before.st_mode):\n        return None\n    fd = os.open(path, os.O_RDONLY | getattr(os, \"O_NOFOLLOW\", 0))\n    opened = os.fstat(fd)\n    if (opened.st_dev, opened.st_ino) != (before.st_dev, before.st_ino):\n        os.close(fd)\n        return None\n    return fd  # caller reads and closes","tryCatchPattern":"try:\n    snapshot = _build_ce_plugin_snapshot(config, destination_parent)\nexcept SandboxError as exc:\n    if \"changed during validation\" in str(exc):\n        log.error(\"plugin tree mutated mid-snapshot; quiesce writers and retry\")\n    raise","preventionTips":["Never pass a live checkout (with active builds/watchers) as --ce-plugin-dir.","Snapshot the plugin into a read-only copy before benchmarking.","Run on local disk, not a synced/networked folder, to avoid inode instability."],"tags":["sandbox","ce-plugin","toctou","race-condition","security","workflow-bench"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}