{"record":{"id":"6379e07806ee424a","repo":"abhigyanpatwari/GitNexus","slug":"repository-root-changed-while-opening-root","errorCode":null,"errorMessage":"repository root changed while opening: {root}","messagePattern":"repository root changed while opening: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"eval/workflow_bench/promotion_apply.py","lineNumber":165,"sourceCode":"    return b\"\".join(chunks), opened.st_mode\n\n\ndef _open_repository_root(repo_root: Path) -> tuple[Path, int]:\n    root = repo_root.expanduser().absolute()\n    try:\n        metadata = root.lstat()\n        resolved = root.resolve(strict=True)\n    except OSError as exc:\n        raise ValueError(f\"repository root is unavailable: {root}\") from exc\n    if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):\n        raise ValueError(f\"repository root must be a real directory: {root}\")\n    if resolved != root:\n        raise ValueError(f\"repository root must not traverse symlinks: {root}\")\n    flags = os.O_RDONLY | os.O_DIRECTORY | getattr(os, \"O_CLOEXEC\", 0) | getattr(os, \"O_NOFOLLOW\", 0)\n    try:\n        descriptor = os.open(root, flags)\n    except OSError as exc:\n        raise ValueError(f\"repository root changed while opening: {root}\") from exc\n    try:\n        opened = os.fstat(descriptor)\n        final = root.lstat()\n        final_resolved = root.resolve(strict=True)\n\n        def identity(value: os.stat_result) -> tuple[int, int, int]:\n            return value.st_dev, value.st_ino, stat.S_IFMT(value.st_mode)\n\n        if (\n            stat.S_ISLNK(final.st_mode)\n            or not stat.S_ISDIR(opened.st_mode)\n            or not stat.S_ISDIR(final.st_mode)\n            or final_resolved != root\n            or not (identity(metadata) == identity(opened) == identity(final))\n        ):\n            raise ValueError(f\"repository root changed while opening: {root}\")\n    except OSError as exc:\n        os.close(descriptor)","sourceCodeStart":147,"sourceCodeEnd":183,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/promotion_apply.py#L147-L183","documentation":"All errors below are raised by internal helpers of `eval/workflow_bench/promotion_apply.py` and propagate to the caller of the public entry points: `apply_promoted_overlay(overlay, repo_root, *, expected_digest, expected_target_bases)`, `destination_base_digests(overlay, repo_root)`, `committed_destination_base_digests(overlay, repo_root, *, ref)` and `freeze_overlay(overlay, destination)`. The module applies promoted skill overlays across the canonical skill tree plus its shipped mirrors (`gitnexus/skills`, `gitnexus-claude-plugin/skills`) in a TOCTOU-hardened, symlink-rejecting, descriptor-bound transaction. `_open_repository_root` calls `os.open(root, flags)` with `O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW` after `lstat()`/`resolve()` succeeded. If that `os.open` raises `OSError`, the path changed between the stat and the open — a TOCTOU window where the root was replaced, deleted, had its permissions changed, or was swapped to a symlink.","triggerScenarios":"A concurrent process `mv`/`rm -rf`/`chmod` of the repository root between the initial `lstat` and the `os.open`; the root is on a filesystem that returns ESTALE (NFS) mid-call.","commonSituations":"Two CI jobs sharing one checkout; an operator re-cloning/`git clean -fdx` during a promotion; network filesystem eviction; container filesystem teardown racing the call.","solutions":["Ensure no other process mutates the repository root during the call.","Run promotion against an exclusive checkout (one job, one clone).","Retry the whole public entry once from a known-good root; if it persists, treat as an environment fault, not a data fault."],"exampleFix":"// before\napply_promoted_overlay(overlay, repo_root=root)\n// after\ndef run_with_retry(fn, attempts=3):\n    for i in range(attempts):\n        try:\n            return fn()\n        except ValueError as exc:\n            if 'changed while opening' in str(exc) and i < attempts-1:\n                continue\n            raise\nrun_with_retry(lambda: apply_promoted_overlay(overlay, repo_root=root))","handlingStrategy":"retry","validationCode":"# No pure pre-validation can defeat a race; the closest guard is an exclusive lock.\nimport fcntl\nwith open(root / '.promotion.lock', 'w') as lock:\n    fcntl.flock(lock, fcntl.LOCK_EX)\n    apply_promoted_overlay(overlay, repo_root=root)","typeGuard":null,"tryCatchPattern":"except ValueError as exc:\n    if 'changed while opening' in str(exc):\n        log.warning('root replaced during open; retrying once after stabilization')\n        time.sleep(0.2)\n        apply_promoted_overlay(overlay, repo_root=root)","preventionTips":["Serialize promotions with an flock/lockfile around the checkout.","Never run two concurrent promotions against the same root.","Use a local filesystem (not NFS) for the checkout during promotion."],"tags":["toctou","filesystem","race-condition","repository-root"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}