os: RemoveAll: fix symlink race for unix

Since all the platforms now support O_DIRECTORY flag for open, it can be
used to (together with O_NOFOLLOW) to ensure we open a directory, thus
eliminating the need to call stat before open. This fixes the symlink race,
when a directory is replaced by a symlink in between stat and open calls.

While at it, rename openFdAt to openDirAt, because this function is (and was)
meant for directories only.

NOTE Solaris supports O_DIRECTORY since before Solaris 11 (which is the
only version Go supports since supported version now), and Illumos
always had it. The only missing piece was O_DIRECTORY flag value, which
is taken from golang.org/x/sys/unix.

Updates #52745.

Change-Id: Ic1111d688eebc8804a87d39d3261c2a6eb33f176
Reviewed-on: https://go-review.googlesource.com/c/go/+/588495
Reviewed-by: Ian Lance Taylor <iant@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Aleksa Sarai <cyphar@cyphar.com>
Reviewed-by: Than McIntosh <thanm@google.com>
Auto-Submit: Ian Lance Taylor <iant@google.com>
This commit is contained in:
Kir Kolyshkin 2024-05-25 23:08:37 -07:00 committed by Gopher Robot
parent 3dcb9623df
commit a3a584e4ab
3 changed files with 16 additions and 23 deletions

View file

@ -152,7 +152,7 @@ const (
kindSock
// kindNoPoll means that we should not put the descriptor into
// non-blocking mode, because we know it is not a pipe or FIFO.
// Used by openFdAt and openDirNolog for directories.
// Used by openDirAt and openDirNolog for directories.
kindNoPoll
)
@ -260,7 +260,7 @@ func epipecheck(file *File, e error) {
const DevNull = "/dev/null"
// openFileNolog is the Unix implementation of OpenFile.
// Changes here should be reflected in openFdAt and openDirNolog, if relevant.
// Changes here should be reflected in openDirAt and openDirNolog, if relevant.
func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
setSticky := false
if !supportsCreateWithStickyBit && flag&O_CREATE != 0 && perm&ModeSticky != 0 {

View file

@ -74,22 +74,7 @@ func removeAllFrom(parent *File, base string) error {
if err != syscall.EISDIR && err != syscall.EPERM && err != syscall.EACCES {
return &PathError{Op: "unlinkat", Path: base, Err: err}
}
// Is this a directory we need to recurse into?
var statInfo syscall.Stat_t
statErr := ignoringEINTR(func() error {
return unix.Fstatat(parentFd, base, &statInfo, unix.AT_SYMLINK_NOFOLLOW)
})
if statErr != nil {
if IsNotExist(statErr) {
return nil
}
return &PathError{Op: "fstatat", Path: base, Err: statErr}
}
if statInfo.Mode&syscall.S_IFMT != syscall.S_IFDIR {
// Not a directory; return the error from the unix.Unlinkat.
return &PathError{Op: "unlinkat", Path: base, Err: err}
}
uErr := err
// Remove the directory's entries.
var recurseErr error
@ -98,11 +83,15 @@ func removeAllFrom(parent *File, base string) error {
var respSize int
// Open the directory to recurse into
file, err := openFdAt(parentFd, base)
file, err := openDirAt(parentFd, base)
if err != nil {
if IsNotExist(err) {
return nil
}
if err == syscall.ENOTDIR {
// Not a directory; return the error from the unix.Unlinkat.
return &PathError{Op: "unlinkat", Path: base, Err: uErr}
}
recurseErr = &PathError{Op: "openfdat", Path: base, Err: err}
break
}
@ -168,16 +157,19 @@ func removeAllFrom(parent *File, base string) error {
return &PathError{Op: "unlinkat", Path: base, Err: unlinkError}
}
// openFdAt opens path relative to the directory in fd.
// Other than that this should act like openFileNolog.
// openDirAt opens a directory name relative to the directory referred to by
// the file descriptor dirfd. If name is anything but a directory (this
// includes a symlink to one), it should return an error. Other than that this
// should act like openFileNolog.
//
// This acts like openFileNolog rather than OpenFile because
// we are going to (try to) remove the file.
// The contents of this file are not relevant for test caching.
func openFdAt(dirfd int, name string) (*File, error) {
func openDirAt(dirfd int, name string) (*File, error) {
var r int
for {
var e error
r, e = unix.Openat(dirfd, name, O_RDONLY|syscall.O_CLOEXEC, 0)
r, e = unix.Openat(dirfd, name, O_RDONLY|syscall.O_CLOEXEC|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
if e == nil {
break
}

View file

@ -634,6 +634,7 @@ const (
O_APPEND = 0x8
O_CLOEXEC = 0x800000
O_CREAT = 0x100
O_DIRECTORY = 0x1000000
O_DSYNC = 0x40
O_EXCL = 0x400
O_EXEC = 0x400000