{"record":{"id":"cb125e215296d8a5","repo":"abhigyanpatwari/GitNexus","slug":"generated-artifact-changed-type-while-opening-pa","errorCode":null,"errorMessage":"generated artifact changed type while opening: {path}","messagePattern":"generated artifact changed type while opening: (.+?)","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"critical","filePath":"eval/workflow_bench/runner_artifacts.py","lineNumber":442,"sourceCode":"            \":(exclude)docs/plans\",\n            \":(exclude).claude/skills\",\n        ],\n    )\n    return parse_shortstat(output)\n\n\ndef _bounded_regular_bytes(path: Path, *, limit: int) -> bytes:\n    \"\"\"Read at most ``limit`` bytes without following a generated link.\"\"\"\n\n    metadata = path.lstat()\n    if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISREG(metadata.st_mode):\n        raise RuntimeError(f\"generated artifact is not a regular non-symlink file: {path}\")\n    nofollow = getattr(os, \"O_NOFOLLOW\", 0)\n    descriptor = os.open(path, os.O_RDONLY | nofollow)\n    try:\n        opened = os.fstat(descriptor)\n        if not stat.S_ISREG(opened.st_mode):\n            raise RuntimeError(f\"generated artifact changed type while opening: {path}\")\n        chunks: list[bytes] = []\n        remaining = limit\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        return b\"\".join(chunks)\n    finally:\n        os.close(descriptor)\n\n\ndef capture_patch(sandbox: SandboxSession, worktree: Path, orig_sha: str) -> bytes:\n    \"\"\"Stream a final patch inside the sandbox while retaining a bounded prefix.\"\"\"\n\n    artifact_dir = Path(tempfile.mkdtemp(prefix=\".wfbench-artifact-\", dir=worktree))\n    artifact_dir.chmod(0o700)","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/d540b00184d71a896261ee02670da9a92d59d8f7/eval/workflow_bench/runner_artifacts.py#L424-L460","documentation":"Thrown by _bounded_regular_bytes as a TOCTOU guard: after the lstat accepted the path as a regular non-symlink file, the path is opened with O_RDONLY|O_NOFOLLOW and the descriptor is fstat'd. If the now-open fd is not a regular file, something swapped the path's target between the lstat and the open, and the read is aborted.","triggerScenarios":"os.fstat(descriptor) on the opened fd reports not S_ISREG. Between path.lstat() and os.open(...), the path was replaced (e.g., a regular file was unlinked and a special file or symlink target put in its place, then opened).","commonSituations":"A concurrent process races to swap the artifact type during capture; the agent's own finalize step rewrites the patch mid-read; a filesystem quirk where O_NOFOLLOW still opened something unexpected.","solutions":["Quiesce the worktree (agent exited, no concurrent writer) before _bounded_regular_bytes runs.","Run on a local filesystem where the open reflects the lstat atomically enough.","If the swap is a legitimate agent rewrite, ensure capture happens only after the agent has fully released the file.","Re-run the arm; genuine TOCTOU swaps require an active concurrent writer."],"exampleFix":"// before — capturing while agent still finalizing\npatch = capture_patch(sandbox, worktree, orig_sha)  # agent rewrites mid-capture\n\n// after — join agent, then capture a stable file\nagent_proc.wait()\npatch = capture_patch(sandbox, worktree, orig_sha)","handlingStrategy":"validation","validationCode":"import os, stat\nfrom pathlib import Path\n\ndef open_is_stable_type(path: Path) -> bool:\n    nofollow = getattr(os, 'O_NOFOLLOW', 0)\n    try:\n        fd = os.open(path, os.O_RDONLY | nofollow)\n    except OSError:\n        return False\n    try:\n        return stat.S_ISREG(os.fstat(fd).st_mode)\n    finally:\n        os.close(fd)","typeGuard":"null","tryCatchPattern":"try:\n    data = _bounded_regular_bytes(patch, limit=MAX_PATCH_BYTES)\nexcept RuntimeError as e:\n    if 'changed type while opening' in str(e):\n        # TOCTOU type swap; quiesce the writer and retry\n        raise\n    raise","preventionTips":["Quiesce the worktree (agent exited) before reading artifacts.","Run on a local filesystem where open reflects lstat atomically.","Treat this as evidence of an active concurrent writer — investigate, do not blindly retry.","Capture artifacts only after the producing process has closed the file."],"tags":["security","toctou","sandbox-isolation","filesystem","workflow-bench"],"backgroundTag":null,"analyzedSha":"d540b00184d71a896261ee02670da9a92d59d8f7","analyzedAt":"2026-08-12T19:50:25.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}