carlos / platform Public

instancefs: RemoveAllBeneath, a tree removal that follows no symlink

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0abd7cc809f0392f45faaa8ade579878087d0a05 parent cd1dc93
3 files changed, +138 −0
  • internal/instancefs/dirs_linux.go +60 −0
  • internal/instancefs/dirs_linux_test.go +71 −0
  • internal/instancefs/instancefs_other.go +7 −0
diff --git a/internal/instancefs/dirs_linux.go b/internal/instancefs/dirs_linux.go
index 5a562a6fd..6e0334802 100644
--- a/internal/instancefs/dirs_linux.go
+++ b/internal/instancefs/dirs_linux.go
@@ -156,3 +156,63 @@ func RenameBetweenNoReplace(srcDir, srcName, dstDir, dstName string) error {
}
return nil
}
+
+// RemoveAllBeneath removes name, directly under dir, and everything below
+// it. No symlink is followed at any depth: dir itself is opened without
+// following one (ErrSymlink), every entry is examined with fstatat
+// AT_SYMLINK_NOFOLLOW, and a symlink is unlinked, never its target. A
+// tenant sharing the agent's uid can plant links and directories in a run
+// directory (run spec §5.2); this is the only way the agent deletes one.
+// A missing name is not an error.
+func RemoveAllBeneath(dir, name string) error {
+ if err := bareName(name); err != nil {
+ return err
+ }
+ dfd, err := openDir(dir)
+ if err != nil {
+ return err
+ }
+ defer unix.Close(dfd)
+ return removeAt(dfd, name, filepath.Join(dir, name), 0)
+}
+
+// removeAt removes name under the directory fd parent; path is for errors.
+func removeAt(parent int, name, path string, depth int) error {
+ if depth > 64 {
+ return fmt.Errorf("instancefs: %s is nested more than 64 deep", path)
+ }
+ var st unix.Stat_t
+ if err := unix.Fstatat(parent, name, &st, unix.AT_SYMLINK_NOFOLLOW); err != nil {
+ if errors.Is(err, unix.ENOENT) {
+ return nil
+ }
+ return &os.PathError{Op: "fstatat", Path: path, Err: err}
+ }
+ if st.Mode&unix.S_IFMT != unix.S_IFDIR {
+ if err := unix.Unlinkat(parent, name, 0); err != nil && !errors.Is(err, unix.ENOENT) {
+ return &os.PathError{Op: "unlinkat", Path: path, Err: err}
+ }
+ return nil
+ }
+ fd, err := unix.Openat(parent, name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
+ if err != nil {
+ return &os.PathError{Op: "openat", Path: path, Err: err}
+ }
+ f := os.NewFile(uintptr(fd), path)
+ names, err := f.Readdirnames(-1)
+ if err != nil {
+ f.Close()
+ return err
+ }
+ for _, n := range names {
+ if err := removeAt(fd, n, filepath.Join(path, n), depth+1); err != nil {
+ f.Close()
+ return err
+ }
+ }
+ f.Close()
+ if err := unix.Unlinkat(parent, name, unix.AT_REMOVEDIR); err != nil && !errors.Is(err, unix.ENOENT) {
+ return &os.PathError{Op: "unlinkat", Path: path, Err: err}
+ }
+ return nil
+}
diff --git a/internal/instancefs/dirs_linux_test.go b/internal/instancefs/dirs_linux_test.go
index 46b2a3aba..0474f9c56 100644
--- a/internal/instancefs/dirs_linux_test.go
+++ b/internal/instancefs/dirs_linux_test.go
@@ -229,3 +229,74 @@ func TestRemoveBeneathRemovesTheEntryNotItsTarget(t *testing.T) {
t.Fatalf("second remove: err = %v, want ErrNotExist", err)
}
}
+
+func TestRemoveAllBeneathRemovesATree(t *testing.T) {
+ dir := t.TempDir()
+ for _, p := range []string{"run/a", "run/sub/b", "run/sub/deeper/c"} {
+ full := filepath.Join(dir, p)
+ if err := os.MkdirAll(filepath.Dir(full), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(full, []byte("x"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := RemoveAllBeneath(dir, "run"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Lstat(filepath.Join(dir, "run")); !os.IsNotExist(err) {
+ t.Fatalf("run survived: %v", err)
+ }
+ if err := RemoveAllBeneath(dir, "run"); err != nil {
+ t.Fatalf("a missing name must be nil: %v", err)
+ }
+}
+
+func TestRemoveAllBeneathNeverFollowsASymlink(t *testing.T) {
+ dir, outside := t.TempDir(), t.TempDir()
+ keep := filepath.Join(outside, "keep")
+ if err := os.WriteFile(keep, []byte("x"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(dir, "run"), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(outside, filepath.Join(dir, "run", "link")); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(outside, filepath.Join(dir, "top")); err != nil {
+ t.Fatal(err)
+ }
+ if err := RemoveAllBeneath(dir, "run"); err != nil {
+ t.Fatal(err)
+ }
+ if err := RemoveAllBeneath(dir, "top"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(keep); err != nil {
+ t.Fatalf("a symlink's target was removed: %v", err)
+ }
+ if _, err := os.Lstat(filepath.Join(dir, "top")); !os.IsNotExist(err) {
+ t.Fatalf("the top-level symlink itself survived: %v", err)
+ }
+}
+
+func TestRemoveAllBeneathRefusesPathsAndSymlinkedParents(t *testing.T) {
+ dir, outside := t.TempDir(), t.TempDir()
+ if err := RemoveAllBeneath(dir, "a/b"); !errors.Is(err, ErrEscapes) {
+ t.Errorf("a/b: %v, want ErrEscapes", err)
+ }
+ link := filepath.Join(dir, "parent")
+ if err := os.Symlink(outside, link); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(filepath.Join(outside, "run"), 0o700); err != nil {
+ t.Fatal(err)
+ }
+ if err := RemoveAllBeneath(link, "run"); !errors.Is(err, ErrSymlink) {
+ t.Errorf("symlinked parent: %v, want ErrSymlink", err)
+ }
+ if _, err := os.Stat(filepath.Join(outside, "run")); err != nil {
+ t.Errorf("removed through a symlinked parent: %v", err)
+ }
+}
diff --git a/internal/instancefs/instancefs_other.go b/internal/instancefs/instancefs_other.go
index 10901ac46..8635ad5c5 100644
--- a/internal/instancefs/instancefs_other.go
+++ b/internal/instancefs/instancefs_other.go
@@ -60,3 +60,10 @@ func RemoveBeneath(dir, name string) error {
}
return errors.ErrUnsupported
}
+
+func RemoveAllBeneath(dir, name string) error {
+ if err := bareName(name); err != nil {
+ return err
+ }
+ return errors.ErrUnsupported
+}