instancefs: RemoveAllBeneath, a tree removal that follows no symlink
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+138
−0
internal/instancefs/dirs_linux.go+60 −0internal/instancefs/dirs_linux_test.go+71 −0internal/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 |
| +} |