|
|
@@ -0,0 +1,317 @@
|
|
|
+// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
+// SPDX-License-Identifier: BSD-3-Clause
|
|
|
+
|
|
|
+package winutil
|
|
|
+
|
|
|
+import (
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "os"
|
|
|
+ "slices"
|
|
|
+ "unsafe"
|
|
|
+
|
|
|
+ "github.com/dblohm7/wingoes"
|
|
|
+ "golang.org/x/sys/windows"
|
|
|
+)
|
|
|
+
|
|
|
+var (
|
|
|
+ // ErrAlreadyResolved is returned by (*StartupInfoBuilder).Resolve when the
|
|
|
+ // StartupInfoBuilder has already been resolved.
|
|
|
+ ErrAlreadyResolved = errors.New("StartupInfo already resolved")
|
|
|
+ // ErrAlreadySet is returned by StartupInfoBuilder setters if the value
|
|
|
+ // has already been set.
|
|
|
+ ErrAlreadySet = errors.New("StartupInfoBuilder value already set")
|
|
|
+ // ErrTooManyMitigationPolicyArguments is returned by
|
|
|
+ // (*StartupInfoBuilder).AddMitigationPolicyFlags if more arguments are
|
|
|
+ // passed than are supported by the current version of Windows. This error
|
|
|
+ // may be wrapped with additional information, so use [errors.Is] to check for it.
|
|
|
+ ErrTooManyMitigationPolicyArguments = errors.New("too many mitigation policy arguments for current Windows version")
|
|
|
+)
|
|
|
+
|
|
|
+// Attribute IDs not yet present in x/sys/windows
|
|
|
+const (
|
|
|
+ _PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x0002000D
|
|
|
+)
|
|
|
+
|
|
|
+// Mitigation flags from the Win32 SDK
|
|
|
+const (
|
|
|
+ PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_NO_REMOTE_ALWAYS_ON = (1 << 52)
|
|
|
+ PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_NO_LOW_LABEL_ALWAYS_ON = (1 << 56)
|
|
|
+ PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON = (1 << 60)
|
|
|
+)
|
|
|
+
|
|
|
+// StartupInfoBuilder constructs a Windows STARTUPINFOEX and optional
|
|
|
+// process/thread attribute list for use with the CreateProcess family of APIs.
|
|
|
+type StartupInfoBuilder struct {
|
|
|
+ siex windows.StartupInfoEx
|
|
|
+ attrs map[uintptr]any // attr -> value
|
|
|
+ attrContainer *windows.ProcThreadAttributeListContainer
|
|
|
+}
|
|
|
+
|
|
|
+func (sib *StartupInfoBuilder) Close() error {
|
|
|
+ si := &sib.siex.StartupInfo
|
|
|
+ if (si.Flags & windows.STARTF_USESTDHANDLES) != 0 {
|
|
|
+ for _, h := range []windows.Handle{si.StdInput, si.StdOutput, si.StdErr} {
|
|
|
+ if canBeInherited(h) {
|
|
|
+ windows.CloseHandle(h)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ sib.siex = windows.StartupInfoEx{}
|
|
|
+ if sib.attrContainer != nil {
|
|
|
+ sib.attrContainer.Delete()
|
|
|
+ sib.attrContainer = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ sib.attrs = nil
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Resolve causes all settings and attributes stored within sib to be processed
|
|
|
+// and formatted into valid arguments for use by CreateProcess* APIs.
|
|
|
+// The returned values will not be altered any further by sib, so the caller
|
|
|
+// is free to make additional customizations to the returned values prior to
|
|
|
+// passing them into CreateProcess.
|
|
|
+func (sib *StartupInfoBuilder) Resolve() (startupInfo *windows.StartupInfo, inheritHandles bool, createProcessFlags uint32, err error) {
|
|
|
+ if sib.siex.StartupInfo.Cb != 0 {
|
|
|
+ return nil, false, 0, ErrAlreadyResolved
|
|
|
+ }
|
|
|
+
|
|
|
+ // Always create a Unicode environment.
|
|
|
+ createProcessFlags = windows.CREATE_UNICODE_ENVIRONMENT
|
|
|
+
|
|
|
+ if l := uint32(len(sib.attrs)); l > 0 {
|
|
|
+ attrCont, err := windows.NewProcThreadAttributeList(l)
|
|
|
+ if err != nil {
|
|
|
+ return nil, false, 0, err
|
|
|
+ }
|
|
|
+ defer func() {
|
|
|
+ if err != nil {
|
|
|
+ attrCont.Delete()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ for attr, val := range sib.attrs {
|
|
|
+ var pval unsafe.Pointer
|
|
|
+ var sval uintptr
|
|
|
+ switch v := val.(type) {
|
|
|
+ case windows.Handle:
|
|
|
+ // An individual handle is pointer-width and is thus passed by value.
|
|
|
+ pval = unsafe.Pointer(v)
|
|
|
+ sval = unsafe.Sizeof(v)
|
|
|
+ case []uint64:
|
|
|
+ pval = unsafe.Pointer(unsafe.SliceData(v))
|
|
|
+ sval = unsafe.Sizeof(v[0]) * uintptr(len(v))
|
|
|
+ case []windows.Handle:
|
|
|
+ pval = unsafe.Pointer(unsafe.SliceData(v))
|
|
|
+ sval = unsafe.Sizeof(v[0]) * uintptr(len(v))
|
|
|
+ default:
|
|
|
+ panic("unsupported data type")
|
|
|
+ }
|
|
|
+
|
|
|
+ // Note that pointer keepalives are managed by attrCont.
|
|
|
+ if err := attrCont.Update(attr, pval, sval); err != nil {
|
|
|
+ return nil, false, 0, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if attr == windows.PROC_THREAD_ATTRIBUTE_HANDLE_LIST {
|
|
|
+ inheritHandles = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ sib.attrContainer = attrCont
|
|
|
+ sib.siex.ProcThreadAttributeList = attrCont.List()
|
|
|
+ sib.siex.StartupInfo.Cb = uint32(unsafe.Sizeof(sib.siex))
|
|
|
+ createProcessFlags |= windows.EXTENDED_STARTUPINFO_PRESENT
|
|
|
+ } else {
|
|
|
+ sib.siex.StartupInfo.Cb = uint32(unsafe.Sizeof(sib.siex.StartupInfo))
|
|
|
+ }
|
|
|
+
|
|
|
+ return &sib.siex.StartupInfo, inheritHandles, createProcessFlags, nil
|
|
|
+}
|
|
|
+
|
|
|
+func canBeInherited(h windows.Handle) bool {
|
|
|
+ if h == 0 || h == windows.InvalidHandle {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ ft, _ := windows.GetFileType(h)
|
|
|
+ switch ft {
|
|
|
+ case windows.FILE_TYPE_DISK, windows.FILE_TYPE_PIPE:
|
|
|
+ return true
|
|
|
+ case windows.FILE_TYPE_CHAR:
|
|
|
+ // Console handles are treated differently from other character devices.
|
|
|
+ // In particular, they should not be set up to be inherited like other
|
|
|
+ // kernel handles. We determine whether h is a console handle by attempting
|
|
|
+ // to retrieve its console mode. If this call fails then h is not a console.
|
|
|
+ var mode uint32
|
|
|
+ return windows.GetConsoleMode(h, &mode) != nil
|
|
|
+ default:
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// SetStdHandles sets the StdInput, StdOutput, and StdErr handles and configures
|
|
|
+// their inheritability as needed. When the handles are valid, non-console
|
|
|
+// kernel objects, sib takes ownership of of them. All three handles may be set
|
|
|
+// to zero to indicate that the parent's std handles should not be implicitly
|
|
|
+// inherited.
|
|
|
+//
|
|
|
+// It returns ErrAlreadySet if the handles have already been set by a previous call.
|
|
|
+func (sib *StartupInfoBuilder) SetStdHandles(stdin, stdout, stderr windows.Handle) error {
|
|
|
+ if (sib.siex.StartupInfo.Flags & windows.STARTF_USESTDHANDLES) != 0 {
|
|
|
+ return ErrAlreadySet
|
|
|
+ }
|
|
|
+
|
|
|
+ toInherit := make([]windows.Handle, 0, 3)
|
|
|
+ for _, h := range []windows.Handle{stdin, stdout, stderr} {
|
|
|
+ if !canBeInherited(h) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ toInherit = append(toInherit, h)
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := sib.InheritHandles(toInherit...); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ sib.siex.StartupInfo.Flags |= windows.STARTF_USESTDHANDLES
|
|
|
+ sib.siex.StartupInfo.StdInput = stdin
|
|
|
+ sib.siex.StartupInfo.StdOutput = stdout
|
|
|
+ sib.siex.StartupInfo.StdErr = stderr
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (sib *StartupInfoBuilder) makeAttrs() {
|
|
|
+ if sib.attrs == nil {
|
|
|
+ // The size of this map should correspond to the number of distinct
|
|
|
+ // attribute values supported by the StartupInfoBuilder API. Currently
|
|
|
+ // we support four:
|
|
|
+ // * Inheritable handle list;
|
|
|
+ // * Pseudoconsole;
|
|
|
+ // * Mitigation policy;
|
|
|
+ // * Job list
|
|
|
+ sib.attrs = make(map[uintptr]any, 4)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (sib *StartupInfoBuilder) getAttr(attr uintptr) any {
|
|
|
+ sib.makeAttrs()
|
|
|
+ return sib.attrs[attr]
|
|
|
+}
|
|
|
+
|
|
|
+// InheritHandles configures each handle in handles to be inheritable and adds
|
|
|
+// it to the inheritable handle list proc/thread attribute. handles must consist
|
|
|
+// entirely of kernel objects (handles that are closed via windows.CloseHandle).
|
|
|
+// InheritHandles may be called multiple times; each successive call accumulates
|
|
|
+// handles into an internal list maintained by sib.
|
|
|
+func (sib *StartupInfoBuilder) InheritHandles(handles ...windows.Handle) error {
|
|
|
+ if len(handles) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ newHandles := make([]windows.Handle, 0, len(handles))
|
|
|
+ for _, h := range handles {
|
|
|
+ if h == 0 || h == windows.InvalidHandle || slices.Contains(newHandles, h) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := windows.SetHandleInformation(h, windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ newHandles = append(newHandles, h)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(newHandles) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ var handleList []windows.Handle
|
|
|
+ if attrv := sib.getAttr(windows.PROC_THREAD_ATTRIBUTE_HANDLE_LIST); attrv != nil {
|
|
|
+ handleList = attrv.([]windows.Handle)
|
|
|
+ }
|
|
|
+
|
|
|
+ sib.attrs[windows.PROC_THREAD_ATTRIBUTE_HANDLE_LIST] = append(handleList, newHandles...)
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// AddMitigationPolicyFlags sets the process mitigation policy flags in newFlags
|
|
|
+// on the mitigation policy proc/thread attribute. It accepts a different
|
|
|
+// number of arguments depending on the current Windows version. If the
|
|
|
+// current Windows version is Windows 10 build 1703 or newer, it accepts up to
|
|
|
+// two arguments. It only accepts one argument on older versions of Windows 10.
|
|
|
+// If too many arguments are supplied, AddMitigationPolicyFlags returns
|
|
|
+// ErrTooManyMitigationPolicyArguments wrapped with additional information;
|
|
|
+// use errors.Is to check for this error.
|
|
|
+// AddMitigationPolicyFlags may be called multiple times; each successive call
|
|
|
+// accumulates additional flags into the mitigation policy.
|
|
|
+func (sib *StartupInfoBuilder) AddMitigationPolicyFlags(newFlags ...uint64) error {
|
|
|
+ if len(newFlags) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ supportedLen := 1
|
|
|
+ if wingoes.IsWin10BuildOrGreater(wingoes.Win10Build1703) {
|
|
|
+ supportedLen++
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(newFlags) > supportedLen {
|
|
|
+ return fmt.Errorf("%w: no more than %d allowed", ErrTooManyMitigationPolicyArguments, supportedLen)
|
|
|
+ }
|
|
|
+
|
|
|
+ attrv := sib.getAttr(windows.PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY)
|
|
|
+ switch v := attrv.(type) {
|
|
|
+ case nil:
|
|
|
+ sib.attrs[windows.PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY] = newFlags
|
|
|
+ case []uint64:
|
|
|
+ if newElems := len(newFlags) - len(v); newElems > 0 {
|
|
|
+ v = append(v, make([]uint64, newElems)...)
|
|
|
+ sib.attrs[windows.PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY] = v
|
|
|
+ }
|
|
|
+ for i := range v {
|
|
|
+ v[i] |= newFlags[i]
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ panic("unexpected attribute type")
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// SetPseudoConsole sets pty as the pseudoconsole proc/thread attribute.
|
|
|
+// pty must be a conpty handle. It returns ErrAlreadySet if the pty has already
|
|
|
+// been successfully set by a previous call.
|
|
|
+func (sib *StartupInfoBuilder) SetPseudoConsole(pty windows.Handle) error {
|
|
|
+ if pty == 0 {
|
|
|
+ return os.ErrInvalid
|
|
|
+ }
|
|
|
+
|
|
|
+ if attrv := sib.getAttr(windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE); attrv != nil {
|
|
|
+ return ErrAlreadySet
|
|
|
+ }
|
|
|
+
|
|
|
+ sib.attrs[windows.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE] = pty
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// AssignToJob assigns the process created by sib to job. AssignToJob may be
|
|
|
+// called multiple times to assign the process to multiple jobs.
|
|
|
+func (sib *StartupInfoBuilder) AssignToJob(job windows.Handle) error {
|
|
|
+ if job == 0 {
|
|
|
+ return os.ErrInvalid
|
|
|
+ }
|
|
|
+
|
|
|
+ var jobList []windows.Handle
|
|
|
+ if attrv := sib.getAttr(_PROC_THREAD_ATTRIBUTE_JOB_LIST); attrv != nil {
|
|
|
+ jobList = attrv.([]windows.Handle)
|
|
|
+ }
|
|
|
+ if slices.Contains(jobList, job) {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ sib.attrs[_PROC_THREAD_ATTRIBUTE_JOB_LIST] = append(jobList, job)
|
|
|
+ return nil
|
|
|
+}
|