113 lines
4.1 KiB
Go
113 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// THE WINDOWS OPEN, DECIDED WHERE A LINUX TEST CAN SEE IT.
|
|
//
|
|
// mcpconfig_open_windows.go opens a config path with CreateFile and asks the
|
|
// HANDLE what it holds; every decision about the answer is here, with no
|
|
// build tag, so the Linux test run checks the Windows rules with fake handles
|
|
// (mcpconfig_winopen_test.go). Nothing here calls Windows.
|
|
//
|
|
// The open does not follow a reparse point at the last component
|
|
// (FILE_FLAG_OPEN_REPARSE_POINT): a symbolic link, junction or other
|
|
// reparse point at a config path is opened as itself, seen as a reparse
|
|
// point, and refused. What is checked is the handle that will be read, so
|
|
// there is no gap between a look and an open for a swap to fall into. A
|
|
// directory, a device, a pipe or anything else that is not a file on disk is
|
|
// refused the same way. A reparse point in a directory ABOVE the file is
|
|
// followed, as for any program opening the path; the file it leads to must
|
|
// then be owned by the account whose profile it is read for, or by SYSTEM or
|
|
// the Administrators group (mcpOwnerAllowed), so a link into another
|
|
// account's profile reads nothing of it.
|
|
|
|
// The Windows values the decision reads, from the SDK headers (winbase.h,
|
|
// winnt.h) as golang.org/x/sys/windows carries them: FILE_TYPE_DISK,
|
|
// FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_DEVICE, FILE_ATTRIBUTE_REPARSE_POINT.
|
|
const (
|
|
mcpWinFileTypeDisk = 0x0001
|
|
mcpWinAttrDirectory = 0x0010
|
|
mcpWinAttrDevice = 0x0040
|
|
mcpWinAttrReparse = 0x0400
|
|
)
|
|
|
|
// mcpWinAdminOwners: the owners a file in any account's profile may have
|
|
// besides that account. SYSTEM (S-1-5-18), and the Administrators group
|
|
// (S-1-5-32-544), which is the default owner of what an elevated
|
|
// administrator creates.
|
|
var mcpWinAdminOwners = map[string]bool{"S-1-5-18": true, "S-1-5-32-544": true}
|
|
|
|
// mcpWinHandleInfo is what the open read from the handle it holds.
|
|
type mcpWinHandleInfo struct {
|
|
FileType uint32 // GetFileType
|
|
Attributes uint32 // GetFileInformationByHandle's FileAttributes
|
|
Owner string // the owner SID from GetSecurityInfo; "" when unread
|
|
}
|
|
|
|
// mcpWinRegular: the handle is a file on disk that is not a directory, a
|
|
// device or a reparse point.
|
|
func mcpWinRegular(i mcpWinHandleInfo) bool {
|
|
return i.FileType == mcpWinFileTypeDisk &&
|
|
i.Attributes&(mcpWinAttrDirectory|mcpWinAttrDevice|mcpWinAttrReparse) == 0
|
|
}
|
|
|
|
// mcpWinOps are the Windows calls the open makes, as values so a test can
|
|
// stand fakes in for them. The field names are not method names anywhere in
|
|
// the package: the claims test resolves a call through a value by name.
|
|
type mcpWinOps struct {
|
|
openPath func(path string) (uintptr, error)
|
|
handleInfo func(h uintptr) (mcpWinHandleInfo, error)
|
|
closeHandle func(h uintptr)
|
|
asFile func(h uintptr, path string) *os.File
|
|
}
|
|
|
|
// mcpWinOpenWith opens path, checks the handle, and hands back the file and
|
|
// its owner SID. A handle that is refused is closed before return and never
|
|
// becomes a file.
|
|
func mcpWinOpenWith(ops mcpWinOps, path string) (*os.File, string, error) {
|
|
h, err := ops.openPath(path)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
info, err := ops.handleInfo(h)
|
|
if err != nil {
|
|
ops.closeHandle(h)
|
|
return nil, "", &os.PathError{Op: "query", Path: path, Err: err}
|
|
}
|
|
if !mcpWinRegular(info) {
|
|
ops.closeHandle(h)
|
|
return nil, "", errMCPNotRegular
|
|
}
|
|
owner := info.Owner
|
|
if owner == "" {
|
|
owner = mcpOwnerUnknown
|
|
}
|
|
return ops.asFile(h, path), owner, nil
|
|
}
|
|
|
|
// mcpWinProfileSID is the SID whose profile directory is dir, from the
|
|
// ProfileList (SID to ProfileImagePath, already expanded). mcpOwnerUnknown
|
|
// when no SID or more than one names it; only an administrator's or
|
|
// SYSTEM's files are then read there.
|
|
func mcpWinProfileSID(dir string, profiles map[string]string) string {
|
|
norm := func(p string) string {
|
|
return strings.ToLower(strings.TrimRight(strings.ReplaceAll(p, "/", `\`), `\`))
|
|
}
|
|
want := norm(dir)
|
|
var hits []string
|
|
for sid, p := range profiles {
|
|
if strings.HasPrefix(sid, "S-1-") && !strings.Contains(sid, ".") && norm(p) == want {
|
|
hits = append(hits, sid)
|
|
}
|
|
}
|
|
sort.Strings(hits)
|
|
if len(hits) != 1 {
|
|
return mcpOwnerUnknown
|
|
}
|
|
return hits[0]
|
|
}
|