Skip to content

fix(sandbox): refuse to rm the workspace root - #4832

Open
coderdailyone wants to merge 2 commits into
openai:mainfrom
coderdailyone:fix/rm-refuses-workspace-root
Open

fix(sandbox): refuse to rm the workspace root#4832
coderdailyone wants to merge 2 commits into
openai:mainfrom
coderdailyone:fix/rm-refuses-workspace-root

Conversation

@coderdailyone

Copy link
Copy Markdown

This pull request fixes SandboxSession.rm() so it refuses to remove the workspace root itself instead of deleting the whole workspace.

Bug

The workspace root is a valid workspace path, so rm("."), rm(""), rm("sub/..") and rm("<root>") all pass _validate_path_access() / normalize_path() and then run:

  • BaseSandboxSession.rm() (every exec-backed backend): rm -rf -- /workspace inside the sandbox;
  • UnixLocalSandboxSession.rm(): shutil.rmtree(<root>) on the host.

After that the session is unusable. Reproduced on main with a real UnixLocalSandboxClient session: await session.rm(".", recursive=True) succeeds, the /tmp/sandbox-local-* directory is gone, and the next session.exec("pwd") raises WorkspaceRootNotFoundError. The same call is reachable from the model through apply_patch (delete_file with path .) and any tool that forwards a path to rm.

Removing the root is never what a caller means by a file operation; clearing a workspace is rm of its entries.

Fix

  • BaseSandboxSession._raise_if_workspace_root_removal(path) compares the validated path with the manifest root, the policy's sandbox_root(), and the resolved root (for UnixLocal, whose validated paths are realpaths and whose /tmp may itself be a symlink), and raises WorkspaceArchiveWriteError(context={"reason": "workspace_root_removal_refused"}).
  • BaseSandboxSession.rm() and UnixLocalSandboxSession.rm() call it after path validation and before any exec or filesystem mutation.
  • rm of entries under the root is unchanged.

Evidence

  • tests/sandbox/test_session_utils.py::test_rm_refuses_to_remove_the_workspace_root (exec-backed base, five spellings of the root, asserts no rm command is executed) and tests/sandbox/test_unix_local.py::TestUnixLocalRmWorkspaceRoot (real filesystem, asserts the workspace and its contents survive). Ten parametrized cases fail on main and pass with this change; the companion tests assert rm of a child entry still works.
  • tests/sandbox/test_unix_local.py, tests/sandbox/test_session_utils.py, ruff, mypy, and pyright on the changed files pass locally (Linux, Python 3.10).

Note: this touches the same rm() in unix_local.py as #4830 (leaf-symlink removal); the two changes are independent and either can be rebased trivially onto the other.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BN4v25msJgrjNgac97g1Az

`rm(".")`, `rm("")` and `rm("<workspace root>")` passed path validation
(the root is a valid workspace path) and then removed the workspace
directory itself: BaseSandboxSession ran `rm -rf -- /workspace` in the
sandbox and UnixLocalSandboxSession called shutil.rmtree on the host
directory. Every later exec, read or write in the session then failed
with WorkspaceRootNotFoundError.

Reject the root before removing anything, in both the exec-backed and
the UnixLocal implementation, with a WorkspaceArchiveWriteError whose
context names the reason. Removing entries under the root is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BN4v25msJgrjNgac97g1Az

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d04dd3b6e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

root = Path(self.state.manifest.root)
candidates = {sandbox_path_str(root), sandbox_path_str(self._workspace_root_path())}
try:
candidates.add(sandbox_path_str(root.resolve(strict=False)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve the root only in the local backend

When a remote sandbox's POSIX root resolves differently on the SDK host—for example, root="/tmp/ws" on macOS while the remote sandbox also has a writable grant at /private/tmp/ws—this host-side resolve() adds the unrelated grant path to the protected candidates. _validate_remote_path_access() legitimately accepts that grant, but rm("/private/tmp/ws", recursive=True) is then falsely rejected as workspace-root removal based solely on the client filesystem. Keep remote comparisons POSIX-only and perform realpath comparison in the UnixLocal boundary that needs it.

AGENTS.md reference: AGENTS.md:L205-L205

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The base guard now compares POSIX spellings only (Manifest.root and the policy's sandbox_root()), with no host-side resolve(). UnixLocalSandboxSession overrides _raise_if_workspace_root_removal() to additionally compare against the resolved root, since its validated paths are host realpaths and Manifest.root (or /tmp on macOS) may be a symlink. The existing parametrized tests cover both layers.

…ssion

The root guard resolved Manifest.root on the SDK host, which is the
wrong filesystem for a remote sandbox and could turn an unrelated host
path into a protected candidate. Keep the base comparison POSIX-only
and do the realpath comparison in UnixLocalSandboxSession, whose
validated paths are host realpaths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BN4v25msJgrjNgac97g1Az
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant