Files
ThothII/tools/thothctl/internal/safeio/files_windows.go
T

260 lines
8.9 KiB
Go

//go:build windows
package safeio
import (
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"unsafe"
"golang.org/x/sys/windows"
)
const windowsRetainedHandleShareMode uint32 = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE
// ReadCanonicalRegular opens each component with FILE_FLAG_OPEN_REPARSE_POINT and rejects a
// reparse point on the opened handle before opening the next component. Retained handles allow
// ordinary read/write sharing but deny delete sharing, which blocks rename or deletion after a
// component is opened and throughout the final read. Windows' Win32 API does not expose a
// portable descriptor-relative equivalent of POSIX openat, so a hostile local actor can still
// replace a not-yet-opened normal component between absolute-path opens. Installation directories
// therefore need trusted local filesystem/ACL ownership on Windows.
func ReadCanonicalRegular(path string, maximum int64) ([]byte, error) {
if err := ValidateCanonicalPath(path); err != nil {
return nil, err
}
volume := filepath.VolumeName(path)
root := volume + string(filepath.Separator)
components := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator))
if volume == "" || len(components) == 0 || components[0] == "" {
return nil, ErrUnsafeFile
}
current := root
parents := make([]windows.Handle, 0, len(components)-1)
defer func() { closeWindowsHandles(parents) }()
for _, component := range components[:len(components)-1] {
current = filepath.Join(current, component)
handle, err := openWindowsComponent(current, true)
if err != nil {
return nil, ErrUnsafeFile
}
parents = append(parents, handle)
}
current = filepath.Join(current, components[len(components)-1])
handle, err := openWindowsComponent(current, false)
if err != nil {
return nil, ErrUnsafeFile
}
file := os.NewFile(uintptr(handle), "thothctl-safeio")
if file == nil {
windows.CloseHandle(handle)
return nil, ErrUnsafeFile
}
defer file.Close()
var before, after windows.ByHandleFileInformation
if windows.GetFileInformationByHandle(handle, &before) != nil || before.NumberOfLinks > 1 {
return nil, ErrUnsafeFile
}
contents, err := readBoundedRegularFile(file, maximum)
if err != nil {
return nil, ErrUnsafeFile
}
if windows.GetFileInformationByHandle(handle, &after) != nil || after.NumberOfLinks > 1 || !sameWindowsFile(before, after) {
return nil, ErrUnsafeFile
}
// Re-opened pathname identity is checked as well as the retained handle. This
// catches replacement by an ordinary local actor where share/delete policy permits it.
check, err := openWindowsComponent(current, false)
if err != nil {
return nil, ErrUnsafeFile
}
defer windows.CloseHandle(check)
var named windows.ByHandleFileInformation
if windows.GetFileInformationByHandle(check, &named) != nil || named.NumberOfLinks > 1 || !sameWindowsFile(before, named) {
return nil, ErrUnsafeFile
}
return contents, nil
}
func openWindowsComponent(path string, directory bool) (windows.Handle, error) {
flags := uint32(windows.FILE_FLAG_OPEN_REPARSE_POINT)
if directory {
flags |= windows.FILE_FLAG_BACKUP_SEMANTICS
} else {
flags |= windows.FILE_ATTRIBUTE_NORMAL
}
handle, err := windows.CreateFile(
windows.StringToUTF16Ptr(path),
windows.GENERIC_READ,
windowsRetainedHandleShareMode,
nil,
windows.OPEN_EXISTING,
flags,
0,
)
if err != nil {
return 0, err
}
var information windows.ByHandleFileInformation
if err := windows.GetFileInformationByHandle(handle, &information); err != nil {
windows.CloseHandle(handle)
return 0, err
}
if information.NumberOfLinks > 1 || information.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 ||
(directory && information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0) ||
(!directory && information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0) {
windows.CloseHandle(handle)
return 0, ErrUnsafeFile
}
return handle, nil
}
func closeWindowsHandles(handles []windows.Handle) {
for _, handle := range handles {
windows.CloseHandle(handle)
}
}
func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) error {
if err := ValidateCanonicalPath(path); err != nil || len(contents) > 16<<20 || mode.Perm() != 0o600 {
return ErrUnsafeFile
}
parent, retainedParents, err := openWindowsParents(path)
if err != nil {
return ErrUnsafeFile
}
defer closeWindowsHandles(retainedParents)
// Parent handles stay open with delete sharing denied until publication and
// identity recheck complete; this is the Windows equivalent of retained dirfds.
securityDescriptor, securityAttributes, err := ownerOnlySecurityAttributes()
if err != nil {
return ErrUnsafeFile
}
h, err := windows.CreateFile(windows.StringToUTF16Ptr(filepath.Join(parent, filepath.Base(path))), windows.GENERIC_WRITE, 0, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil {
return ErrUnsafeFile
}
f := os.NewFile(uintptr(h), "thothctl-safeio-output")
if f == nil {
windows.CloseHandle(h)
return ErrUnsafeFile
}
defer f.Close()
// Go's Windows Chmod only toggles the read-only attribute; it cannot enforce
// owner-only permissions. The restrictive DACL is installed at creation time
// through SECURITY_ATTRIBUTES above.
_ = securityDescriptor
if mode.Perm() != 0o600 {
return ErrUnsafeFile
}
if _, err := f.Write(contents); err != nil {
return ErrUnsafeFile
}
if err := f.Sync(); err != nil {
return ErrUnsafeFile
}
var opened, named windows.ByHandleFileInformation
if windows.GetFileInformationByHandle(h, &opened) != nil || opened.NumberOfLinks != 1 {
return ErrUnsafeFile
}
check, err := openWindowsComponent(path, false)
if err != nil {
return ErrUnsafeFile
}
defer windows.CloseHandle(check)
if windows.GetFileInformationByHandle(check, &named) != nil || named.NumberOfLinks != 1 || !sameWindowsFile(opened, named) {
return ErrUnsafeFile
}
return nil
}
func openWindowsParents(path string) (string, []windows.Handle, error) {
volume := filepath.VolumeName(path)
root := volume + string(filepath.Separator)
components := strings.Split(strings.TrimPrefix(path, root), string(filepath.Separator))
if volume == "" || len(components) < 2 || components[0] == "" {
return "", nil, ErrUnsafeFile
}
current := root
parents := make([]windows.Handle, 0, len(components)-1)
for _, component := range components[:len(components)-1] {
current = filepath.Join(current, component)
h, err := openWindowsComponent(current, true)
if err != nil {
closeWindowsHandles(parents)
return "", nil, ErrUnsafeFile
}
parents = append(parents, h)
}
return current, parents, nil
}
func validateCanonicalOutputPath(path string) error {
if err := ValidateCanonicalPath(path); err != nil {
return err
}
current, parents, err := openWindowsParents(path)
if err != nil {
return ErrUnsafeFile
}
defer closeWindowsHandles(parents)
if _, err := os.Lstat(filepath.Join(current, filepath.Base(path))); err == nil || !os.IsNotExist(err) {
return ErrUnsafeFile
}
return nil
}
func sameWindowsFile(a, b windows.ByHandleFileInformation) bool {
return a.VolumeSerialNumber == b.VolumeSerialNumber && a.FileIndexHigh == b.FileIndexHigh && a.FileIndexLow == b.FileIndexLow
}
// ownerOnlySecurityAttributes builds a non-inheriting DACL granting the current
// token's user read/write access and no access to inherited or other trustees.
// The descriptor is retained by the returned Go value for the duration of CreateFile.
func ownerOnlySecurityAttributes() (*windows.SECURITY_DESCRIPTOR, *windows.SecurityAttributes, error) {
user, err := windows.GetCurrentProcessToken().GetTokenUser()
if err != nil || user == nil || user.User.Sid == nil {
return nil, nil, ErrUnsafeFile
}
var pinner runtime.Pinner
pinner.Pin(user.User.Sid)
defer pinner.Unpin()
trustee := windows.TRUSTEE{
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_USER,
TrusteeValue: windows.TrusteeValueFromSID(user.User.Sid),
}
entries := []windows.EXPLICIT_ACCESS{{
AccessPermissions: windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE,
AccessMode: windows.SET_ACCESS,
Inheritance: windows.NO_INHERITANCE,
Trustee: trustee,
}}
descriptor, err := windows.BuildSecurityDescriptor(nil, nil, entries, nil, nil)
if err != nil || descriptor == nil {
return nil, nil, ErrUnsafeFile
}
acl, _, err := descriptor.DACL()
if err != nil || acl == nil {
return nil, nil, ErrUnsafeFile
}
if err := descriptor.SetControl(windows.SE_DACL_PROTECTED, windows.SE_DACL_PROTECTED); err != nil {
return nil, nil, ErrUnsafeFile
}
// BuildSecurityDescriptor returns a self-relative descriptor. Re-using its
// DACL as the creation descriptor is valid, and the explicit DACL has no
// inheritable ACEs; the protected flag is applied by the kernel on creation.
_ = acl
attrs := &windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: descriptor,
}
return descriptor, attrs, nil
}