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 // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build unix //go:build unix || wasip1
package unix package unix

View File

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

View File

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

View File

@ -21,6 +21,16 @@ func IsPathSeparator(c uint8) bool {
return c == '\\' || c == '/' 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 { func dirname(path string) string {
vol := filepathlite.VolumeName(path) vol := filepathlite.VolumeName(path)
i := len(path) - 1 i := len(path) - 1

View File

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

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
//go:build !unix //go:build (js && wasm) || plan9
package os 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 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 { func renameat(oldfd int, oldname string, newfd int, newname string) error {
return unix.Renameat(oldfd, oldname, newfd, newname) 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 { 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 { func chtimesat(dirfd syscall.Handle, name string, atime time.Time, mtime time.Time) error {