aboutsummaryrefslogtreecommitdiff
path: root/src/os
diff options
context:
space:
mode:
authorDamien Neil <dneil@google.com>2024-11-12 17:16:10 +0100
committerDamien Neil <dneil@google.com>2024-11-20 23:21:14 +0000
commit49d24d469eb4ecbbf5a77d905ca2bd1da0e18bbd (patch)
tree620dfeb866b8bdc9174da763bd19e9cbd0e53b28 /src/os
parent43d90c6a14e7b3fd1b3b8085b8071a09231c4b62 (diff)
downloadgo-49d24d469eb4ecbbf5a77d905ca2bd1da0e18bbd.tar.xz
os: add Root.Remove
For #67002 Change-Id: Ibbf44c0bf62f53695a7399ba0dae5b84d5efd374 Reviewed-on: https://go-review.googlesource.com/c/go/+/627076 Reviewed-by: Quim Muntal <quimmuntal@gmail.com> Reviewed-by: Ian Lance Taylor <iant@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Diffstat (limited to 'src/os')
-rw-r--r--src/os/os_test.go20
-rw-r--r--src/os/root.go6
-rw-r--r--src/os/root_js.go21
-rw-r--r--src/os/root_noopenat.go10
-rw-r--r--src/os/root_openat.go10
-rw-r--r--src/os/root_plan9.go4
-rw-r--r--src/os/root_test.go92
-rw-r--r--src/os/root_unix.go22
-rw-r--r--src/os/root_windows.go4
-rw-r--r--src/os/root_windows_test.go7
10 files changed, 192 insertions, 4 deletions
diff --git a/src/os/os_test.go b/src/os/os_test.go
index e891c1a422..c646ca8246 100644
--- a/src/os/os_test.go
+++ b/src/os/os_test.go
@@ -3815,3 +3815,23 @@ func TestAppendDoesntOverwrite(t *testing.T) {
}
})
}
+
+func TestRemoveReadOnlyFile(t *testing.T) {
+ testMaybeRooted(t, func(t *testing.T, r *Root) {
+ if err := WriteFile("file", []byte("1"), 0); err != nil {
+ t.Fatal(err)
+ }
+ var err error
+ if r == nil {
+ err = Remove("file")
+ } else {
+ err = r.Remove("file")
+ }
+ if err != nil {
+ t.Fatalf("Remove read-only file: %v", err)
+ }
+ if _, err := Stat("file"); !IsNotExist(err) {
+ t.Fatalf("Stat read-only file after removal: %v (want IsNotExist)", err)
+ }
+ })
+}
diff --git a/src/os/root.go b/src/os/root.go
index 1574817098..55455d2c94 100644
--- a/src/os/root.go
+++ b/src/os/root.go
@@ -120,6 +120,12 @@ func (r *Root) Mkdir(name string, perm FileMode) error {
return rootMkdir(r, name, perm)
}
+// Remove removes the named file or (empty) directory in the root.
+// See [Remove] for more details.
+func (r *Root) Remove(name string) error {
+ return rootRemove(r, name)
+}
+
func (r *Root) logOpen(name string) {
if log := testlog.Logger(); log != nil {
// This won't be right if r's name has changed since it was opened,
diff --git a/src/os/root_js.go b/src/os/root_js.go
index 72138d1e89..70aa5f9ccd 100644
--- a/src/os/root_js.go
+++ b/src/os/root_js.go
@@ -12,7 +12,24 @@ import (
"syscall"
)
+// checkPathEscapes reports whether name escapes the root.
+//
+// Due to the lack of openat, checkPathEscapes is subject to TOCTOU races
+// when symlinks change during the resolution process.
func checkPathEscapes(r *Root, name string) error {
+ return checkPathEscapesInternal(r, name, false)
+}
+
+// checkPathEscapesLstat reports whether name escapes the root.
+// It does not resolve symlinks in the final path component.
+//
+// Due to the lack of openat, checkPathEscapes is subject to TOCTOU races
+// when symlinks change during the resolution process.
+func checkPathEscapesLstat(r *Root, name string) error {
+ return checkPathEscapesInternal(r, name, true)
+}
+
+func checkPathEscapesInternal(r *Root, name string, lstat bool) error {
if r.root.closed.Load() {
return ErrClosed
}
@@ -44,6 +61,10 @@ func checkPathEscapes(r *Root, name string) error {
continue
}
+ if lstat && i == len(parts)-1 {
+ break
+ }
+
next := joinPath(base, parts[i])
fi, err := Lstat(next)
if err != nil {
diff --git a/src/os/root_noopenat.go b/src/os/root_noopenat.go
index be7f5507eb..d59720a7b7 100644
--- a/src/os/root_noopenat.go
+++ b/src/os/root_noopenat.go
@@ -84,3 +84,13 @@ func rootMkdir(r *Root, name string, perm FileMode) error {
}
return nil
}
+
+func rootRemove(r *Root, name string) error {
+ if err := checkPathEscapesLstat(r, name); err != nil {
+ return &PathError{Op: "removeat", Path: name, Err: err}
+ }
+ if err := Remove(joinPath(r.root.name, name)); err != nil {
+ return &PathError{Op: "removeat", Path: name, Err: underlyingError(err)}
+ }
+ return nil
+}
diff --git a/src/os/root_openat.go b/src/os/root_openat.go
index 7f6619bab4..a03208b4c1 100644
--- a/src/os/root_openat.go
+++ b/src/os/root_openat.go
@@ -74,6 +74,16 @@ func rootMkdir(r *Root, name string, perm FileMode) error {
return err
}
+func rootRemove(r *Root, name string) error {
+ _, err := doInRoot(r, name, func(parent sysfdType, name string) (struct{}, error) {
+ return struct{}{}, removeat(parent, name)
+ })
+ if err != nil {
+ return &PathError{Op: "removeat", Path: name, Err: err}
+ }
+ return err
+}
+
// doInRoot performs an operation on a path in a Root.
//
// It opens the directory containing the final element of the path,
diff --git a/src/os/root_plan9.go b/src/os/root_plan9.go
index 0a26e7352a..08005accb5 100644
--- a/src/os/root_plan9.go
+++ b/src/os/root_plan9.go
@@ -19,3 +19,7 @@ func checkPathEscapes(r *Root, name string) error {
}
return nil
}
+
+func checkPathEscapesLstat(r *Root, name string) error {
+ return checkPathEscapes(r, name)
+}
diff --git a/src/os/root_test.go b/src/os/root_test.go
index 1edccf362c..70c378cef8 100644
--- a/src/os/root_test.go
+++ b/src/os/root_test.go
@@ -105,6 +105,13 @@ type rootTest struct {
// the target is the filename that should not have been opened.
target string
+ // ltarget is the filename that we expect to accessed, after resolving all symlinks
+ // except the last one. This is the file we expect to be removed by Remove or statted
+ // by Lstat.
+ //
+ // If the last path component in open is not a symlink, ltarget should be "".
+ ltarget string
+
// wantError is true if accessing the file should fail.
wantError bool
@@ -176,8 +183,9 @@ var rootTestCases = []rootTest{{
fs: []string{
"link => target",
},
- open: "link",
- target: "target",
+ open: "link",
+ target: "target",
+ ltarget: "link",
}, {
name: "symlink chain",
fs: []string{
@@ -188,8 +196,9 @@ var rootTestCases = []rootTest{{
"g/h/i => ..",
"g/c/",
},
- open: "link",
- target: "g/c/target",
+ open: "link",
+ target: "g/c/target",
+ ltarget: "link",
}, {
name: "path with dot",
fs: []string{
@@ -251,6 +260,7 @@ var rootTestCases = []rootTest{{
"a => a",
},
open: "a",
+ ltarget: "a",
wantError: true,
alwaysFails: true,
}, {
@@ -273,6 +283,7 @@ var rootTestCases = []rootTest{{
"link => $ABS/target",
},
open: "link",
+ ltarget: "link",
target: "target",
wantError: true,
}, {
@@ -282,6 +293,7 @@ var rootTestCases = []rootTest{{
},
open: "link",
target: "target",
+ ltarget: "link",
wantError: true,
}, {
name: "symlink chain escapes",
@@ -293,6 +305,7 @@ var rootTestCases = []rootTest{{
},
open: "link",
target: "c/target",
+ ltarget: "link",
wantError: true,
}}
@@ -421,6 +434,60 @@ func TestRootOpenRoot(t *testing.T) {
}
}
+func TestRootRemoveFile(t *testing.T) {
+ for _, test := range rootTestCases {
+ test.run(t, func(t *testing.T, target string, root *os.Root) {
+ wantError := test.wantError
+ if test.ltarget != "" {
+ // Remove doesn't follow symlinks in the final path component,
+ // so it will successfully remove ltarget.
+ wantError = false
+ target = filepath.Join(root.Name(), test.ltarget)
+ } else if target != "" {
+ if err := os.WriteFile(target, nil, 0o666); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ err := root.Remove(test.open)
+ if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
+ return
+ }
+ _, err = os.Lstat(target)
+ if !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
+ }
+ })
+ }
+}
+
+func TestRootRemoveDirectory(t *testing.T) {
+ for _, test := range rootTestCases {
+ test.run(t, func(t *testing.T, target string, root *os.Root) {
+ wantError := test.wantError
+ if test.ltarget != "" {
+ // Remove doesn't follow symlinks in the final path component,
+ // so it will successfully remove ltarget.
+ wantError = false
+ target = filepath.Join(root.Name(), test.ltarget)
+ } else if target != "" {
+ if err := os.Mkdir(target, 0o777); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ err := root.Remove(test.open)
+ if errEndsTest(t, err, wantError, "root.Remove(%q)", test.open) {
+ return
+ }
+ _, err = os.Lstat(target)
+ if !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf(`stat file removed with Root.Remove(%q): %v, want ErrNotExist`, test.open, err)
+ }
+ })
+ }
+}
+
func TestRootOpenFileAsRoot(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "target")
@@ -733,6 +800,23 @@ func TestRootConsistencyMkdir(t *testing.T) {
}
}
+func TestRootConsistencyRemove(t *testing.T) {
+ for _, test := range rootConsistencyTestCases {
+ if test.open == "." || test.open == "./" {
+ continue // can't remove the root itself
+ }
+ test.run(t, func(t *testing.T, path string, r *os.Root) (string, error) {
+ var err error
+ if r == nil {
+ err = os.Remove(path)
+ } else {
+ err = r.Remove(path)
+ }
+ return "", err
+ })
+ }
+}
+
func TestRootRenameAfterOpen(t *testing.T) {
switch runtime.GOOS {
case "windows":
diff --git a/src/os/root_unix.go b/src/os/root_unix.go
index 496a11903b..6f8f9c8e3e 100644
--- a/src/os/root_unix.go
+++ b/src/os/root_unix.go
@@ -119,6 +119,28 @@ func mkdirat(fd int, name string, perm FileMode) error {
})
}
+func removeat(fd int, name string) error {
+ // The system call interface forces us to know whether
+ // we are removing a file or directory. Try both.
+ e := ignoringEINTR(func() error {
+ return unix.Unlinkat(fd, name, 0)
+ })
+ if e == nil {
+ return nil
+ }
+ e1 := ignoringEINTR(func() error {
+ return unix.Unlinkat(fd, name, unix.AT_REMOVEDIR)
+ })
+ if e1 == nil {
+ return nil
+ }
+ // Both failed. See comment in Remove for how we decide which error to return.
+ if e1 != syscall.ENOTDIR {
+ return e1
+ }
+ return e
+}
+
// checkSymlink resolves the symlink name in parent,
// and returns errSymlink with the link contents.
//
diff --git a/src/os/root_windows.go b/src/os/root_windows.go
index 685737ea44..68f938de93 100644
--- a/src/os/root_windows.go
+++ b/src/os/root_windows.go
@@ -201,3 +201,7 @@ func rootOpenDir(parent syscall.Handle, name string) (syscall.Handle, error) {
func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
return windows.Mkdirat(dirfd, name, syscallMode(perm))
}
+
+func removeat(dirfd syscall.Handle, name string) error {
+ return windows.Deleteat(dirfd, name)
+}
diff --git a/src/os/root_windows_test.go b/src/os/root_windows_test.go
index f9bddc0d67..62e2097123 100644
--- a/src/os/root_windows_test.go
+++ b/src/os/root_windows_test.go
@@ -7,6 +7,7 @@
package os_test
import (
+ "errors"
"os"
"path/filepath"
"testing"
@@ -43,4 +44,10 @@ func TestRootWindowsCaseInsensitivity(t *testing.T) {
t.Fatal(err)
}
f.Close()
+ if err := r.Remove("FILE"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := os.Stat(filepath.Join(dir, "file")); !errors.Is(err, os.ErrNotExist) {
+ t.Fatalf("os.Stat(file) after deletion: %v, want ErrNotFound", err)
+ }
}