From: qmuntal Date: Tue, 21 Oct 2025 14:14:03 +0000 (+0200) Subject: [release-branch.go1.25] os: support deleting read-only files in RemoveAll on older... X-Git-Tag: go1.25.4~3 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=8097b1915f617167f3b12b03e78a23859d256eb6;p=gostls13.git [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 Reviewed-by: David Chase Reviewed-by: Damien Neil Auto-Submit: Damien Neil (cherry picked from commit b31dc77ceab962c0f4f5e4a9fc5e1a403fbd2d7c) Reviewed-on: https://go-review.googlesource.com/c/go/+/715360 Auto-Submit: Michael Knyszek --- 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 {