os: avoid symlink races in RemoveAll on Windows

Make the openat-using version of RemoveAll use the appropriate
Windows equivalent, via new portable (but internal) functions
added for os.Root.

We could reimplement everything in terms of os.Root,
but this is a bit simpler and keeps the existing code structure.

Fixes #52745

Change-Id: I0eba0286398b351f2ee9abaa60e1675173988787
Reviewed-on: https://go-review.googlesource.com/c/go/+/661575
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Damien Neil <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Damien Neil 2025-03-28 16:14:43 -07:00 committed by Gopher Robot
parent c6a1dc4729
commit 6d418096b2
10 changed files with 83 additions and 28 deletions

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix
//go:build unix || wasip1
package unix

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix && !dragonfly && !freebsd && !netbsd
//go:build (unix && !dragonfly && !freebsd && !netbsd) || wasip1
package unix

View File

@ -188,7 +188,7 @@ func Mkdirat(dirfd syscall.Handle, name string, mode uint32) error {
return nil
}
func Deleteat(dirfd syscall.Handle, name string) error {
func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(dirfd, name); err != nil {
return err
@ -200,7 +200,7 @@ func Deleteat(dirfd syscall.Handle, name string) error {
objAttrs,
&IO_STATUS_BLOCK{},
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT,
FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|options,
)
if err != nil {
return ntCreateFileError(err, 0)

View File

@ -21,6 +21,16 @@ func IsPathSeparator(c uint8) bool {
return c == '\\' || c == '/'
}
// splitPath returns the base name and parent directory.
func splitPath(path string) (string, string) {
dirname, basename := filepathlite.Split(path)
volnamelen := filepathlite.VolumeNameLen(dirname)
for len(dirname) > volnamelen && IsPathSeparator(dirname[len(dirname)-1]) {
dirname = dirname[:len(dirname)-1]
}
return dirname, basename
}
func dirname(path string) string {
vol := filepathlite.VolumeName(path)
i := len(path) - 1

View File

@ -2,12 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix
//go:build unix || wasip1 || windows
package os
import (
"internal/syscall/unix"
"io"
"syscall"
)
@ -56,11 +55,10 @@ func removeAll(path string) error {
}
func removeAllFrom(parent *File, base string) error {
parentFd := int(parent.Fd())
parentFd := sysfdType(parent.Fd())
// Simple case: if Unlink (aka remove) works, we're done.
err := ignoringEINTR(func() error {
return unix.Unlinkat(parentFd, base, 0)
})
err := removefileat(parentFd, base)
if err == nil || IsNotExist(err) {
return nil
}
@ -82,13 +80,13 @@ func removeAllFrom(parent *File, base string) error {
const reqSize = 1024
var respSize int
// Open the directory to recurse into
// Open the directory to recurse into.
file, err := openDirAt(parentFd, base)
if err != nil {
if IsNotExist(err) {
return nil
}
if err == syscall.ENOTDIR || err == unix.NoFollowErrno {
if err == syscall.ENOTDIR || isErrNoFollow(err) {
// Not a directory; return the error from the unix.Unlinkat.
return &PathError{Op: "unlinkat", Path: base, Err: uErr}
}
@ -144,9 +142,7 @@ func removeAllFrom(parent *File, base string) error {
}
// Remove the directory itself.
unlinkError := ignoringEINTR(func() error {
return unix.Unlinkat(parentFd, base, unix.AT_REMOVEDIR)
})
unlinkError := removedirat(parentFd, base)
if unlinkError == nil || IsNotExist(unlinkError) {
return nil
}
@ -165,18 +161,10 @@ func removeAllFrom(parent *File, base string) error {
// 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 openDirAt(dirfd int, name string) (*File, error) {
r, err := ignoringEINTR2(func() (int, error) {
return unix.Openat(dirfd, name, O_RDONLY|syscall.O_CLOEXEC|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
})
func openDirAt(dirfd sysfdType, name string) (*File, error) {
fd, err := rootOpenDir(dirfd, name)
if err != nil {
return nil, err
}
if !supportsCloseOnExec {
syscall.CloseOnExec(r)
}
// We use kindNoPoll because we know that this is a directory.
return newFile(r, name, kindNoPoll, false), nil
return newDirFile(fd, name)
}

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !unix
//go:build (js && wasm) || plan9
package os

20
src/os/removeall_unix.go Normal file
View File

@ -0,0 +1,20 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build unix || wasip1
package os
import (
"internal/syscall/unix"
)
func isErrNoFollow(err error) bool {
return err == unix.NoFollowErrno
}
func newDirFile(fd int, name string) (*File, error) {
// We use kindNoPoll because we know that this is a directory.
return newFile(fd, name, kindNoPoll, false), nil
}

View File

@ -0,0 +1,17 @@
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
package os
import "syscall"
func isErrNoFollow(err error) bool {
return err == syscall.ELOOP
}
func newDirFile(fd syscall.Handle, name string) (*File, error) {
return newFile(fd, name, "file"), nil
}

View File

@ -219,6 +219,18 @@ func removeat(fd int, name string) error {
return e
}
func removefileat(fd int, name string) error {
return ignoringEINTR(func() error {
return unix.Unlinkat(fd, name, 0)
})
}
func removedirat(fd int, name string) error {
return ignoringEINTR(func() error {
return unix.Unlinkat(fd, name, unix.AT_REMOVEDIR)
})
}
func renameat(oldfd int, oldname string, newfd int, newname string) error {
return unix.Renameat(oldfd, oldname, newfd, newname)
}

View File

@ -336,7 +336,15 @@ func mkdirat(dirfd syscall.Handle, name string, perm FileMode) error {
}
func removeat(dirfd syscall.Handle, name string) error {
return windows.Deleteat(dirfd, name)
return windows.Deleteat(dirfd, name, 0)
}
func removefileat(dirfd syscall.Handle, name string) error {
return windows.Deleteat(dirfd, name, windows.FILE_NON_DIRECTORY_FILE)
}
func removedirat(dirfd syscall.Handle, name string) error {
return windows.Deleteat(dirfd, name, windows.FILE_DIRECTORY_FILE)
}
func chtimesat(dirfd syscall.Handle, name string, atime time.Time, mtime time.Time) error {