Skip to content

Commit b31dc77

Browse files
qmuntalgopherbot
authored andcommitted
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>
1 parent 46cc532 commit b31dc77

File tree

6 files changed

+95
-20
lines changed

6 files changed

+95
-20
lines changed

src/internal/syscall/windows/at_windows.go

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
209209
var h syscall.Handle
210210
err := NtOpenFile(
211211
&h,
212-
SYNCHRONIZE|DELETE,
212+
SYNCHRONIZE|FILE_READ_ATTRIBUTES|DELETE,
213213
objAttrs,
214214
&IO_STATUS_BLOCK{},
215215
FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
@@ -220,14 +220,22 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
220220
}
221221
defer syscall.CloseHandle(h)
222222

223-
const (
224-
FileDispositionInformation = 13
225-
FileDispositionInformationEx = 64
226-
)
223+
if TestDeleteatFallback {
224+
return deleteatFallback(h)
225+
}
226+
227+
const FileDispositionInformationEx = 64
227228

228229
// First, attempt to delete the file using POSIX semantics
229230
// (which permit a file to be deleted while it is still open).
230231
// This matches the behavior of DeleteFileW.
232+
//
233+
// The following call uses features available on different Windows versions:
234+
// - FILE_DISPOSITION_INFORMATION_EX: Windows 10, version 1607 (aka RS1)
235+
// - FILE_DISPOSITION_POSIX_SEMANTICS: Windows 10, version 1607 (aka RS1)
236+
// - FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE: Windows 10, version 1809 (aka RS5)
237+
//
238+
// Also, some file systems, like FAT32, don't support POSIX semantics.
231239
err = NtSetInformationFile(
232240
h,
233241
&IO_STATUS_BLOCK{},
@@ -246,28 +254,57 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
246254
switch err {
247255
case nil:
248256
return nil
249-
case STATUS_CANNOT_DELETE, STATUS_DIRECTORY_NOT_EMPTY:
257+
case STATUS_INVALID_INFO_CLASS, // the operating system doesn't support FileDispositionInformationEx
258+
STATUS_INVALID_PARAMETER, // the operating system doesn't support one of the flags
259+
STATUS_NOT_SUPPORTED: // the file system doesn't support FILE_DISPOSITION_INFORMATION_EX or one of the flags
260+
return deleteatFallback(h)
261+
default:
250262
return err.(NTStatus).Errno()
251263
}
264+
}
252265

253-
// If the prior deletion failed, the filesystem either doesn't support
254-
// POSIX semantics (for example, FAT), or hasn't implemented
255-
// FILE_DISPOSITION_INFORMATION_EX.
256-
//
257-
// Try again.
258-
err = NtSetInformationFile(
266+
// TestDeleteatFallback should only be used for testing purposes.
267+
// When set, [Deleteat] uses the fallback path unconditionally.
268+
var TestDeleteatFallback bool
269+
270+
// deleteatFallback is a deleteat implementation that strives
271+
// for compatibility with older Windows versions and file systems
272+
// over performance.
273+
func deleteatFallback(h syscall.Handle) error {
274+
var data syscall.ByHandleFileInformation
275+
if err := syscall.GetFileInformationByHandle(h, &data); err == nil && data.FileAttributes&syscall.FILE_ATTRIBUTE_READONLY != 0 {
276+
// Remove read-only attribute. Reopen the file, as it was previously open without FILE_WRITE_ATTRIBUTES access
277+
// in order to maximize compatibility in the happy path.
278+
wh, err := ReOpenFile(h,
279+
FILE_WRITE_ATTRIBUTES,
280+
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
281+
syscall.FILE_FLAG_OPEN_REPARSE_POINT|syscall.FILE_FLAG_BACKUP_SEMANTICS,
282+
)
283+
if err != nil {
284+
return err
285+
}
286+
err = SetFileInformationByHandle(
287+
wh,
288+
FileBasicInfo,
289+
unsafe.Pointer(&FILE_BASIC_INFO{
290+
FileAttributes: data.FileAttributes &^ FILE_ATTRIBUTE_READONLY,
291+
}),
292+
uint32(unsafe.Sizeof(FILE_BASIC_INFO{})),
293+
)
294+
syscall.CloseHandle(wh)
295+
if err != nil {
296+
return err
297+
}
298+
}
299+
300+
return SetFileInformationByHandle(
259301
h,
260-
&IO_STATUS_BLOCK{},
261-
unsafe.Pointer(&FILE_DISPOSITION_INFORMATION{
302+
FileDispositionInfo,
303+
unsafe.Pointer(&FILE_DISPOSITION_INFO{
262304
DeleteFile: true,
263305
}),
264-
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION{})),
265-
FileDispositionInformation,
306+
uint32(unsafe.Sizeof(FILE_DISPOSITION_INFO{})),
266307
)
267-
if st, ok := err.(NTStatus); ok {
268-
return st.Errno()
269-
}
270-
return err
271308
}
272309

273310
func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {

src/internal/syscall/windows/symlink_windows.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
FileBasicInfo = 0 // FILE_BASIC_INFO
2020
FileStandardInfo = 1 // FILE_STANDARD_INFO
2121
FileNameInfo = 2 // FILE_NAME_INFO
22+
FileDispositionInfo = 4 // FILE_DISPOSITION_INFO
2223
FileStreamInfo = 7 // FILE_STREAM_INFO
2324
FileCompressionInfo = 8 // FILE_COMPRESSION_INFO
2425
FileAttributeTagInfo = 9 // FILE_ATTRIBUTE_TAG_INFO

src/internal/syscall/windows/syscall_windows.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,8 @@ const (
531531
//sys GetOverlappedResult(handle syscall.Handle, overlapped *syscall.Overlapped, done *uint32, wait bool) (err error)
532532
//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
533533

534+
//sys ReOpenFile(filehandle syscall.Handle, desiredAccess uint32, shareMode uint32, flagAndAttributes uint32) (handle syscall.Handle, err error)
535+
534536
// NTStatus corresponds with NTSTATUS, error values returned by ntdll.dll and
535537
// other native functions.
536538
type NTStatus uint32
@@ -556,6 +558,9 @@ const (
556558
STATUS_NOT_A_DIRECTORY NTStatus = 0xC0000103
557559
STATUS_CANNOT_DELETE NTStatus = 0xC0000121
558560
STATUS_REPARSE_POINT_ENCOUNTERED NTStatus = 0xC000050B
561+
STATUS_NOT_SUPPORTED NTStatus = 0xC00000BB
562+
STATUS_INVALID_PARAMETER NTStatus = 0xC000000D
563+
STATUS_INVALID_INFO_CLASS NTStatus = 0xC0000003
559564
)
560565

561566
const (

src/internal/syscall/windows/types_windows.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ const (
216216
FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000
217217
)
218218

219+
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_disposition_info
220+
type FILE_DISPOSITION_INFO struct {
221+
DeleteFile bool
222+
}
223+
219224
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/ns-ntddk-_file_disposition_information
220225
type FILE_DISPOSITION_INFORMATION struct {
221226
DeleteFile bool

src/internal/syscall/windows/zsyscall_windows.go

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/os/path_windows_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,23 @@ func TestRemoveAllLongPathRelative(t *testing.T) {
236236
}
237237
}
238238

239+
func TestRemoveAllFallback(t *testing.T) {
240+
windows.TestDeleteatFallback = true
241+
t.Cleanup(func() { windows.TestDeleteatFallback = false })
242+
243+
dir := t.TempDir()
244+
if err := os.WriteFile(filepath.Join(dir, "file1"), []byte{}, 0700); err != nil {
245+
t.Fatal(err)
246+
}
247+
if err := os.WriteFile(filepath.Join(dir, "file2"), []byte{}, 0400); err != nil { // read-only file
248+
t.Fatal(err)
249+
}
250+
251+
if err := os.RemoveAll(dir); err != nil {
252+
t.Fatal(err)
253+
}
254+
}
255+
239256
func testLongPathAbs(t *testing.T, target string) {
240257
t.Helper()
241258
testWalkFn := func(path string, info os.FileInfo, err error) error {

0 commit comments

Comments
 (0)