]> Cypherpunks repositories - gostls13.git/commitdiff
[release-branch.go1.25] os: support deleting read-only files in RemoveAll on older...
authorqmuntal <quimmuntal@gmail.com>
Tue, 21 Oct 2025 14:14:03 +0000 (16:14 +0200)
committerGopher Robot <gobot@golang.org>
Tue, 28 Oct 2025 20:58:08 +0000 (13:58 -0700)
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>

src/internal/syscall/windows/at_windows.go
src/internal/syscall/windows/symlink_windows.go
src/internal/syscall/windows/syscall_windows.go
src/internal/syscall/windows/types_windows.go
src/internal/syscall/windows/zsyscall_windows.go
src/os/path_windows_test.go

index d48fce1c99dc3673b18da2bca542e1dbf06008b0..41cdaf0d2e34ca2fbba2d31b1ad5502ca03801f3 100644 (file)
@@ -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 {
index b91246037b5efec8e48cc4e6ac35b83706c86020..b8249b3848ea0201ad0c04618e9ac63dba34e1f4 100644 (file)
@@ -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
index 905cabc81e479873fab9f7437bcd4ca5d8a4106a..c34cc795a0ea900809d9ead547b0105f5322630e 100644 (file)
@@ -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 (
index 93664b4b7da8ca5f2ca963f305ddd223142f0c3a..6d989e7e7e78bccbdf83d529571c6943cb0e2ebc 100644 (file)
@@ -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
index 90cf0b92a49bc44b8296929d2248f66dac206f40..b3f01ef5c002811ffe07990257ed0d0c7c681a32 100644 (file)
@@ -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))
index 3fa02e2a65b083d6454f8d3aae728d89b710078f..eea2b58ee0a88653350c9e01ae32ef1eb309a433 100644 (file)
@@ -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 {