aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorqmuntal <quimmuntal@gmail.com>2025-10-21 16:14:03 +0200
committerGopher Robot <gobot@golang.org>2025-10-28 13:58:08 -0700
commit8097b1915f617167f3b12b03e78a23859d256eb6 (patch)
treec643e42d2e9fa5601543f07b19d33694d48d2829
parent4942c74d04295c72e293b4a67200513b9a36f99d (diff)
downloadgo-8097b1915f617167f3b12b03e78a23859d256eb6.tar.xz
[release-branch.go1.25] 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. For #75922 Fixes #75989 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> (cherry picked from commit b31dc77ceab962c0f4f5e4a9fc5e1a403fbd2d7c) Reviewed-on: https://go-review.googlesource.com/c/go/+/715360 Auto-Submit: Michael Knyszek <mknyszek@google.com>
-rw-r--r--src/internal/syscall/windows/at_windows.go77
-rw-r--r--src/internal/syscall/windows/symlink_windows.go1
-rw-r--r--src/internal/syscall/windows/syscall_windows.go5
-rw-r--r--src/internal/syscall/windows/types_windows.go5
-rw-r--r--src/internal/syscall/windows/zsyscall_windows.go10
-rw-r--r--src/os/path_windows_test.go17
6 files changed, 95 insertions, 20 deletions
diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go
index d48fce1c99..41cdaf0d2e 100644
--- a/src/internal/syscall/windows/at_windows.go
+++ b/src/internal/syscall/windows/at_windows.go
@@ -204,7 +204,7 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
var h syscall.Handle
err := NtOpenFile(
&h,
- SYNCHRONIZE|DELETE,
+ SYNCHRONIZE|FILE_READ_ATTRIBUTES|DELETE,
objAttrs,
&IO_STATUS_BLOCK{},
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
@@ -215,14 +215,22 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
}
defer syscall.CloseHandle(h)
- const (
- FileDispositionInformation = 13
- FileDispositionInformationEx = 64
- )
+ if TestDeleteatFallback {
+ return deleteatFallback(h)
+ }
+
+ const FileDispositionInformationEx = 64
// First, attempt to delete the file using POSIX semantics
// (which permit a file to be deleted while it is still open).
// 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(
h,
&IO_STATUS_BLOCK{},
@@ -241,28 +249,57 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
switch err {
case 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()
}
+}
- // If the prior deletion failed, the filesystem either doesn't support
- // POSIX semantics (for example, FAT), or hasn't implemented
- // FILE_DISPOSITION_INFORMATION_EX.
- //
- // Try again.
- err = NtSetInformationFile(
+// TestDeleteatFallback should only be used for testing purposes.
+// When set, [Deleteat] uses the fallback path unconditionally.
+var TestDeleteatFallback bool
+
+// deleteatFallback is a deleteat implementation that strives
+// 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,
- &IO_STATUS_BLOCK{},
- unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
+ FileDispositionInfo,
+ unsafe.Pointer(&FILE_DISPOSITION_INFO{
DeleteFile: true,
}),
- uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
- FileDispositionInformation,
+ uint32(unsafe.Sizeof(FILE_DISPOSITION_INFO{})),
)
- if st, ok := err.(NTStatus); ok {
- return st.Errno()
- }
- return err
}
func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
diff --git a/src/internal/syscall/windows/symlink_windows.go b/src/internal/syscall/windows/symlink_windows.go
index b91246037b..b8249b3848 100644
--- a/src/internal/syscall/windows/symlink_windows.go
+++ b/src/internal/syscall/windows/symlink_windows.go
@@ -19,6 +19,7 @@ const (
FileBasicInfo = 0 // FILE_BASIC_INFO
FileStandardInfo = 1 // FILE_STANDARD_INFO
FileNameInfo = 2 // FILE_NAME_INFO
+ FileDispositionInfo = 4 // FILE_DISPOSITION_INFO
FileStreamInfo = 7 // FILE_STREAM_INFO
FileCompressionInfo = 8 // FILE_COMPRESSION_INFO
FileAttributeTagInfo = 9 // FILE_ATTRIBUTE_TAG_INFO
diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go
index 905cabc81e..c34cc795a0 100644
--- a/src/internal/syscall/windows/syscall_windows.go
+++ b/src/internal/syscall/windows/syscall_windows.go
@@ -529,6 +529,8 @@ const (
//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 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
// other native functions.
type NTStatus uint32
@@ -554,6 +556,9 @@ const (
STATUS_NOT_A_DIRECTORY NTStatus = 0xC0000103
STATUS_CANNOT_DELETE NTStatus = 0xC0000121
STATUS_REPARSE_POINT_ENCOUNTERED NTStatus = 0xC000050B
+ STATUS_NOT_SUPPORTED NTStatus = 0xC00000BB
+ STATUS_INVALID_PARAMETER NTStatus = 0xC000000D
+ STATUS_INVALID_INFO_CLASS NTStatus = 0xC0000003
)
const (
diff --git a/src/internal/syscall/windows/types_windows.go b/src/internal/syscall/windows/types_windows.go
index 93664b4b7d..6d989e7e7e 100644
--- a/src/internal/syscall/windows/types_windows.go
+++ b/src/internal/syscall/windows/types_windows.go
@@ -199,6 +199,11 @@ const (
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
type FILE_DISPOSITION_INFORMATION struct {
DeleteFile bool
diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go
index 90cf0b92a4..b3f01ef5c0 100644
--- a/src/internal/syscall/windows/zsyscall_windows.go
+++ b/src/internal/syscall/windows/zsyscall_windows.go
@@ -85,6 +85,7 @@ var (
procModule32NextW = modkernel32.NewProc("Module32NextW")
procMoveFileExW = modkernel32.NewProc("MoveFileExW")
procMultiByteToWideChar = modkernel32.NewProc("MultiByteToWideChar")
+ procReOpenFile = modkernel32.NewProc("ReOpenFile")
procRtlLookupFunctionEntry = modkernel32.NewProc("RtlLookupFunctionEntry")
procRtlVirtualUnwind = modkernel32.NewProc("RtlVirtualUnwind")
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
@@ -431,6 +432,15 @@ func MultiByteToWideChar(codePage uint32, dwFlags uint32, str *byte, nstr int32,
return
}
+func ReOpenFile(filehandle syscall.Handle, desiredAccess uint32, shareMode uint32, flagAndAttributes uint32) (handle syscall.Handle, err error) {
+ r0, _, e1 := syscall.Syscall6(procReOpenFile.Addr(), 4, uintptr(filehandle), uintptr(desiredAccess), uintptr(shareMode), uintptr(flagAndAttributes), 0, 0)
+ handle = syscall.Handle(r0)
+ if handle == 0 {
+ err = errnoErr(e1)
+ }
+ return
+}
+
func RtlLookupFunctionEntry(pc uintptr, baseAddress *uintptr, table unsafe.Pointer) (ret *RUNTIME_FUNCTION) {
r0, _, _ := syscall.Syscall(procRtlLookupFunctionEntry.Addr(), 3, uintptr(pc), uintptr(unsafe.Pointer(baseAddress)), uintptr(table))
ret = (*RUNTIME_FUNCTION)(unsafe.Pointer(r0))
diff --git a/src/os/path_windows_test.go b/src/os/path_windows_test.go
index 3fa02e2a65..eea2b58ee0 100644
--- a/src/os/path_windows_test.go
+++ b/src/os/path_windows_test.go
@@ -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) {
t.Helper()
testWalkFn := func(path string, info os.FileInfo, err error) error {