//go:build windows package safeio import ( "errors" "os" "path/filepath" "runtime" "strings" "unsafe" "golang.org/x/sys/windows" ) func createPrivateDirectory(path string) error { parents, target, err := openCanonicalWindowsParent(path) if err != nil { return ErrUnsafeFile } defer parents.Close() security, err := newOwnerOnlySecurityDescriptor() if err != nil { return ErrUnsafeFile } defer security.Close() attributes := &windows.SecurityAttributes{ Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), SecurityDescriptor: security.descriptor, } err = windows.CreateDirectory(windows.StringToUTF16Ptr(filepath.Join(parents.directory, target)), attributes) runtime.KeepAlive(security) if errors.Is(err, windows.ERROR_ALREADY_EXISTS) { return os.ErrExist } if err != nil { return ErrUnsafeFile } return ValidatePrivateDirectory(path) } // ProtectPrivateDirectory sets a protected DACL containing only the current owner. func ProtectPrivateDirectory(path string) error { parents, target, err := openCanonicalWindowsParent(path) if err != nil { return ErrUnsafeFile } defer parents.Close() handle, err := openWindowsComponentWithAccess(filepath.Join(parents.directory, 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 { return ErrUnsafeFile } defer parents.Close() handle, err := openWindowsComponent(filepath.Join(parents.directory, target), true) 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 { return ErrUnsafeFile } defer parents.Close() handle, err := openWindowsComponentWithAccess(filepath.Join(parents.directory, 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) { security, err := newOwnerOnlySecurityDescriptor() if err != nil { return nil, ErrUnsafeFile } defer security.Close() attributes := &windows.SecurityAttributes{ Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), SecurityDescriptor: security.descriptor, } handle, err := windows.CreateFile( windows.StringToUTF16Ptr(path), windows.GENERIC_WRITE, windowsRetainedHandleShareMode, attributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL, 0, ) runtime.KeepAlive(security) if err != nil { return nil, err } if err := validateOwnerOnlyDACL(handle); err != nil { _ = windows.CloseHandle(handle) _ = os.Remove(path) return nil, ErrUnsafeFile } file := os.NewFile(uintptr(handle), "tht-safeio-private") if file == nil { _ = windows.CloseHandle(handle) _ = os.Remove(path) return nil, ErrUnsafeFile } 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 { return ErrUnsafeFile } defer parents.Close() handle, err := openWindowsComponent(filepath.Join(parents.directory, target), false) if err != nil { return ErrUnsafeFile } defer windows.CloseHandle(handle) if err := validateOwnerOnlyDACL(handle); 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. The resulting parent cannot be renamed or replaced by // a reparse point while an operation uses its absolute child paths. func openCanonicalWindowsParent(path string) (*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} rootHandle, err := openWindowsComponent(root, true) if err != nil { return nil, "", err } parents.handles = append(parents.handles, rootHandle) for _, component := range components[:len(components)-1] { parents.directory = filepath.Join(parents.directory, component) handle, err := openWindowsComponent(parents.directory, true) if err != nil { parents.Close() return nil, "", err } 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 } return &ownerOnlySecurityDescriptor{ownerOnlyDACL: owner, descriptor: descriptor}, 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 || ace.Mask != windows.GENERIC_ALL { return ErrUnsafeFile } aceSID := (*windows.SID)(unsafe.Pointer(&ace.SidStart)) if !windows.EqualSid(aceSID, ownerSID) { return ErrUnsafeFile } return nil }) } // 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 }