mirror of
https://github.com/golang/go.git
synced 2025-12-08 06:10:04 +00:00
os: support deleting read-only files in RemoveAll on older Windows versions
The Windows implementation of RemoveAll supports deleting read-only files only on file systems that supports POSIX semantics and on newer Windows versions (Windows 10 RS5 and latter). For all the other cases, the read-only bit was not clearer before deleting read-only files, so they fail to delete. Note that this case was supported prior to CL 75922, which landed on Go 1.25. Fixes #75922 Change-Id: Id6e6477f42e1952d08318ca3e4ab7c1648969f66 Reviewed-on: https://go-review.googlesource.com/c/go/+/713480 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: David Chase <drchase@google.com> Reviewed-by: Damien Neil <dneil@google.com> Auto-Submit: Damien Neil <dneil@google.com>
This commit is contained in:
parent
46cc532900
commit
b31dc77cea
6 changed files with 95 additions and 20 deletions
|
|
@ -209,7 +209,7 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
|
||||||
var h syscall.Handle
|
var h syscall.Handle
|
||||||
err := NtOpenFile(
|
err := NtOpenFile(
|
||||||
&h,
|
&h,
|
||||||
SYNCHRONIZE|DELETE,
|
SYNCHRONIZE|FILE_READ_ATTRIBUTES|DELETE,
|
||||||
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,
|
||||||
|
|
@ -220,14 +220,22 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
|
||||||
}
|
}
|
||||||
defer syscall.CloseHandle(h)
|
defer syscall.CloseHandle(h)
|
||||||
|
|
||||||
const (
|
if TestDeleteatFallback {
|
||||||
FileDispositionInformation = 13
|
return deleteatFallback(h)
|
||||||
FileDispositionInformationEx = 64
|
}
|
||||||
)
|
|
||||||
|
const FileDispositionInformationEx = 64
|
||||||
|
|
||||||
// First, attempt to delete the file using POSIX semantics
|
// First, attempt to delete the file using POSIX semantics
|
||||||
// (which permit a file to be deleted while it is still open).
|
// (which permit a file to be deleted while it is still open).
|
||||||
// This matches the behavior of DeleteFileW.
|
// This matches the behavior of DeleteFileW.
|
||||||
|
//
|
||||||
|
// The following call uses features available on different Windows versions:
|
||||||
|
// - FILE_DISPOSITION_INFORMATION_EX: Windows 10, version 1607 (aka RS1)
|
||||||
|
// - FILE_DISPOSITION_POSIX_SEMANTICS: Windows 10, version 1607 (aka RS1)
|
||||||
|
// - FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE: Windows 10, version 1809 (aka RS5)
|
||||||
|
//
|
||||||
|
// Also, some file systems, like FAT32, don't support POSIX semantics.
|
||||||
err = NtSetInformationFile(
|
err = NtSetInformationFile(
|
||||||
h,
|
h,
|
||||||
&IO_STATUS_BLOCK{},
|
&IO_STATUS_BLOCK{},
|
||||||
|
|
@ -246,28 +254,57 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
|
||||||
switch err {
|
switch err {
|
||||||
case nil:
|
case nil:
|
||||||
return nil
|
return nil
|
||||||
case STATUS_CANNOT_DELETE, STATUS_DIRECTORY_NOT_EMPTY:
|
case STATUS_INVALID_INFO_CLASS, // the operating system doesn't support FileDispositionInformationEx
|
||||||
|
STATUS_INVALID_PARAMETER, // the operating system doesn't support one of the flags
|
||||||
|
STATUS_NOT_SUPPORTED: // the file system doesn't support FILE_DISPOSITION_INFORMATION_EX or one of the flags
|
||||||
|
return deleteatFallback(h)
|
||||||
|
default:
|
||||||
return err.(NTStatus).Errno()
|
return err.(NTStatus).Errno()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If the prior deletion failed, the filesystem either doesn't support
|
// TestDeleteatFallback should only be used for testing purposes.
|
||||||
// POSIX semantics (for example, FAT), or hasn't implemented
|
// When set, [Deleteat] uses the fallback path unconditionally.
|
||||||
// FILE_DISPOSITION_INFORMATION_EX.
|
var TestDeleteatFallback bool
|
||||||
//
|
|
||||||
// Try again.
|
// deleteatFallback is a deleteat implementation that strives
|
||||||
err = NtSetInformationFile(
|
// for compatibility with older Windows versions and file systems
|
||||||
|
// over performance.
|
||||||
|
func deleteatFallback(h syscall.Handle) error {
|
||||||
|
var data syscall.ByHandleFileInformation
|
||||||
|
if err := syscall.GetFileInformationByHandle(h, &data); err == nil && data.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
|
||||||
|
// Remove read-only attribute. Reopen the file, as it was previously open without FILE_WRITE_ATTRIBUTES access
|
||||||
|
// in order to maximize compatibility in the happy path.
|
||||||
|
wh, err := ReOpenFile(h,
|
||||||
|
FILE_WRITE_ATTRIBUTES,
|
||||||
|
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
|
||||||
|
syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = SetFileInformationByHandle(
|
||||||
|
wh,
|
||||||
|
FileBasicInfo,
|
||||||
|
unsafe.Pointer(&FILE_BASIC_INFO{
|
||||||
|
FileAttributes: data.FileAttributes &^ FILE_ATTRIBUTE_READONLY,
|
||||||
|
}),
|
||||||
|
uint32(unsafe.Sizeof(FILE_BASIC_INFO{})),
|
||||||
|
)
|
||||||
|
syscall.CloseHandle(wh)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetFileInformationByHandle(
|
||||||
h,
|
h,
|
||||||
&IO_STATUS_BLOCK{},
|
FileDispositionInfo,
|
||||||
unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
|
unsafe.Pointer(&FILE_DISPOSITION_INFO{
|
||||||
DeleteFile: true,
|
DeleteFile: true,
|
||||||
}),
|
}),
|
||||||
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
|
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFO{})),
|
||||||
FileDispositionInformation,
|
|
||||||
)
|
)
|
||||||
if st, ok := err.(NTStatus); ok {
|
|
||||||
return st.Errno()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
|
func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const (
|
||||||
FileBasicInfo = 0 // FILE_BASIC_INFO
|
FileBasicInfo = 0 // FILE_BASIC_INFO
|
||||||
FileStandardInfo = 1 // FILE_STANDARD_INFO
|
FileStandardInfo = 1 // FILE_STANDARD_INFO
|
||||||
FileNameInfo = 2 // FILE_NAME_INFO
|
FileNameInfo = 2 // FILE_NAME_INFO
|
||||||
|
FileDispositionInfo = 4 // FILE_DISPOSITION_INFO
|
||||||
FileStreamInfo = 7 // FILE_STREAM_INFO
|
FileStreamInfo = 7 // FILE_STREAM_INFO
|
||||||
FileCompressionInfo = 8 // FILE_COMPRESSION_INFO
|
FileCompressionInfo = 8 // FILE_COMPRESSION_INFO
|
||||||
FileAttributeTagInfo = 9 // FILE_ATTRIBUTE_TAG_INFO
|
FileAttributeTagInfo = 9 // FILE_ATTRIBUTE_TAG_INFO
|
||||||
|
|
|
||||||
|
|
@ -531,6 +531,8 @@ const (
|
||||||
//sys GetOverlappedResult(handle syscall.Handle, overlapped *syscall.Overlapped, done *uint32, wait bool) (err error)
|
//sys GetOverlappedResult(handle syscall.Handle, overlapped *syscall.Overlapped, done *uint32, wait bool) (err error)
|
||||||
//sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
|
//sys CreateNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *syscall.SecurityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
|
||||||
|
|
||||||
|
//sys ReOpenFile(filehandle syscall.Handle, desiredAccess uint32, shareMode uint32, flagAndAttributes uint32) (handle syscall.Handle, err error)
|
||||||
|
|
||||||
// NTStatus corresponds with NTSTATUS, error values returned by ntdll.dll and
|
// NTStatus corresponds with NTSTATUS, error values returned by ntdll.dll and
|
||||||
// other native functions.
|
// other native functions.
|
||||||
type NTStatus uint32
|
type NTStatus uint32
|
||||||
|
|
@ -556,6 +558,9 @@ const (
|
||||||
STATUS_NOT_A_DIRECTORY NTStatus = 0xC0000103
|
STATUS_NOT_A_DIRECTORY NTStatus = 0xC0000103
|
||||||
STATUS_CANNOT_DELETE NTStatus = 0xC0000121
|
STATUS_CANNOT_DELETE NTStatus = 0xC0000121
|
||||||
STATUS_REPARSE_POINT_ENCOUNTERED NTStatus = 0xC000050B
|
STATUS_REPARSE_POINT_ENCOUNTERED NTStatus = 0xC000050B
|
||||||
|
STATUS_NOT_SUPPORTED NTStatus = 0xC00000BB
|
||||||
|
STATUS_INVALID_PARAMETER NTStatus = 0xC000000D
|
||||||
|
STATUS_INVALID_INFO_CLASS NTStatus = 0xC0000003
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,11 @@ const (
|
||||||
FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000
|
FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_disposition_info
|
||||||
|
type FILE_DISPOSITION_INFO struct {
|
||||||
|
DeleteFile bool
|
||||||
|
}
|
||||||
|
|
||||||
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information
|
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information
|
||||||
type FILE_DISPOSITION_INFORMATION struct {
|
type FILE_DISPOSITION_INFORMATION struct {
|
||||||
DeleteFile bool
|
DeleteFile bool
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ var (
|
||||||
procModule32NextW = modkernel32.NewProc("Module32NextW")
|
procModule32NextW = modkernel32.NewProc("Module32NextW")
|
||||||
procMoveFileExW = modkernel32.NewProc("MoveFileExW")
|
procMoveFileExW = modkernel32.NewProc("MoveFileExW")
|
||||||
procMultiByteToWideChar = modkernel32.NewProc("MultiByteToWideChar")
|
procMultiByteToWideChar = modkernel32.NewProc("MultiByteToWideChar")
|
||||||
|
procReOpenFile = modkernel32.NewProc("ReOpenFile")
|
||||||
procRtlLookupFunctionEntry = modkernel32.NewProc("RtlLookupFunctionEntry")
|
procRtlLookupFunctionEntry = modkernel32.NewProc("RtlLookupFunctionEntry")
|
||||||
procRtlVirtualUnwind = modkernel32.NewProc("RtlVirtualUnwind")
|
procRtlVirtualUnwind = modkernel32.NewProc("RtlVirtualUnwind")
|
||||||
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
|
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
|
||||||
|
|
@ -440,6 +441,15 @@ func MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReOpenFile(filehandle syscall.Handle, desiredAccess uint32, shareMode uint32, flagAndAttributes uint32) (handle syscall.Handle, err error) {
|
||||||
|
r0, _, e1 := syscall.SyscallN(procReOpenFile.Addr(), uintptr(filehandle), uintptr(desiredAccess), uintptr(shareMode), uintptr(flagAndAttributes))
|
||||||
|
handle = syscall.Handle(r0)
|
||||||
|
if handle == 0 {
|
||||||
|
err = errnoErr(e1)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table unsafe.Pointer) (ret *RUNTIME_FUNCTION) {
|
func RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table unsafe.Pointer) (ret *RUNTIME_FUNCTION) {
|
||||||
r0, _, _ := syscall.SyscallN(procRtlLookupFunctionEntry.Addr(), uintptr(pc), uintptr(unsafe.Pointer(baseAddress)), uintptr(table))
|
r0, _, _ := syscall.SyscallN(procRtlLookupFunctionEntry.Addr(), uintptr(pc), uintptr(unsafe.Pointer(baseAddress)), uintptr(table))
|
||||||
ret = (*RUNTIME_FUNCTION)(unsafe.Pointer(r0))
|
ret = (*RUNTIME_FUNCTION)(unsafe.Pointer(r0))
|
||||||
|
|
|
||||||
|
|
@ -236,6 +236,23 @@ func TestRemoveAllLongPathRelative(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoveAllFallback(t *testing.T) {
|
||||||
|
windows.TestDeleteatFallback = true
|
||||||
|
t.Cleanup(func() { windows.TestDeleteatFallback = false })
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "file1"), []byte{}, 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "file2"), []byte{}, 0400); err != nil { // read-only file
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testLongPathAbs(t *testing.T, target string) {
|
func testLongPathAbs(t *testing.T, target string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
testWalkFn := func(path string, info os.FileInfo, err error) error {
|
testWalkFn := func(path string, info os.FileInfo, err error) error {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue