diff --git a/util/winutil/conpty/conpty_windows.go b/util/winutil/conpty/conpty_windows.go new file mode 100644 index 000000000..0a35759b4 --- /dev/null +++ b/util/winutil/conpty/conpty_windows.go @@ -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 +} diff --git a/util/winutil/startupinfo_windows.go b/util/winutil/startupinfo_windows.go new file mode 100644 index 000000000..f2234fdbe --- /dev/null +++ b/util/winutil/startupinfo_windows.go @@ -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 +}