{"record":{"id":"c08b4b3345bf4ac5","repo":"agentscope-ai/agentscope","slug":"path-escapes-writable-bubblewrap-mounts-path","errorCode":null,"errorMessage":"path escapes writable Bubblewrap mounts: {path}","messagePattern":"path escapes writable Bubblewrap mounts: (.+?)","errorType":"exception","errorClass":"PermissionError","httpStatus":null,"severity":"error","filePath":"src/agentscope/workspace/_bubblewrap/_bubblewrap_backend.py","lineNumber":272,"sourceCode":"            [\n                \"sh\",\n                \"-c\",\n                (\n                    'resolved=$(realpath -e -- \"$1\") || exit 66; '\n                    'case \"$resolved\" in '\n                    \"/workspace|/workspace/*|/tmp|/tmp/*) \"\n                    'cat -- \"$resolved\" ;; '\n                    \"*) exit 65 ;; \"\n                    \"esac\"\n                ),\n                \"sh\",\n                sandbox_path,\n            ],\n        )\n        if result.exit_code == 66:\n            raise FileNotFoundError(f\"not found in Bubblewrap sandbox: {path}\")\n        if result.exit_code == 65:\n            raise PermissionError(\n                f\"path escapes writable Bubblewrap mounts: {path}\",\n            )\n        if not result.ok():\n            raise RuntimeError(\n                \"Bubblewrap read_file failed \"\n                f\"(exit {result.exit_code}): \"\n                f\"{result.stderr.decode(errors='replace')}\",\n            )\n        return result.stdout\n\n    async def write_file(self, path: str, data: bytes) -> None:\n        \"\"\"Write raw bytes, refusing every symbolic-link final component.\"\"\"\n        await self._write_via_cat(path, data)\n\n    async def write_stream(\n        self,\n        path: str,\n        stream: AsyncIterator[bytes],","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/workspace/_bubblewrap/_bubblewrap_backend.py#L254-L290","documentation":"read_file inside the sandbox returns exit code 65 when the target path resolves outside the sandbox's writable bind mounts. The backend translates this into PermissionError to signal that the sandboxed shell refused to touch the path for containment reasons.","triggerScenarios":"Calling read_file on a path like /etc/passwd or ../../etc/shadow — anything outside the writable SANDBOX_WORKDIR/SANDBOX_TMPDIR trees once normalized inside the sandbox.","commonSituations":"Passing host-absolute paths that don't exist in the sandbox view, or traversal sequences that escape the workspace root.","solutions":["Use an absolute path under the sandbox workdir or tmpdir only","Normalize and prefix-join relative paths with the sandbox workdir before calling read_file","Treat PermissionError from read_file as a path-design bug, not a transient failure"],"exampleFix":"# before\nawait ws.read_file('../../etc/passwd')\n# after\nawait ws.read_file('/workspace/notes.txt')","handlingStrategy":"validation","validationCode":"from pathlib import PurePosixPath\nALLOWED = ('/workspace/', '/tmp/')\nnorm = posixpath.normpath(path)\nassert norm.startswith(ALLOWED), f'outside sandbox: {norm}'","typeGuard":"null","tryCatchPattern":"except PermissionError as e:\n    if 'escapes writable' in str(e): reject_path(path)","preventionTips":["Reject '..' segments and non-workspace-prefixed paths before I/O","Never pass host paths through to sandbox APIs"],"tags":["bubblewrap","permission","sandbox-escape","path-traversal"],"backgroundTag":"path-outside-sandbox-root","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}