anthropics / anthropics/sandbox-runtime

filesystem: allow unlink/rename to be denied inside write-allowed paths

Open
#545 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
5.2k
Forks
441
Avg merge
1d 19h
Merged PRs (30d)
12

Description

## Summary

A write-allowed path can also be deleted or moved, and there is no opt-in path
to anything narrower. "May edit this file" and "may destroy this file" are one
permission today.

`.git` is the case that cannot be worked around with `denyWrite`, because the
runtime deliberately keeps it writable — `sandbox-utils.js`:

export const DANGEROUS_DIRECTORIES = ['.git', '.vscode', '.idea'];
/** Excludes .git since we need it writable for git operations -
* instead we block specific paths within .git (hooks and config). */

## Reproducer

settings.json — writes allowed in the project, nothing else:

```json
{ "network": { "allowedDomains": [], "deniedDomains": [], "allowUnixSockets": [], "allowLocalBinding": false },
"filesystem": { "allowRead": ["/tmp/x/proj"], "denyRead": [], "allowWrite": ["/tmp/x/proj"], "denyWrite": [] } }
```

```bash
# proj/ is a git repo with one commit
srt --settings settings.json -- sh -c "rm -rf /tmp/x/proj/.git"

ls proj/.git # config hooks <- directory survives
git -C proj log # fatal: not a git repository <- history does not
```

`objects`, `refs`, `HEAD` and `index` are gone. `rm` stopped only when it
reached the denied `.git/hooks` and could not remove a non-empty directory, so
what is left on disk looks like an intact `.git`.

## Notes

- The mechanism is already there, just never fed anything but denies:
`generateMoveBlockingRules` (`macos-sandbox-utils.js:433`) denies
`file-write-unlink` + `file-write-create` for any pattern list; `:522` passes
it only `resolved.denies`; `:542` then re-allows both for every write root
unconditionally. A `filesystem.denyUnlink` list subtracted from
`writeAllowFilters` at `:542` and added at `:522` looks sufficient from
outside.
- `rename` has to be in scope, or the same files are destroyed by a different
syscall. The existing rule already pairs unlink with create for that reason.
- **Linux:** bubblewrap is mount-based (`--ro-bind` gives read-only, not
"writable but not deletable") and seccomp cannot filter on path arguments, so
this is probably macOS-only. Precedent: `allowMachLookup` (#83). A field that
silently did nothing on Linux would be worse than the gap — better to refuse
to start there, as an invalid config already does.
- Measured downstream: over ~330 rounds of a multi-agent run, 66 destructive
commands landed inside a role's own writable tree (34 recursive deletes, 11
`reset --hard`), each one permitted by the sandbox and caught only by a
text-inspecting guard in front of it.
- Longer writeup, with the reasoning and the source of that number:
https://github.com/carlostapiaolguin3-stack/seisin/blob/main/docs/upstream/denyUnlink.md

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by tracing DANGEROUS_DIRECTORIES in sandbox-utils.js and generateMoveBlockingRules in macos-sandbox-utils.js, especially the referenced lines where deny rules are passed and write roots are re-allowed. Verify how filesystem settings are validated and how macOS rules pair unlink with create. Done means a denyUnlink setting protects matching paths inside writable roots, covers rename, and does not silently claim Linux support.

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, typescript
Domain
operating-systems, security
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.