Skip to content

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

Open

Description

@carlostapiacl

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:

{ "network": { "allowedDomains": [], "deniedDomains": [], "allowUnixSockets": [], "allowLocalBinding": false },
  "filesystem": { "allowRead": ["/tmp/x/proj"], "denyRead": [], "allowWrite": ["/tmp/x/proj"], "denyWrite": [] } }
# 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 (Feature request: Add allowMachLookup config for XPC services (1Password CLI, etc.) #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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions