Skip to content

Commit 9ab706c

Browse files
committed
os: Add support for long path names on unix RemoveAll
On unix systems, long enough path names will fail when performing syscalls like `Lstat`. The current RemoveAll uses several of these syscalls, and so will fail for long paths. This can be risky, as it can let users "hide" files from the system or otherwise make long enough paths for programs to fail. By using `Unlinkat` and `Openat` syscalls instead, RemoveAll is safer on unix systems. Initially implemented for linux, darwin, freebsd and openbsd. Fixes #27029 Co-authored-by: Giuseppe Capizzi <[email protected]> Co-authored-by: Julia Nedialkova <[email protected]> Change-Id: I7a2b0164ab973c3b6b59b30914406796c7045114
1 parent 930ce09 commit 9ab706c

14 files changed

+614
-224
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package unix
6+
7+
const unlinkatTrap = uintptr(472)
8+
const openatTrap = uintptr(463)
9+
10+
const AT_REMOVEDIR = 0x80
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package unix
6+
7+
import "syscall"
8+
9+
const unlinkatTrap uintptr = syscall.SYS_UNLINKAT
10+
const openatTrap uintptr = syscall.SYS_OPENAT
11+
12+
const AT_REMOVEDIR = 0x2
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package unix
6+
7+
import "syscall"
8+
9+
const unlinkatTrap uintptr = syscall.SYS_UNLINKAT
10+
const openatTrap uintptr = syscall.SYS_OPENAT
11+
12+
const AT_REMOVEDIR = 0x800
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package unix
6+
7+
import "syscall"
8+
9+
const unlinkatTrap uintptr = syscall.SYS_UNLINKAT
10+
const openatTrap uintptr = syscall.SYS_OPENAT
11+
12+
const AT_REMOVEDIR = 0x200
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package unix
6+
7+
import "syscall"
8+
9+
const unlinkatTrap uintptr = syscall.SYS_UNLINKAT
10+
const openatTrap uintptr = syscall.SYS_OPENAT
11+
12+
const AT_REMOVEDIR = 0x800
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package unix
6+
7+
import "syscall"
8+
9+
const unlinkatTrap uintptr = syscall.SYS_UNLINKAT
10+
const openatTrap uintptr = syscall.SYS_OPENAT
11+
12+
const AT_REMOVEDIR = 0x08
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build linux darwin freebsd openbsd netbsd dragonfly
6+
7+
package unix
8+
9+
import (
10+
"syscall"
11+
"unsafe"
12+
)
13+
14+
func Openat(fdat int, path string, flags int, perm uint32) (int, error) {
15+
var pathBytePointer *byte
16+
pathBytePointer, err := syscall.BytePtrFromString(path)
17+
if err != nil {
18+
return 0, err
19+
}
20+
21+
fdPointer, _, errNo := syscall.Syscall6(openatTrap, uintptr(fdat), uintptr(unsafe.Pointer(pathBytePointer)), uintptr(flags), uintptr(perm), 0, 0)
22+
fd := int(fdPointer)
23+
if errNo != 0 {
24+
return 0, errNo
25+
}
26+
27+
return fd, nil
28+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2018 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// +build linux darwin freebsd openbsd netbsd dragonfly
6+
7+
package unix
8+
9+
import (
10+
"syscall"
11+
"unsafe"
12+
)
13+
14+
func Unlinkat(dirfd int, path string, flags int) error {
15+
var pathBytePointer *byte
16+
pathBytePointer, err := syscall.BytePtrFromString(path)
17+
if err != nil {
18+
return err
19+
}
20+
21+
_, _, errNo := syscall.Syscall(unlinkatTrap, uintptr(dirfd), uintptr(unsafe.Pointer(pathBytePointer)), uintptr(flags))
22+
if errNo != 0 {
23+
return errNo
24+
}
25+
26+
return nil
27+
}

src/os/path.go

Lines changed: 0 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package os
66

77
import (
8-
"io"
98
"syscall"
109
)
1110

@@ -58,101 +57,3 @@ func MkdirAll(path string, perm FileMode) error {
5857
}
5958
return nil
6059
}
61-
62-
// RemoveAll removes path and any children it contains.
63-
// It removes everything it can but returns the first error
64-
// it encounters. If the path does not exist, RemoveAll
65-
// returns nil (no error).
66-
func RemoveAll(path string) error {
67-
// Simple case: if Remove works, we're done.
68-
err := Remove(path)
69-
if err == nil || IsNotExist(err) {
70-
return nil
71-
}
72-
73-
// Otherwise, is this a directory we need to recurse into?
74-
dir, serr := Lstat(path)
75-
if serr != nil {
76-
if serr, ok := serr.(*PathError); ok && (IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
77-
return nil
78-
}
79-
return serr
80-
}
81-
if !dir.IsDir() {
82-
// Not a directory; return the error from Remove.
83-
return err
84-
}
85-
86-
// Remove contents & return first error.
87-
err = nil
88-
for {
89-
fd, err := Open(path)
90-
if err != nil {
91-
if IsNotExist(err) {
92-
// Already deleted by someone else.
93-
return nil
94-
}
95-
return err
96-
}
97-
98-
const request = 1024
99-
names, err1 := fd.Readdirnames(request)
100-
101-
// Removing files from the directory may have caused
102-
// the OS to reshuffle it. Simply calling Readdirnames
103-
// again may skip some entries. The only reliable way
104-
// to avoid this is to close and re-open the
105-
// directory. See issue 20841.
106-
fd.Close()
107-
108-
for _, name := range names {
109-
err1 := RemoveAll(path + string(PathSeparator) + name)
110-
if err == nil {
111-
err = err1
112-
}
113-
}
114-
115-
if err1 == io.EOF {
116-
break
117-
}
118-
// If Readdirnames returned an error, use it.
119-
if err == nil {
120-
err = err1
121-
}
122-
if len(names) == 0 {
123-
break
124-
}
125-
126-
// We don't want to re-open unnecessarily, so if we
127-
// got fewer than request names from Readdirnames, try
128-
// simply removing the directory now. If that
129-
// succeeds, we are done.
130-
if len(names) < request {
131-
err1 := Remove(path)
132-
if err1 == nil || IsNotExist(err1) {
133-
return nil
134-
}
135-
136-
if err != nil {
137-
// We got some error removing the
138-
// directory contents, and since we
139-
// read fewer names than we requested
140-
// there probably aren't more files to
141-
// remove. Don't loop around to read
142-
// the directory again. We'll probably
143-
// just get the same error.
144-
return err
145-
}
146-
}
147-
}
148-
149-
// Remove directory.
150-
err1 := Remove(path)
151-
if err1 == nil || IsNotExist(err1) {
152-
return nil
153-
}
154-
if err == nil {
155-
err = err1
156-
}
157-
return err
158-
}

src/os/path_test.go

Lines changed: 0 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package os_test
66

77
import (
8-
"fmt"
98
"internal/testenv"
109
"io/ioutil"
1110
. "os"
@@ -76,130 +75,6 @@ func TestMkdirAll(t *testing.T) {
7675
}
7776
}
7877

79-
func TestRemoveAll(t *testing.T) {
80-
tmpDir := TempDir()
81-
// Work directory.
82-
path := tmpDir + "/_TestRemoveAll_"
83-
fpath := path + "/file"
84-
dpath := path + "/dir"
85-
86-
// Make directory with 1 file and remove.
87-
if err := MkdirAll(path, 0777); err != nil {
88-
t.Fatalf("MkdirAll %q: %s", path, err)
89-
}
90-
fd, err := Create(fpath)
91-
if err != nil {
92-
t.Fatalf("create %q: %s", fpath, err)
93-
}
94-
fd.Close()
95-
if err = RemoveAll(path); err != nil {
96-
t.Fatalf("RemoveAll %q (first): %s", path, err)
97-
}
98-
if _, err = Lstat(path); err == nil {
99-
t.Fatalf("Lstat %q succeeded after RemoveAll (first)", path)
100-
}
101-
102-
// Make directory with file and subdirectory and remove.
103-
if err = MkdirAll(dpath, 0777); err != nil {
104-
t.Fatalf("MkdirAll %q: %s", dpath, err)
105-
}
106-
fd, err = Create(fpath)
107-
if err != nil {
108-
t.Fatalf("create %q: %s", fpath, err)
109-
}
110-
fd.Close()
111-
fd, err = Create(dpath + "/file")
112-
if err != nil {
113-
t.Fatalf("create %q: %s", fpath, err)
114-
}
115-
fd.Close()
116-
if err = RemoveAll(path); err != nil {
117-
t.Fatalf("RemoveAll %q (second): %s", path, err)
118-
}
119-
if _, err := Lstat(path); err == nil {
120-
t.Fatalf("Lstat %q succeeded after RemoveAll (second)", path)
121-
}
122-
123-
// Determine if we should run the following test.
124-
testit := true
125-
if runtime.GOOS == "windows" {
126-
// Chmod is not supported under windows.
127-
testit = false
128-
} else {
129-
// Test fails as root.
130-
testit = Getuid() != 0
131-
}
132-
if testit {
133-
// Make directory with file and subdirectory and trigger error.
134-
if err = MkdirAll(dpath, 0777); err != nil {
135-
t.Fatalf("MkdirAll %q: %s", dpath, err)
136-
}
137-
138-
for _, s := range []string{fpath, dpath + "/file1", path + "/zzz"} {
139-
fd, err = Create(s)
140-
if err != nil {
141-
t.Fatalf("create %q: %s", s, err)
142-
}
143-
fd.Close()
144-
}
145-
if err = Chmod(dpath, 0); err != nil {
146-
t.Fatalf("Chmod %q 0: %s", dpath, err)
147-
}
148-
149-
// No error checking here: either RemoveAll
150-
// will or won't be able to remove dpath;
151-
// either way we want to see if it removes fpath
152-
// and path/zzz. Reasons why RemoveAll might
153-
// succeed in removing dpath as well include:
154-
// * running as root
155-
// * running on a file system without permissions (FAT)
156-
RemoveAll(path)
157-
Chmod(dpath, 0777)
158-
159-
for _, s := range []string{fpath, path + "/zzz"} {
160-
if _, err = Lstat(s); err == nil {
161-
t.Fatalf("Lstat %q succeeded after partial RemoveAll", s)
162-
}
163-
}
164-
}
165-
if err = RemoveAll(path); err != nil {
166-
t.Fatalf("RemoveAll %q after partial RemoveAll: %s", path, err)
167-
}
168-
if _, err = Lstat(path); err == nil {
169-
t.Fatalf("Lstat %q succeeded after RemoveAll (final)", path)
170-
}
171-
}
172-
173-
// Test RemoveAll on a large directory.
174-
func TestRemoveAllLarge(t *testing.T) {
175-
if testing.Short() {
176-
t.Skip("skipping in short mode")
177-
}
178-
179-
tmpDir := TempDir()
180-
// Work directory.
181-
path := tmpDir + "/_TestRemoveAllLarge_"
182-
183-
// Make directory with 1000 files and remove.
184-
if err := MkdirAll(path, 0777); err != nil {
185-
t.Fatalf("MkdirAll %q: %s", path, err)
186-
}
187-
for i := 0; i < 1000; i++ {
188-
fpath := fmt.Sprintf("%s/file%d", path, i)
189-
fd, err := Create(fpath)
190-
if err != nil {
191-
t.Fatalf("create %q: %s", fpath, err)
192-
}
193-
fd.Close()
194-
}
195-
if err := RemoveAll(path); err != nil {
196-
t.Fatalf("RemoveAll %q: %s", path, err)
197-
}
198-
if _, err := Lstat(path); err == nil {
199-
t.Fatalf("Lstat %q succeeded after RemoveAll", path)
200-
}
201-
}
202-
20378
func TestMkdirAllWithSymlink(t *testing.T) {
20479
testenv.MustHaveSymlink(t)
20580

0 commit comments

Comments
 (0)