StartupInfoBuilder is a helper for constructing StartupInfoEx structures featuring proc/thread attribute lists. Calling its setters triggers the appropriate setting of fields, adjusting flags as necessary, and populating the proc/thread attribute list as necessary. Currently it supports four features: setting std handles, setting pseudo-consoles, specifying handles for inheritance, and specifying jobs. The conpty package simplifies creation of pseudo-consoles, their associated pipes, and assignment of the pty to StartupInfoEx proc/thread attributes. Updates #12383 Signed-off-by: Aaron Klotz <aaron@tailscale.com>main
parent
8a11a43c28
commit
34e8820301
@ -0,0 +1,134 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package conpty implements support for Windows pseudo-consoles.
|
||||
package conpty |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"os" |
||||
|
||||
"github.com/dblohm7/wingoes" |
||||
"golang.org/x/sys/windows" |
||||
"tailscale.com/util/winutil" |
||||
) |
||||
|
||||
var ( |
||||
// ErrUnsupported is returned by NewPseudoConsole if the current Windows
|
||||
// build does not support this package's API.
|
||||
ErrUnsupported = errors.New("conpty unsupported on this version of Windows") |
||||
) |
||||
|
||||
// PseudoConsole encapsulates a Windows pseudo-console. Use NewPseudoConsole
|
||||
// to create a new instance.
|
||||
type PseudoConsole struct { |
||||
outputRead io.ReadCloser |
||||
inputWrite io.WriteCloser |
||||
console windows.Handle |
||||
} |
||||
|
||||
// NewPseudoConsole creates a new PseudoConsole using size for its initial
|
||||
// width and height. It requires Windows 10 1809 or newer, and will return
|
||||
// ErrUnsupported if that requirement is not met.
|
||||
func NewPseudoConsole(size windows.Coord) (pty *PseudoConsole, err error) { |
||||
if !wingoes.IsWin10BuildOrGreater(wingoes.Win10Build1809) { |
||||
return nil, ErrUnsupported |
||||
} |
||||
if size.X <= 0 || size.Y <= 0 { |
||||
return nil, fmt.Errorf("%w: size must contain positive values", os.ErrInvalid) |
||||
} |
||||
|
||||
var inputRead, inputWrite windows.Handle |
||||
if err := windows.CreatePipe(&inputRead, &inputWrite, nil, 0); err != nil { |
||||
return nil, err |
||||
} |
||||
defer func() { |
||||
windows.CloseHandle(inputRead) |
||||
if err != nil { |
||||
windows.CloseHandle(inputWrite) |
||||
} |
||||
}() |
||||
|
||||
var outputRead, outputWrite windows.Handle |
||||
if err := windows.CreatePipe(&outputRead, &outputWrite, nil, 0); err != nil { |
||||
return nil, err |
||||
} |
||||
defer func() { |
||||
windows.CloseHandle(outputWrite) |
||||
if err != nil { |
||||
windows.CloseHandle(outputRead) |
||||
} |
||||
}() |
||||
|
||||
var console windows.Handle |
||||
if err := windows.CreatePseudoConsole(size, inputRead, outputWrite, 0, &console); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
pty = &PseudoConsole{ |
||||
outputRead: os.NewFile(uintptr(outputRead), "ptyOutputRead"), |
||||
inputWrite: os.NewFile(uintptr(inputWrite), "ptyInputWrite"), |
||||
console: console, |
||||
} |
||||
return pty, nil |
||||
} |
||||
|
||||
// Resize sets the width and height of pty to size.
|
||||
func (pty *PseudoConsole) Resize(size windows.Coord) error { |
||||
if pty.console == 0 { |
||||
return fmt.Errorf("PseudoConsole is closed") |
||||
} |
||||
if size.X <= 0 || size.Y <= 0 { |
||||
return fmt.Errorf("%w: size must contain positive values", os.ErrInvalid) |
||||
} |
||||
|
||||
return windows.ResizePseudoConsole(pty.console, size) |
||||
} |
||||
|
||||
// Close shuts down the pty. The caller must continue reading from the
|
||||
// ReadCloser returned by Output until either EOF is reached or Close returns;
|
||||
// failure to adequately drain the ReadCloser may result in Close deadlocking.
|
||||
func (pty *PseudoConsole) Close() error { |
||||
if pty.console != 0 { |
||||
windows.ClosePseudoConsole(pty.console) |
||||
pty.console = 0 |
||||
} |
||||
|
||||
// now we can stop these
|
||||
if pty.outputRead != nil { |
||||
pty.outputRead.Close() |
||||
pty.outputRead = nil |
||||
} |
||||
if pty.inputWrite != nil { |
||||
pty.inputWrite.Close() |
||||
pty.inputWrite = nil |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// ConfigureStartupInfo associates pty with the process to be started using sib.
|
||||
func (pty *PseudoConsole) ConfigureStartupInfo(sib *winutil.StartupInfoBuilder) error { |
||||
if sib == nil { |
||||
return os.ErrInvalid |
||||
} |
||||
// We need to explicitly set null std handles.
|
||||
// Failure to do so causes interference between the pty and the console
|
||||
// handles that are implicitly inherited from the parent.
|
||||
// This isn't explicitly documented anywhere. Windows Terminal does this too.
|
||||
if err := sib.SetStdHandles(0, 0, 0); err != nil { |
||||
return err |
||||
} |
||||
return sib.SetPseudoConsole(pty.console) |
||||
} |
||||
|
||||
// OutputPipe returns the ReadCloser for reading pty's output.
|
||||
func (pty *PseudoConsole) OutputPipe() io.ReadCloser { |
||||
return pty.outputRead |
||||
} |
||||
|
||||
// InputPipe returns the WriteCloser for writing pty's output.
|
||||
func (pty *PseudoConsole) InputPipe() io.WriteCloser { |
||||
return pty.inputWrite |
||||
} |
||||
@ -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 |
||||
} |
||||
Loading…
Reference in new issue