Skip to content

proposal: os: support deleting files with POSIX semantics for Windows 2019 and older #78072

Description

@stanhu

Proposal Details

os.Remove on Windows uses syscall.DeleteFile, which cannot delete files that are currently open.

In Go 1.26 (https://go-review.googlesource.com/c/go/+/713480, b31dc77) os.RemoveAll attempts to use FILE_DISPOSITION_POSIX_SEMANTICS for Windows if available.

I propose we do something similar for os.Remove to make this consistent:

diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go
index f654e036ac..7020ee2766 100644
--- a/src/internal/syscall/windows/at_windows.go
+++ b/src/internal/syscall/windows/at_windows.go
@@ -319,6 +319,53 @@ func deleteatFallback(h syscall.Handle) error {
 	)
 }
 
+func RemoveEx(path string) (fallback bool, err error) {
+	// Attempt to remove the file using POSIX semantics
+	// (which permit a file to be deleted while it is still open).
+	objAttrs := &OBJECT_ATTRIBUTES{}
+	if err := objAttrs.init(syscall.InvalidHandle, path); err != nil {
+		return true, err
+	}
+	var h syscall.Handle
+	err = NtOpenFile(
+		&h,
+		DELETE,
+		objAttrs,
+		&IO_STATUS_BLOCK{},
+		FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
+		FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT,
+	)
+	if err != nil {
+		return true, err
+	}
+	defer syscall.CloseHandle(h)
+
+	const FileDispositionInformationEx = 64
+
+	err = NtSetInformationFile(
+		h,
+		&IO_STATUS_BLOCK{},
+		unsafe.Pointer(&FILE_DISPOSITION_INFORMATION_EX{
+			Flags: FILE_DISPOSITION_DELETE |
+				FILE_DISPOSITION_FORCE_IMAGE_SECTION_CHECK |
+				FILE_DISPOSITION_POSIX_SEMANTICS |
+				FILE_DISPOSITION_IGNORE_READONLY_ATTRIBUTE,
+		}),
+		uint32(unsafe.Sizeof(FILE_DISPOSITION_INFORMATION_EX{})),
+		FileDispositionInformationEx,
+	)
+	switch err {
+	case nil:
+		return false, nil
+	case STATUS_INVALID_INFO_CLASS,
+		STATUS_INVALID_PARAMETER,
+		STATUS_NOT_SUPPORTED:
+		return true, nil
+	default:
+		return false, err
+	}
+}
+
 func Renameat(olddirfd syscall.Handle, oldpath string, newdirfd syscall.Handle, newpath string) error {
 	objAttrs := &OBJECT_ATTRIBUTES{}
 	if err := objAttrs.init(olddirfd, oldpath); err != nil {
diff --git a/src/os/file_windows.go b/src/os/file_windows.go
index ad172b5048..4480c7d8d3 100644
--- a/src/os/file_windows.go
+++ b/src/os/file_windows.go
@@ -220,6 +220,15 @@ func Truncate(name string, size int64) error {
 // Remove removes the named file or directory.
 // If there is an error, it will be of type [*PathError].
 func Remove(name string) error {
+	// First try using POSIX semantics (allows deleting while open).
+	// This matches the behavior of Deleteat and os.Remove on Unix.
+	if fallback, err := windows.RemoveEx(name); err == nil {
+		return nil
+	} else if !fallback {
+		return &PathError{Op: "remove", Path: name, Err: err}
+	}
+
+	// Fall back to traditional DeleteFile/RemoveDirectory.
 	p, e := syscall.UTF16PtrFromString(fixLongPath(name))
 	if e != nil {
 		return &PathError{Op: "remove", Path: name, Err: e}

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolOS-WindowsProposal

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions