{"record":{"id":"19777bdaed6826ed","repo":"affaan-m/ECC","slug":"output-artifact-must-be-a-regular-file-relative","errorCode":null,"errorMessage":"output artifact must be a regular file: {relative}","messagePattern":"output artifact must be a regular file: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-application/scripts/tasteforge/workflow.py","lineNumber":249,"sourceCode":"                opened.append(self._open_dir((directory,), create=True))\n        finally:\n            for descriptor in opened:\n                os.close(descriptor)\n\n    def write_json(self, relative: str, payload: Any) -> None:\n        path = Path(relative)\n        if path.is_absolute() or not path.name or any(part in {\".\", \"..\"} for part in path.parts):\n            raise ValueError(\"artifact path must stay beneath output root\")\n        parent_fd = self._open_dir(tuple(path.parts[:-1]), create=False)\n        temporary = f\".{path.name}.tmp-{secrets.token_hex(8)}\"\n        descriptor = -1\n        try:\n            try:\n                existing = os.stat(path.name, dir_fd=parent_fd, follow_symlinks=False)\n            except FileNotFoundError:\n                existing = None\n            if existing is not None and not stat.S_ISREG(existing.st_mode):\n                raise ValueError(f\"output artifact must be a regular file: {relative}\")\n            data = json.dumps(payload, indent=2, sort_keys=True).encode(\"utf-8\") + b\"\\n\"\n            descriptor = os.open(\n                temporary,\n                os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,\n                0o600,\n                dir_fd=parent_fd,\n            )\n            view = memoryview(data)\n            while view:\n                written = os.write(descriptor, view)\n                view = view[written:]\n            os.fsync(descriptor)\n            os.close(descriptor)\n            descriptor = -1\n            os.replace(temporary, path.name, src_dir_fd=parent_fd, dst_dir_fd=parent_fd)\n            os.fsync(parent_fd)\n            if relative not in self._written:\n                self._written.append(relative)","sourceCodeStart":231,"sourceCodeEnd":267,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-application/scripts/tasteforge/workflow.py#L231-L267","documentation":"Before replacing an artifact, write_json stats the existing name (no symlink following) and refuses to overwrite anything that is not a regular file. This prevents clobbering directories, FIFOs, sockets, or symlinks placed at the artifact path.","triggerScenarios":"write_json targets a path where an existing entry is a directory, FIFO, device, or symlink; e.g. 'output/manifest.json' is a symlink planted by an attacker or a leftover directory.","commonSituations":"Attacker-controlled output directories with symlink preplants; a previous version of the tool created a directory at the artifact name; users manually linked outputs to shared locations.","solutions":["Remove or replace the non-regular entry at the artifact path, then re-run","Point the workflow at a fresh, trusted output directory","If a symlink is intentional, copy the target's content into a real file at the artifact path instead"],"exampleFix":"# before: output/manifest.json is a symlink\n# $ rm output/manifest.json\n$ ls -l output/manifest.json   # verify regular file after rerun\n# after: workflow writes a fresh regular file","handlingStrategy":"validation","validationCode":"import os, stat\ntarget = Path(output_root) / relative\nif target.exists() or target.is_symlink():\n    st = os.lstat(target)\n    assert stat.S_ISREG(st.st_mode), f\"{target} is not a regular file\"","typeGuard":"def is_regular_file_or_absent(path: Path) -> bool:\n    if not (path.exists() or path.is_symlink()):\n        return True\n    import os, stat\n    return stat.S_ISREG(os.lstat(path).st_mode)","tryCatchPattern":"try:\n    writer.write_json(relative, payload)\nexcept ValueError:\n    target = Path(output_root) / relative\n    if target.is_symlink() or (target.exists() and not target.is_file()):\n        target.unlink()\n        writer.write_json(relative, payload)\n    else:\n        raise","preventionTips":["Write artifacts only into trusted, non-shared directories","Audit output directories for symlinks before running in multi-user environments","Avoid pointing output root at /tmp or other shared locations"],"tags":["filesystem","symlink","security"],"backgroundTag":"incompatible-source-type","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}