{"record":{"id":"d97a4d23fa0ee8d0","repo":"abhigyanpatwari/GitNexus","slug":"overlay-destination-parent-must-not-be-a-symlink","errorCode":null,"errorMessage":"overlay destination parent must not be a symlink: {target}","messagePattern":"overlay destination parent must not be a symlink: (.+?)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"critical","filePath":"eval/workflow_bench/promotion_apply.py","lineNumber":203,"sourceCode":"    except BaseException:\n        os.close(descriptor)\n        raise\n    return root, descriptor\n\n\ndef _open_target_parent(root_descriptor: int, target: PurePosixPath) -> int:\n    if target.is_absolute() or not target.parts or \"..\" in target.parts:\n        raise ValueError(f\"overlay destination escapes repository: {target}\")\n    flags = os.O_RDONLY | os.O_DIRECTORY | getattr(os, \"O_CLOEXEC\", 0) | getattr(os, \"O_NOFOLLOW\", 0)\n    current = os.dup(root_descriptor)\n    try:\n        for part in target.parts[:-1]:\n            try:\n                metadata = os.stat(part, dir_fd=current, follow_symlinks=False)\n            except OSError as exc:\n                raise ValueError(f\"overlay destination parent is unavailable: {target}\") from exc\n            if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):\n                raise ValueError(f\"overlay destination parent must not be a symlink: {target}\")\n            try:\n                child = os.open(part, flags, dir_fd=current)\n            except OSError as exc:\n                raise ValueError(f\"overlay destination parent changed while opening: {target}\") from exc\n            opened = os.fstat(child)\n            if (\n                opened.st_dev,\n                opened.st_ino,\n                stat.S_IFMT(opened.st_mode),\n            ) != (\n                metadata.st_dev,\n                metadata.st_ino,\n                stat.S_IFMT(metadata.st_mode),\n            ):\n                os.close(child)\n                raise ValueError(f\"overlay destination parent changed while opening: {target}\")\n            os.close(current)\n            current = child","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/promotion_apply.py#L185-L221","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. During the parent walk, an intermediate component is a symlink (`stat.S_ISLNK(metadata.st_mode)`) or not a directory. The walk refuses to descend through symlinks or non-directory entries — every component of the target's parent chain must be a real directory.","triggerScenarios":"An intermediate directory in a target's parent chain is a symlink, or has been replaced by a regular file/socket/etc.","commonSituations":"Someone symlinked a skill category dir for sharing across repos; a broken merge left a file where a directory was expected; a packaging tool wrote a symlink member.","solutions":["Locate the offending component: walk `target.parts[:-1]` from root, `lstat` each.","Replace the symlink with the real directory it should name (or remove it).","Re-run promotion once the parent chain is all real directories."],"exampleFix":"// before\n# root/.claude/skills/foo -> /shared/foo (symlink)\napply_promoted_overlay(overlay, repo_root=root)\n// after\nbad = root / '.claude' / 'skills' / 'foo'\nif bad.is_symlink():\n    bad.unlink(); bad.mkdir()\napply_promoted_overlay(overlay, repo_root=root)","handlingStrategy":"validation","validationCode":"def assert_parents_are_real_dirs(root, payload):\n    for rel, _ in payload:\n        for t in mirror_targets(rel):\n            cur = root\n            for part in t.parts[:-1]:\n                cur = cur / part\n                st = cur.lstat()\n                if stat.S_ISLNK(st.st_mode) or not stat.S_ISDIR(st.st_mode):\n                    raise ValueError(f'parent component not a real dir: {cur}')","typeGuard":"import stat\ndef parents_are_real_dirs(root, payload) -> bool:\n    for rel, _ in payload:\n        for t in mirror_targets(rel):\n            cur = root\n            for part in t.parts[:-1]:\n                cur = cur / part\n                try:\n                    st = cur.lstat()\n                except OSError:\n                    return False\n                if stat.S_ISLNK(st.st_mode) or not stat.S_ISDIR(st.st_mode):\n                    return False\n    return True","tryCatchPattern":"except ValueError as exc:\n    if 'parent must not be a symlink' in str(exc):\n        raise SystemExit(f'refuse to descend through symlink in target chain: {exc}')","preventionTips":["Never symlink any directory in the skill mirror tree; the walk forbids it.","Validate the parent chain is all real dirs before applying.","Treat a symlink in the chain as a tamper signal and refuse, not auto-fix."],"tags":["security","symlink","filesystem","validation","overlay"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}