361 lines
12 KiB
Go
361 lines
12 KiB
Go
//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
|
|
}
|
|
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 || !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 (including FILE_DELETE_CHILD). Both are the same semantic
|
|
// authority; any additional bit remains unsafe.
|
|
const fileDeleteChild = uint32(0x40)
|
|
effective := uint32(windows.FILE_GENERIC_READ|windows.FILE_GENERIC_WRITE|windows.FILE_GENERIC_EXECUTE|windows.DELETE) | fileDeleteChild
|
|
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
|
|
}
|