Skip to content

Commit

Permalink
Add new exec package for host process containers
Browse files Browse the repository at this point in the history
This change adds a new exec package thats main goal is to run
external processes on Windows. Unfortunately due to a couple
things that can't be accomplished with the stdlib os/exec package,
this new package is meant to replace how processes for host process
containers are launched.

The main shortcomings are not being able to pass in a pseudo console to use
for tty scenarios, and not being able to start a process assigned to a job object
instead of doing the Create -> Assign dance. Both of these issue are centered
around not having access to the process thread attribute list that is setup inside
of syscall.StartProcess. This is needed to be able to properly setup both cases,
as it requires calling UpdateProcThreadAttribute and passing in what's necessary
for both scenarios.

Signed-off-by: Daniel Canter <dcanter@microsoft.com>
  • Loading branch information
dcantah committed Nov 22, 2021
1 parent 0f39fc7 commit dbe2d29
Show file tree
Hide file tree
Showing 8 changed files with 978 additions and 20 deletions.
487 changes: 487 additions & 0 deletions internal/exec/exec.go

Large diffs are not rendered by default.

161 changes: 161 additions & 0 deletions internal/exec/exec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package exec

import (
"context"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"testing"

"github.com/Microsoft/hcsshim/internal/jobobject"
)

func TestExec(t *testing.T) {
// Exec a simple process and wait for exit.
e, err := New(
`C:\Windows\System32\ping.exe`,
"ping 127.0.0.1",
WithEnv(os.Environ()),
)
if err != nil {
t.Fatal(err)
}

err = e.Start()
if err != nil {
t.Fatalf("failed to start process: %v", err)
}

err = e.Wait()
if err != nil {
t.Fatalf("error waiting for process: %v", err)
}
t.Logf("exit code was: %d", e.ExitCode())
}

func TestExecWithDir(t *testing.T) {
// Test that the working directory is successfully set to whatever was passed in.
dir, err := ioutil.TempDir("", "exec-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)

e, err := New(
`C:\Windows\System32\cmd.exe`,
"cmd /c echo 'test' > test.txt",
WithDir(dir),
WithEnv(os.Environ()),
)
if err != nil {
t.Fatal(err)
}

err = e.Start()
if err != nil {
t.Fatalf("failed to start process: %v", err)
}

err = e.Wait()
if err != nil {
t.Fatalf("error waiting for process: %v", err)
}

if _, err := os.Stat(filepath.Join(dir, "test.txt")); err != nil {
t.Fatal(err)
}

t.Logf("exit code was: %d", e.ExitCode())
}

func TestExecStdinPowershell(t *testing.T) {
// Exec a powershel instance and test that we can write commands to stdin and receive the output from stdout.
e, err := New(
`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`,
"powershell",
WithStdio(true, false, true),
WithEnv(os.Environ()),
)
if err != nil {
t.Fatal(err)
}

err = e.Start()
if err != nil {
t.Fatalf("failed to start process: %v", err)
}

stdinChan := make(chan error)
go func() {
_, _ = io.Copy(os.Stdout, e.Stdout())
}()

go func() {
cmd := `ping 127.0.0.1
`

exit := `exit
`
if _, err := e.Stdin().Write([]byte(cmd)); err != nil {
stdinChan <- err
}
if _, err := e.Stdin().Write([]byte(exit)); err != nil {
stdinChan <- err
}
close(stdinChan)
}()

err = <-stdinChan
if err != nil {
t.Fatal(err)
}

err = e.Wait()
if err != nil {
t.Fatalf("error waiting for process: %v", err)
}
t.Logf("exit code was: %d", e.ExitCode())
}

func TestExecWithJob(t *testing.T) {
// Test that we can assign a process to a job object at creation time.
job, err := jobobject.Create(context.Background(), &jobobject.Options{Name: "test"})
if err != nil {
log.Fatal(err)
}
defer job.Close()

e, err := New(
`C:\Windows\System32\ping.exe`,
"ping 127.0.0.1",
WithJobObject(job),
WithStdio(true, false, false),
WithEnv(os.Environ()),
)
if err != nil {
t.Fatal(err)
}

err = e.Start()
if err != nil {
t.Fatalf("failed to start process: %v", err)
}

pids, err := job.Pids()
if err != nil {
t.Fatal(err)
}

// Should only be one process in the job
if pids[0] != uint32(e.Pid()) {
t.Fatal(err)
}

err = e.Wait()
if err != nil {
t.Fatalf("error waiting for process: %v", err)
}
t.Logf("exit code was: %d", e.ExitCode())
}
69 changes: 69 additions & 0 deletions internal/exec/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package exec

import (
"github.com/Microsoft/hcsshim/internal/jobobject"
"golang.org/x/sys/windows"
)

type ExecOpts func(e *execConfig) error

type execConfig struct {
dir string
env []string
stdout, stderr, stdin bool

job *jobobject.JobObject
token windows.Token
processFlags uint32
}

// WithDir will use `dir` as the working directory for the process.
func WithDir(dir string) ExecOpts {
return func(e *execConfig) error {
e.dir = dir
return nil
}
}

// WithStdio will hook up stdio for the process to a pipe, the other end of which can be retrieved by calling Stdout(), stdErr(), or Stdin()
// respectively on the Exec object. Stdio will be hooked up to the NUL device otherwise.
func WithStdio(stdout, stderr, stdin bool) ExecOpts {
return func(e *execConfig) error {
e.stdout = stdout
e.stderr = stderr
e.stdin = stdin
return nil
}
}

// WithEnv will use the passed in environment variables for the new process.
func WithEnv(env []string) ExecOpts {
return func(e *execConfig) error {
e.env = env
return nil
}
}

// WithJobObject will launch the newly created process in the passed in job.
func WithJobObject(job *jobobject.JobObject) ExecOpts {
return func(e *execConfig) error {
e.job = job
return nil
}
}

// WithToken will run the process as the user that `token` represents.
func WithToken(token windows.Token) ExecOpts {
return func(e *execConfig) error {
e.token = token
return nil
}
}

// WithProcessFlags will pass `flags` to CreateProcess's creationFlags parameter.
func WithProcessFlags(flags uint32) ExecOpts {
return func(e *execConfig) error {
e.processFlags = flags
return nil
}
}
27 changes: 27 additions & 0 deletions internal/jobobject/jobobject.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jobobject

import (
"context"
"fmt"
"sync"
"unsafe"

Expand Down Expand Up @@ -237,6 +238,32 @@ func (job *JobObject) PollNotification() (interface{}, error) {
return job.mq.ReadOrWait()
}

// UpdateProcThreadAttribute updates the passed in ProcThreadAttributeList to contain what is necessary to
// launch a process in a job at creation time. This can be used to avoid having to call Assign() after a process
// has already started running.
func (job *JobObject) UpdateProcThreadAttribute(attrList *winapi.ProcThreadAttributeList) error {
job.handleLock.RLock()
defer job.handleLock.RUnlock()

if job.handle == 0 {
return ErrAlreadyClosed
}

err := winapi.UpdateProcThreadAttribute(
attrList,
0,
winapi.PROC_THREAD_ATTRIBUTE_JOB_LIST,
unsafe.Pointer(&job.handle),
unsafe.Sizeof(job.handle),
nil,
nil,
)
if err != nil {
return fmt.Errorf("failed to update proc thread attributes for job object: %w", err)
}
return nil
}

// Close closes the job object handle.
func (job *JobObject) Close() error {
job.handleLock.Lock()
Expand Down
80 changes: 75 additions & 5 deletions internal/winapi/process.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,80 @@
package winapi

import (
"errors"
"unsafe"

"golang.org/x/sys/windows"
)

const PROCESS_ALL_ACCESS uint32 = 2097151

// DWORD GetProcessImageFileNameW(
// HANDLE hProcess,
// LPWSTR lpImageFileName,
// DWORD nSize
const (
PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x20016
PROC_THREAD_ATTRIBUTE_JOB_LIST = 0x2000D
)

type ProcThreadAttributeList struct {
_ [1]byte
}

// typedef struct _STARTUPINFOEXW {
// STARTUPINFOW StartupInfo;
// LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
// } STARTUPINFOEXW, *LPSTARTUPINFOEXW;
type StartupInfoEx struct {
// This is a recreation of the same binding from the stdlib. The x/sys/windows variant for whatever reason
// doesn't work when updating the list for the pseudo console attribute. It has the process immediately exit
// with exit code 0xc0000142 shortly after start.
windows.StartupInfo
ProcThreadAttributeList *ProcThreadAttributeList
}

// NewProcThreadAttributeList allocates a new ProcThreadAttributeList, with
// the requested maximum number of attributes. This must be cleaned up by calling
// DeleteProcThreadAttributeList.
func NewProcThreadAttributeList(maxAttrCount uint32) (*ProcThreadAttributeList, error) {
var size uintptr
err := InitializeProcThreadAttributeList(nil, maxAttrCount, 0, &size)
if err != windows.ERROR_INSUFFICIENT_BUFFER {
if err == nil {
return nil, errors.New("unable to query buffer size from InitializeProcThreadAttributeList")
}
return nil, err
}
al := (*ProcThreadAttributeList)(unsafe.Pointer(&make([]byte, size)[0]))
err = InitializeProcThreadAttributeList(al, maxAttrCount, 0, &size)
if err != nil {
return nil, err
}
return al, nil
}

// BOOL InitializeProcThreadAttributeList(
// [out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
// [in] DWORD dwAttributeCount,
// DWORD dwFlags,
// [in, out] PSIZE_T lpSize
// );
//sys GetProcessImageFileName(hProcess windows.Handle, imageFileName *uint16, nSize uint32) (size uint32, err error) = kernel32.GetProcessImageFileNameW
//
//sys InitializeProcThreadAttributeList(lpAttributeList *ProcThreadAttributeList, dwAttributeCount uint32, dwFlags uint32, lpSize *uintptr) (err error) = kernel32.InitializeProcThreadAttributeList

// void DeleteProcThreadAttributeList(
// [in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList
// );
//
//sys DeleteProcThreadAttributeList(lpAttributeList *ProcThreadAttributeList) = kernel32.DeleteProcThreadAttributeList

// BOOL UpdateProcThreadAttribute(
// [in, out] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
// [in] DWORD dwFlags,
// [in] DWORD_PTR Attribute,
// [in] PVOID lpValue,
// [in] SIZE_T cbSize,
// [out, optional] PVOID lpPreviousValue,
// [in, optional] PSIZE_T lpReturnSize
// );
//
//sys UpdateProcThreadAttribute(lpAttributeList *ProcThreadAttributeList, dwFlags uint32, attribute uintptr, lpValue unsafe.Pointer, cbSize uintptr, lpPreviousValue unsafe.Pointer, lpReturnSize *uintptr) (err error) = kernel32.UpdateProcThreadAttribute

//sys CreateProcessAsUser(token windows.Token, appName *uint16, commandLine *uint16, procSecurity *windows.SecurityAttributes, threadSecurity *windows.SecurityAttributes, inheritHandles bool, creationFlags uint32, env *uint16, currentDir *uint16, startupInfo *windows.StartupInfo, outProcInfo *windows.ProcessInformation) (err error) = advapi32.CreateProcessAsUserW
Loading

0 comments on commit dbe2d29

Please sign in to comment.