os: treat EACCES as a permission error in RemoveAll
Fixes #29983
Change-Id: I24077bde991e621c23d00973b2a77bb3a18e4ae7
Reviewed-on: https://go-review.googlesource.com/c/160180
Run-TryBot: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Damien Neil <dneil@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/src/os/removeall_at.go b/src/os/removeall_at.go
index fe8b1fa..7f2d592 100644
--- a/src/os/removeall_at.go
+++ b/src/os/removeall_at.go
@@ -57,8 +57,13 @@
return nil
}
- // If not a "is directory" error, we have a problem
- if err != syscall.EISDIR && err != syscall.EPERM {
+ // EISDIR means that we have a directory, and we need to
+ // remove its contents.
+ // EPERM or EACCES means that we don't have write permission on
+ // the parent directory, but this entry might still be a directory
+ // whose contents need to be removed.
+ // Otherwise just return the error.
+ if err != syscall.EISDIR && err != syscall.EPERM && err != syscall.EACCES {
return err
}
@@ -69,11 +74,11 @@
return statErr
}
if statInfo.Mode&syscall.S_IFMT != syscall.S_IFDIR {
- // Not a directory; return the error from the Remove
+ // Not a directory; return the error from the Remove.
return err
}
- // Remove the directory's entries
+ // Remove the directory's entries.
var recurseErr error
for {
const request = 1024
@@ -88,7 +93,7 @@
}
names, readErr := file.Readdirnames(request)
- // Errors other than EOF should stop us from continuing
+ // Errors other than EOF should stop us from continuing.
if readErr != nil && readErr != io.EOF {
file.Close()
if IsNotExist(readErr) {
@@ -117,7 +122,7 @@
}
}
- // Remove the directory itself
+ // Remove the directory itself.
unlinkError := unix.Unlinkat(parentFd, path, unix.AT_REMOVEDIR)
if unlinkError == nil || IsNotExist(unlinkError) {
return nil
diff --git a/src/os/removeall_test.go b/src/os/removeall_test.go
index 0f7dce0..9dab0d4bb 100644
--- a/src/os/removeall_test.go
+++ b/src/os/removeall_test.go
@@ -292,3 +292,83 @@
t.Error("subdirectory was not removed")
}
}
+
+// Issue #29983.
+func TestRemoveAllButReadOnly(t *testing.T) {
+ switch runtime.GOOS {
+ case "nacl", "js", "windows":
+ t.Skipf("skipping test on %s", runtime.GOOS)
+ }
+
+ if Getuid() == 0 {
+ t.Skip("skipping test when running as root")
+ }
+
+ t.Parallel()
+
+ tempDir, err := ioutil.TempDir("", "TestRemoveAllButReadOnly-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer RemoveAll(tempDir)
+
+ dirs := []string{
+ "a",
+ "a/x",
+ "a/x/1",
+ "b",
+ "b/y",
+ "b/y/2",
+ "c",
+ "c/z",
+ "c/z/3",
+ }
+ readonly := []string{
+ "b",
+ }
+ inReadonly := func(d string) bool {
+ for _, ro := range readonly {
+ if d == ro {
+ return true
+ }
+ dd, _ := filepath.Split(d)
+ if filepath.Clean(dd) == ro {
+ return true
+ }
+ }
+ return false
+ }
+
+ for _, dir := range dirs {
+ if err := Mkdir(filepath.Join(tempDir, dir), 0777); err != nil {
+ t.Fatal(err)
+ }
+ }
+ for _, dir := range readonly {
+ d := filepath.Join(tempDir, dir)
+ if err := Chmod(d, 0555); err != nil {
+ t.Fatal(err)
+ }
+
+ // Defer changing the mode back so that the deferred
+ // RemoveAll(tempDir) can succeed.
+ defer Chmod(d, 0777)
+ }
+
+ if err := RemoveAll(tempDir); err == nil {
+ t.Fatal("RemoveAll succeeded unexpectedly")
+ }
+
+ for _, dir := range dirs {
+ _, err := Stat(filepath.Join(tempDir, dir))
+ if inReadonly(dir) {
+ if err != nil {
+ t.Errorf("file %q was deleted but should still exist", dir)
+ }
+ } else {
+ if err == nil {
+ t.Errorf("file %q still exists but should have been deleted", dir)
+ }
+ }
+ }
+}