//go:build windows package safeio import ( "os" "path/filepath" "runtime" "strings" "unsafe" "golang.org/x/sys/windows" ) func createPrivateDirectory(path string) error { parents, target, err := openCanonicalWindowsParentWithFinalAccess(path, windows.FILE_APPEND_DATA) if err != nil || parents == nil || len(parents.handles) == 0 { if parents != nil { parents.Close() } return ErrUnsafeFile } defer parents.Close() handle, err := createWindowsRelativePrivateDirectory(parents.handles[len(parents.handles)-1], target) if isWindowsRelativeCollision(err) { return os.ErrExist } if err != nil { return ErrUnsafeFile } if err := windows.CloseHandle(handle); err != nil { return ErrUnsafeFile } return nil } // ProtectPrivateDirectory sets a protected DACL containing only the current owner. func ProtectPrivateDirectory(path string) error { parents, target, err := openCanonicalWindowsParent(path) if err != nil || parents == nil || len(parents.handles) == 0 { if parents != nil { parents.Close() } return ErrUnsafeFile } defer parents.Close() handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], target, true, windows.GENERIC_READ|windows.WRITE_DAC|windows.WRITE_OWNER) if err != nil { return ErrUnsafeFile } defer windows.CloseHandle(handle) if err := setOwnerOnlyDACL(handle); err != nil { return ErrUnsafeFile } return validateOwnerOnlyDACL(handle) } // ValidatePrivateDirectory requires a canonical directory protected for its current owner only. func ValidatePrivateDirectory(path string) error { parents, target, err := openCanonicalWindowsParent(path) if err != nil || parents == nil || len(parents.handles) == 0 { if parents != nil { parents.Close() } return ErrUnsafeFile } defer parents.Close() handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], target, true, windows.GENERIC_READ) if err != nil { return ErrUnsafeFile } defer windows.CloseHandle(handle) if err := validateOwnerOnlyDACL(handle); err != nil { return ErrUnsafeFile } return nil } // ProtectPrivateRegular sets a protected DACL containing only the current owner. func ProtectPrivateRegular(path string) error { parents, target, err := openCanonicalWindowsParent(path) if err != nil || parents == nil || len(parents.handles) == 0 { if parents != nil { parents.Close() } return ErrUnsafeFile } defer parents.Close() handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], target, false, windows.GENERIC_READ|windows.WRITE_DAC|windows.WRITE_OWNER) if err != nil { return ErrUnsafeFile } defer windows.CloseHandle(handle) if err := setOwnerOnlyDACL(handle); err != nil { return ErrUnsafeFile } return validateOwnerOnlyDACL(handle) } // createCanonicalNewPrivateFile installs the owner-only protected DACL in the CreateFile call, so // another mutation can never observe a newly-created lock with an inherited/default DACL. func createCanonicalNewPrivateFile(path string, mode os.FileMode) (*os.File, error) { return createCanonicalNewFile(path, mode, false, windows.GENERIC_WRITE) } func createCanonicalNewPrivateParentFile(path string, mode os.FileMode) (*os.File, error) { return createCanonicalNewFile(path, mode, true, windows.GENERIC_WRITE) } func createCanonicalNewPrivateParentReadWriteFile(path string, mode os.FileMode) (*os.File, error) { return createCanonicalNewFile(path, mode, true, windows.GENERIC_READ|windows.GENERIC_WRITE) } func createCanonicalNewFile(path string, mode os.FileMode, requirePrivateParent bool, access uint32) (*os.File, error) { // FILE_WRITE_DATA is FILE_ADD_FILE when the retained handle names a directory. Request it // while that final parent is opened, rather than reopening its lexical path later to gain // create permission. parents, target, err := openCanonicalWindowsParentWithFinalAccess(path, windows.FILE_WRITE_DATA) if err != nil || parents == nil || len(parents.handles) == 0 || (requirePrivateParent && validateOwnerOnlyDACL(parents.handles[len(parents.handles)-1]) != nil) { if parents != nil { parents.Close() } return nil, ErrUnsafeFile } defer parents.Close() NotifyPrivateDirectoryTestHookForTest("after-canonical-private-file-parent-open") // mode remains accepted for the existing helper contract; Windows installs the owner-only // DACL in the NtCreateFile call below rather than relying on inherited file attributes. _ = mode value, err := createWindowsPrivateRegularAtWithAccess(parents.handles[len(parents.handles)-1], target, access) if err != nil { return nil, err } file := os.NewFile(uintptr(value.handle), "tht-safeio-private") if file == nil { _ = closeAndDeleteWindowsPrivateRegular(value) return nil, ErrUnsafeFile } value.handle = 0 return file, nil } // ValidatePrivateRegular requires a canonical, single-link file protected for its current owner only. func ValidatePrivateRegular(path string) error { parents, target, err := openCanonicalWindowsParent(path) if err != nil || parents == nil || len(parents.handles) == 0 { if parents != nil { parents.Close() } return ErrUnsafeFile } defer parents.Close() value, err := openWindowsPrivateRegularAt(parents.handles[len(parents.handles)-1], target, windows.GENERIC_READ, 1) if err != nil { return ErrUnsafeFile } if err := value.Close(); err != nil { return ErrUnsafeFile } return nil } type windowsParentHandles struct { directory string handles []windows.Handle } func (parents *windowsParentHandles) Close() { for index := len(parents.handles) - 1; index >= 0; index-- { _ = windows.CloseHandle(parents.handles[index]) } } // openCanonicalWindowsParent retains every directory handle from the volume root through the // target parent without FILE_SHARE_DELETE. Every component after the volume root is resolved // through the prior retained handle's NT RootDirectory, never by re-opening an absolute prefix. func openCanonicalWindowsParent(path string) (*windowsParentHandles, string, error) { return openCanonicalWindowsParentWithFinalAccess(path, 0) } // openCanonicalWindowsParentWithFinalAccess gives only the final retained parent the requested // child-operation capability. It is the Windows openat traversal for a later relative create; // reopening that parent by its reconstructed path would recreate the ancestor-swap race. func openCanonicalWindowsParentWithFinalAccess(path string, finalParentAccess uint32) (*windowsParentHandles, string, error) { if err := ValidateCanonicalPath(path); err != nil { return nil, "", err } volume := filepath.VolumeName(path) root := volume + `\` components := strings.Split(strings.TrimPrefix(path, root), `\`) if volume == "" || len(components) == 0 || components[0] == "" { return nil, "", ErrUnsafeFile } parents := &windowsParentHandles{directory: root} rootAccess := uint32(windows.GENERIC_READ) if len(components) == 1 { rootAccess |= finalParentAccess } rootHandle, err := openWindowsComponentWithAccess(root, true, rootAccess) if err != nil { return nil, "", err } parents.handles = append(parents.handles, rootHandle) for index, component := range components[:len(components)-1] { componentAccess := uint32(windows.GENERIC_READ) if index == len(components)-2 { componentAccess |= finalParentAccess } handle, err := openWindowsRelativeComponent(parents.handles[len(parents.handles)-1], component, true, componentAccess) if err != nil { parents.Close() return nil, "", err } parents.directory = filepath.Join(parents.directory, component) parents.handles = append(parents.handles, handle) } return parents, components[len(components)-1], nil } type ownerOnlyDACL struct { sid *windows.SID acl *windows.ACL pinner runtime.Pinner } func newOwnerOnlyDACL() (*ownerOnlyDACL, error) { tokenUser, err := windows.GetCurrentProcessToken().GetTokenUser() if err != nil || tokenUser == nil || tokenUser.User.Sid == nil { return nil, ErrUnsafeFile } sid, err := tokenUser.User.Sid.Copy() if err != nil { return nil, ErrUnsafeFile } owner := &ownerOnlyDACL{sid: sid} owner.pinner.Pin(owner.sid) acl, err := windows.ACLFromEntries([]windows.EXPLICIT_ACCESS{{ AccessPermissions: windows.GENERIC_ALL, AccessMode: windows.GRANT_ACCESS, Trustee: windows.TRUSTEE{ TrusteeForm: windows.TRUSTEE_IS_SID, TrusteeType: windows.TRUSTEE_IS_USER, TrusteeValue: windows.TrusteeValueFromSID(owner.sid), }, }}, nil) if err != nil { owner.pinner.Unpin() return nil, err } owner.acl = acl return owner, nil } func (owner *ownerOnlyDACL) Close() { owner.pinner.Unpin() } type ownerOnlySecurityDescriptor struct { *ownerOnlyDACL descriptor *windows.SECURITY_DESCRIPTOR } func newOwnerOnlySecurityDescriptor() (*ownerOnlySecurityDescriptor, error) { owner, err := newOwnerOnlyDACL() if err != nil { return nil, err } descriptor, err := windows.NewSecurityDescriptor() if err == nil { err = descriptor.SetOwner(owner.sid, false) } if err == nil { err = descriptor.SetDACL(owner.acl, true, false) } if err == nil { err = descriptor.SetControl(windows.SE_DACL_PROTECTED, windows.SE_DACL_PROTECTED) } if err != nil || !descriptor.IsValid() { owner.Close() return nil, ErrUnsafeFile } selfRelative, err := descriptor.ToSelfRelative() if err != nil || selfRelative == nil || !selfRelative.IsValid() { owner.Close() return nil, ErrUnsafeFile } return &ownerOnlySecurityDescriptor{ownerOnlyDACL: owner, descriptor: selfRelative}, nil } func (descriptor *ownerOnlySecurityDescriptor) Close() { descriptor.ownerOnlyDACL.Close() } func setOwnerOnlyDACL(handle windows.Handle) error { owner, err := newOwnerOnlyDACL() if err != nil { return err } defer owner.Close() return windows.SetSecurityInfo(handle, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, owner.sid, nil, owner.acl, nil) } func validateOwnerOnlyDACL(handle windows.Handle) error { ownerSID, err := currentOwnerSID() if err != nil { return err } return withWindowsSecurityDescriptor(handle, func(descriptor *windows.SECURITY_DESCRIPTOR) error { owner, _, err := descriptor.Owner() if err != nil || owner == nil || !windows.EqualSid(owner, ownerSID) { return ErrUnsafeFile } control, _, err := descriptor.Control() if err != nil || control&windows.SE_DACL_PROTECTED == 0 { return ErrUnsafeFile } dacl, defaulted, err := descriptor.DACL() if err != nil || defaulted || dacl == nil || dacl.AceCount != 1 { return ErrUnsafeFile } var ace *windows.ACCESS_ALLOWED_ACE if err := windows.GetAce(dacl, 0, &ace); err != nil || ace == nil || ace.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE || ace.Header.AceFlags != 0 || !isOwnerOnlyFullControlMask(uint32(ace.Mask)) { return ErrUnsafeFile } aceSID := (*windows.SID)(unsafe.Pointer(&ace.SidStart)) if !windows.EqualSid(aceSID, ownerSID) { return ErrUnsafeFile } return nil }) } func isOwnerOnlyFullControlMask(mask uint32) bool { // Windows may persist GENERIC_ALL in the ACE or expand it to the file-object // full-control mask. FILE_ALL_ACCESS is the standard-rights set, synchronize, // and all file-specific rights. Both are the same semantic authority; any // additional or missing bit remains unsafe. const fileSpecificAll = uint32(0x1ff) effective := uint32(windows.STANDARD_RIGHTS_REQUIRED|windows.SYNCHRONIZE) | fileSpecificAll return mask == uint32(windows.GENERIC_ALL) || mask == effective } // withWindowsSecurityDescriptor confines inspection to x/sys's Go-owned descriptor copy. Its // GetSecurityInfo wrapper releases the native LocalAlloc result with LocalFree before returning. func withWindowsSecurityDescriptor(handle windows.Handle, inspect func(*windows.SECURITY_DESCRIPTOR) error) error { descriptor, err := windows.GetSecurityInfo(handle, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION) if err != nil || descriptor == nil { return ErrUnsafeFile } return inspect(descriptor) } func currentOwnerSID() (*windows.SID, error) { user, err := windows.GetCurrentProcessToken().GetTokenUser() if err != nil || user == nil || user.User.Sid == nil { return nil, ErrUnsafeFile } sid, err := user.User.Sid.Copy() if err != nil { return nil, ErrUnsafeFile } return sid, nil }