From: Michal Pristas Date: Wed, 4 Feb 2026 09:36:59 +0000 (+0000) Subject: os: support deleting inaccessible files in RemoveAll X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=db5882ea9bf63849d554910f486c4c5cfea08852;p=gostls13.git os: support deleting inaccessible files in RemoveAll 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 LUCI-TryBot-Result: Go LUCI Reviewed-by: Michael Pratt Auto-Submit: Michael Pratt Reviewed-by: Michael Knyszek --- diff --git a/src/internal/syscall/windows/at_windows.go b/src/internal/syscall/windows/at_windows.go index de733c523c..81361c98e4 100644 --- a/src/internal/syscall/windows/at_windows.go +++ b/src/internal/syscall/windows/at_windows.go @@ -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) diff --git a/src/internal/syscall/windows/at_windows_test.go b/src/internal/syscall/windows/at_windows_test.go index daeb4fcde3..afe46a1796 100644 --- a/src/internal/syscall/windows/at_windows_test.go +++ b/src/internal/syscall/windows/at_windows_test.go @@ -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) + } +} diff --git a/src/internal/syscall/windows/syscall_windows.go b/src/internal/syscall/windows/syscall_windows.go index b908a2c251..5a4db289b8 100644 --- a/src/internal/syscall/windows/syscall_windows.go +++ b/src/internal/syscall/windows/syscall_windows.go @@ -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 diff --git a/src/internal/syscall/windows/types_windows.go b/src/internal/syscall/windows/types_windows.go index 568f94624c..50213a2160 100644 --- a/src/internal/syscall/windows/types_windows.go +++ b/src/internal/syscall/windows/types_windows.go @@ -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 +) diff --git a/src/internal/syscall/windows/zsyscall_windows.go b/src/internal/syscall/windows/zsyscall_windows.go index d087fd46f6..9526e855e8 100644 --- a/src/internal/syscall/windows/zsyscall_windows.go +++ b/src/internal/syscall/windows/zsyscall_windows.go @@ -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 {