]> Cypherpunks repositories - gostls13.git/commitdiff
os: support deleting inaccessible files in RemoveAll
authorMichal Pristas <michal.pristas@gmail.com>
Wed, 4 Feb 2026 09:36:59 +0000 (09:36 +0000)
committerGopher Robot <gobot@golang.org>
Fri, 6 Feb 2026 19:28:15 +0000 (11:28 -0800)
windows: retry file open with DELETE access after access denied

Additional access rights when opening files, including SYNCHRONIZE,
break deletion when the caller has FILE_DELETE_CHILD on the parent
directory but not the file. Retry with DELETE only restores correct
Windows semantics.

Fixes #77402

Change-Id: Ie53bc6f1673de1a8af4dcfb7496daf99e71098cb
GitHub-Last-Rev: 0ad635cf1a13c0242e3b1922cf47a8c594dd7215
GitHub-Pull-Request: golang/go#77403
Reviewed-on: https://go-review.googlesource.com/c/go/+/741040
Reviewed-by: Quim Muntal <quimmuntal@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
Auto-Submit: Michael Pratt <mpratt@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
src/internal/syscall/windows/at_windows.go
src/internal/syscall/windows/at_windows_test.go
src/internal/syscall/windows/syscall_windows.go
src/internal/syscall/windows/types_windows.go
src/internal/syscall/windows/zsyscall_windows.go

index de733c523cd7a1c1822c06d841e88f2f1fe385a7..81361c98e4e58388c7f48a93a2378fa4fd6ed218 100644 (file)
@@ -246,14 +246,31 @@ func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
        var h syscall.Handle
        err := NtOpenFile(
                &h,
-               SYNCHRONIZE|FILE_READ_ATTRIBUTES|DELETE,
+               FILE_READ_ATTRIBUTES|DELETE,
                objAttrs,
                &IO_STATUS_BLOCK{},
                FILE_SHARE_DELETE|FILE_SHARE_READ|FILE_SHARE_WRITE,
-               FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|FILE_SYNCHRONOUS_IO_NONALERT|options,
+               FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT|options,
        )
        if err != nil {
-               return ntCreateFileError(err, 0)
+               if ntStatus, ok := err.(NTStatus); !ok || ntStatus != STATUS_ACCESS_DENIED {
+                       return ntCreateFileError(err, 0)
+               }
+
+               // Access denied, try opening with DELETE only.
+               // This may succeed if the file has restrictive permissions
+               // but the caller has delete child permission on the parent directory.
+               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|options,
+               )
+               if err != nil {
+                       return ntCreateFileError(err, 0)
+               }
        }
        defer syscall.CloseHandle(h)
 
index daeb4fcde37d70487ed36901f94682829f72a439..afe46a1796b0fca65cd8bf5e75b9819c6e621325 100644 (file)
@@ -10,6 +10,7 @@ import (
        "path/filepath"
        "syscall"
        "testing"
+       "unsafe"
 )
 
 func TestOpen(t *testing.T) {
@@ -56,3 +57,111 @@ func TestOpen(t *testing.T) {
                }
        }
 }
+
+func TestDeleteAt(t *testing.T) {
+       testCases := []struct {
+               name     string
+               modifier func(t *testing.T, path string)
+       }{
+               {"DeleteAt removes normal file", func(t *testing.T, name string) {}},
+               {"DeleteAt removes file with no read permission", makeFileNotReadable},
+               {"DeleteAt removes readonly file", makeFileReadonly},
+       }
+
+       for _, tc := range testCases {
+               t.Run(tc.name, func(t *testing.T) {
+                       t.Parallel()
+
+                       dir := t.TempDir()
+                       file := filepath.Join(dir, "a")
+                       f, err := os.Create(file)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       f.Close()
+
+                       // Remove all permissions from the file.
+                       // Do not use os.Chmod it sets only readonly attribute on Windows.
+                       tc.modifier(t, file)
+
+                       // delete file using DeleteAt
+                       dirfd, err := syscall.Open(dir, syscall.O_RDONLY, 0)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       base := filepath.Base(file)
+                       err = windows.Deleteat(dirfd, base, 0)
+                       syscall.CloseHandle(dirfd)
+                       if err != nil {
+                               t.Fatalf("Deleteat failed: %v", err)
+                       }
+
+                       // Verify the file has been deleted.
+                       if _, err := os.Stat(file); !os.IsNotExist(err) {
+                               t.Fatalf("file still exists after DeleteAt")
+                       }
+               })
+       }
+}
+
+func makeFileReadonly(t *testing.T, name string) {
+       if err := os.Chmod(name, 0); err != nil {
+               t.Fatal(err)
+       }
+}
+
+func makeFileNotReadable(t *testing.T, name string) {
+       creatorOwnerSID, err := syscall.StringToSid("S-1-3-0")
+       if err != nil {
+               t.Fatal(err)
+       }
+       creatorGroupSID, err := syscall.StringToSid("S-1-3-1")
+       if err != nil {
+               t.Fatal(err)
+       }
+       everyoneSID, err := syscall.StringToSid("S-1-1-0")
+       if err != nil {
+               t.Fatal(err)
+       }
+
+       entryForSid := func(sid *syscall.SID) windows.EXPLICIT_ACCESS {
+               return windows.EXPLICIT_ACCESS{
+                       AccessPermissions: 0,
+                       AccessMode:        windows.GRANT_ACCESS,
+                       Inheritance:       windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
+                       Trustee: windows.TRUSTEE{
+                               TrusteeForm: windows.TRUSTEE_IS_SID,
+                               Name:        (*uint16)(unsafe.Pointer(sid)),
+                       },
+               }
+       }
+
+       entries := []windows.EXPLICIT_ACCESS{
+               entryForSid(creatorOwnerSID),
+               entryForSid(creatorGroupSID),
+               entryForSid(everyoneSID),
+       }
+
+       var oldAcl, newAcl syscall.Handle
+       if err := windows.SetEntriesInAcl(
+               uint32(len(entries)),
+               &entries[0],
+               oldAcl,
+               &newAcl,
+       ); err != nil {
+               t.Fatal(err)
+       }
+
+       defer syscall.LocalFree((syscall.Handle)(unsafe.Pointer(newAcl)))
+       if err := windows.SetNamedSecurityInfo(
+               name,
+               windows.SE_FILE_OBJECT,
+               windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
+               nil,
+               nil,
+               newAcl,
+               0,
+       ); err != nil {
+               t.Fatal(err)
+       }
+}
index b908a2c251921975eecb8f7e4981109bd8cd228e..5a4db289b888167b5ae73213b11ad2620f204421 100644 (file)
@@ -561,6 +561,7 @@ const (
        STATUS_NOT_SUPPORTED             NTStatus = 0xC00000BB
        STATUS_INVALID_PARAMETER         NTStatus = 0xC000000D
        STATUS_INVALID_INFO_CLASS        NTStatus = 0xC0000003
+       STATUS_ACCESS_DENIED             NTStatus = 0xC0000022
 )
 
 const (
@@ -579,3 +580,6 @@ type FILE_MODE_INFORMATION struct {
 //sys   NtSetInformationFile(handle syscall.Handle, iosb *IO_STATUS_BLOCK, inBuffer unsafe.Pointer, inBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtSetInformationFile
 //sys  RtlIsDosDeviceName_U(name *uint16) (ret uint32) = ntdll.RtlIsDosDeviceName_U
 //sys   NtQueryInformationFile(handle syscall.Handle, iosb *IO_STATUS_BLOCK, inBuffer unsafe.Pointer, inBufferLen uint32, class uint32) (ntstatus error) = ntdll.NtQueryInformationFile
+
+//sys  SetEntriesInAcl(countExplicitEntries uint32, explicitEntries *EXPLICIT_ACCESS, oldACL syscall.Handle, newACL *syscall.Handle) (ret error) =  advapi32.SetEntriesInAclW
+//sys  SetNamedSecurityInfo(objectName string, objectType int32, securityInformation uint32, owner *syscall.SID, group *syscall.SID, dacl syscall.Handle, sacl syscall.Handle) (ret error) = advapi32.SetNamedSecurityInfoW
index 568f94624c441611a0276b19a5da972c86b850e2..50213a21604203bc110e75dd5ccb0d73d7b44af9 100644 (file)
@@ -301,3 +301,85 @@ const ValidFileFlagsMask = O_FILE_FLAG_OPEN_REPARSE_POINT |
        O_FILE_FLAG_NO_BUFFERING |
        O_FILE_FLAG_RANDOM_ACCESS |
        O_FILE_FLAG_WRITE_THROUGH
+
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa379636.aspx
+type TRUSTEE struct {
+       MultipleTrustee          *TRUSTEE
+       MultipleTrusteeOperation int32
+       TrusteeForm              int32
+       TrusteeType              int32
+       Name                     *uint16
+}
+
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa379638.aspx
+const (
+       TRUSTEE_IS_SID = iota
+       TRUSTEE_IS_NAME
+       TRUSTEE_BAD_FORM
+       TRUSTEE_IS_OBJECTS_AND_SID
+       TRUSTEE_IS_OBJECTS_AND_NAME
+)
+
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa446627.aspx
+type EXPLICIT_ACCESS struct {
+       AccessPermissions uint32
+       AccessMode        int32
+       Inheritance       uint32
+       Trustee           TRUSTEE
+}
+
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374899.aspx
+const (
+       NOT_USED_ACCESS = iota
+       GRANT_ACCESS
+       SET_ACCESS
+       DENY_ACCESS
+       REVOKE_ACCESS
+       SET_AUDIT_SUCCESS
+       SET_AUDIT_FAILURE
+)
+
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa446627.aspx
+const (
+       NO_INHERITANCE                     = 0x0
+       SUB_OBJECTS_ONLY_INHERIT           = 0x1
+       SUB_CONTAINERS_ONLY_INHERIT        = 0x2
+       SUB_CONTAINERS_AND_OBJECTS_INHERIT = 0x3
+       INHERIT_NO_PROPAGATE               = 0x4
+       INHERIT_ONLY                       = 0x8
+)
+
+// https://msdn.microsoft.com/en-us/library/windows/desktop/aa379593.aspx
+const (
+       SE_UNKNOWN_OBJECT_TYPE = iota
+       SE_FILE_OBJECT
+       SE_SERVICE
+       SE_PRINTER
+       SE_REGISTRY_KEY
+       SE_LMSHARE
+       SE_KERNEL_OBJECT
+       SE_WINDOW_OBJECT
+       SE_DS_OBJECT
+       SE_DS_OBJECT_ALL
+       SE_PROVIDER_DEFINED_OBJECT
+       SE_WMIGUID_OBJECT
+       SE_REGISTRY_WOW64_32KEY
+       SE_REGISTRY_WOW64_64KEY
+)
+
+// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/23e75ca3-98fd-4396-84e5-86cd9d40d343
+const (
+       OWNER_SECURITY_INFORMATION               = 0x00000001
+       GROUP_SECURITY_INFORMATION               = 0x00000002
+       DACL_SECURITY_INFORMATION                = 0x00000004
+       SACL_SECURITY_INFORMATION                = 0x00000008
+       LABEL_SECURITY_INFORMATION               = 0x00000010
+       UNPROTECTED_SACL_SECURITY_INFORMATION    = 0x10000000
+       UNPROTECTED_DACL_SECURITY_INFORMATION    = 0x20000000
+       PROTECTED_SACL_SECURITY_INFORMATION      = 0x40000000
+       PROTECTED_DACL_SECURITY_INFORMATION      = 0x80000000
+       ATTRIBUTE_SECURITY_INFORMATION           = 0x00000020
+       SCOPE_SECURITY_INFORMATION               = 0x00000040
+       PROCESS_TRUST_LABEL_SECURITY_INFORMATION = 0x00000080
+       BACKUP_SECURITY_INFORMATION              = 0x00010000
+)
index d087fd46f6b27b718b8de5b967a792fad4d37f8b..9526e855e8f1ba240bc81c10e89a64547c33c32d 100644 (file)
@@ -62,6 +62,8 @@ var (
        procOpenThreadToken                   = modadvapi32.NewProc("OpenThreadToken")
        procQueryServiceStatus                = modadvapi32.NewProc("QueryServiceStatus")
        procRevertToSelf                      = modadvapi32.NewProc("RevertToSelf")
+       procSetEntriesInAclW                  = modadvapi32.NewProc("SetEntriesInAclW")
+       procSetNamedSecurityInfoW             = modadvapi32.NewProc("SetNamedSecurityInfoW")
        procSetTokenInformation               = modadvapi32.NewProc("SetTokenInformation")
        procProcessPrng                       = modbcryptprimitives.NewProc("ProcessPrng")
        procGetAdaptersAddresses              = modiphlpapi.NewProc("GetAdaptersAddresses")
@@ -236,6 +238,31 @@ func RevertToSelf() (err error) {
        return
 }
 
+func SetEntriesInAcl(countExplicitEntries uint32, explicitEntries *EXPLICIT_ACCESS, oldACL syscall.Handle, newACL *syscall.Handle) (ret error) {
+       r0, _, _ := syscall.SyscallN(procSetEntriesInAclW.Addr(), uintptr(countExplicitEntries), uintptr(unsafe.Pointer(explicitEntries)), uintptr(oldACL), uintptr(unsafe.Pointer(newACL)))
+       if r0 != 0 {
+               ret = syscall.Errno(r0)
+       }
+       return
+}
+
+func SetNamedSecurityInfo(objectName string, objectType int32, securityInformation uint32, owner *syscall.SID, group *syscall.SID, dacl syscall.Handle, sacl syscall.Handle) (ret error) {
+       var _p0 *uint16
+       _p0, ret = syscall.UTF16PtrFromString(objectName)
+       if ret != nil {
+               return
+       }
+       return _SetNamedSecurityInfo(_p0, objectType, securityInformation, owner, group, dacl, sacl)
+}
+
+func _SetNamedSecurityInfo(objectName *uint16, objectType int32, securityInformation uint32, owner *syscall.SID, group *syscall.SID, dacl syscall.Handle, sacl syscall.Handle) (ret error) {
+       r0, _, _ := syscall.SyscallN(procSetNamedSecurityInfoW.Addr(), uintptr(unsafe.Pointer(objectName)), uintptr(objectType), uintptr(securityInformation), uintptr(unsafe.Pointer(owner)), uintptr(unsafe.Pointer(group)), uintptr(dacl), uintptr(sacl))
+       if r0 != 0 {
+               ret = syscall.Errno(r0)
+       }
+       return
+}
+
 func SetTokenInformation(tokenHandle syscall.Token, tokenInformationClass uint32, tokenInformation unsafe.Pointer, tokenInformationLength uint32) (err error) {
        r1, _, e1 := syscall.SyscallN(procSetTokenInformation.Addr(), uintptr(tokenHandle), uintptr(tokenInformationClass), uintptr(tokenInformation), uintptr(tokenInformationLength))
        if r1 == 0 {