Files
rvbox/internal/client/supervisor/windows/launcher_windows.go
T

851 lines
30 KiB
Go

//go:build windows
package windows
import (
"context"
cryptorand "crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"github.com/rvbox/rvbox/internal/client/supervisor"
winapi "golang.org/x/sys/windows"
)
const (
launcherHandshakeTimeout = 30 * time.Second
launcherPipeBuffer = 64 << 10
launcherPipePrefix = `\\.\pipe\rvbox-launch-`
)
var (
procGetNamedPipeClientProcessID = winapi.NewLazySystemDLL("kernel32.dll").NewProc("GetNamedPipeClientProcessId")
procGetNamedPipeClientSessionID = winapi.NewLazySystemDLL("kernel32.dll").NewProc("GetNamedPipeClientSessionId")
procIsProcessInJob = winapi.NewLazySystemDLL("kernel32.dll").NewProc("IsProcessInJob")
)
type launcherPipe struct {
name string
file *os.File
}
type launcherPipeSet struct {
channel string
generation [16]byte
control launcherPipe
stdin launcherPipe
stdout launcherPipe
stderr launcherPipe
}
func (pipes *launcherPipeSet) closeAll() {
for _, pipe := range []*launcherPipe{&pipes.control, &pipes.stdin, &pipes.stdout, &pipes.stderr} {
if pipe.file != nil {
_ = pipe.file.Close()
pipe.file = nil
}
}
}
func newLauncherPipes(effectiveSID string) (*launcherPipeSet, error) {
if effectiveSID == "" {
return nil, errors.New("launcher pipe ACL requires an effective SID")
}
if _, err := winapi.StringToSid(effectiveSID); err != nil {
return nil, fmt.Errorf("invalid effective SID for launcher pipe ACL: %w", err)
}
var generation [16]byte
if _, err := io.ReadFull(cryptorand.Reader, generation[:]); err != nil {
return nil, fmt.Errorf("generate launcher channel: %w", err)
}
channel := hex.EncodeToString(generation[:])
securityDescriptor, err := winapi.SecurityDescriptorFromString(fmt.Sprintf("O:SYD:(A;;GA;;;SY)(A;;GA;;;%s)", effectiveSID))
if err != nil {
return nil, fmt.Errorf("build launcher pipe ACL: %w", err)
}
security := &winapi.SecurityAttributes{Length: uint32(unsafe.Sizeof(winapi.SecurityAttributes{})), SecurityDescriptor: securityDescriptor}
newPipe := func(suffix string, access uint32) (launcherPipe, error) {
name := launcherPipePrefix + channel + "-" + suffix
namePtr, err := winapi.UTF16PtrFromString(name)
if err != nil {
return launcherPipe{}, err
}
mode := uint32(winapi.PIPE_TYPE_BYTE | winapi.PIPE_READMODE_BYTE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS)
handle, err := winapi.CreateNamedPipe(namePtr, access, mode, 1, launcherPipeBuffer, launcherPipeBuffer, 0, security)
if err != nil {
return launcherPipe{}, fmt.Errorf("create launcher pipe %s: %w", suffix, err)
}
return launcherPipe{name: name, file: os.NewFile(uintptr(handle), "rvbox-launch-"+suffix)}, nil
}
pipes := &launcherPipeSet{channel: channel, generation: generation}
if pipes.control, err = newPipe("control", winapi.PIPE_ACCESS_DUPLEX); err != nil {
return nil, err
}
if pipes.stdin, err = newPipe("stdin", winapi.PIPE_ACCESS_OUTBOUND); err != nil {
pipes.closeAll()
return nil, err
}
if pipes.stdout, err = newPipe("stdout", winapi.PIPE_ACCESS_INBOUND); err != nil {
pipes.closeAll()
return nil, err
}
if pipes.stderr, err = newPipe("stderr", winapi.PIPE_ACCESS_INBOUND); err != nil {
pipes.closeAll()
return nil, err
}
return pipes, nil
}
type launcherPeer struct {
PID uint32
Creation uint64
SessionID uint32
UserSID string
}
func processCreation(handle winapi.Handle) (uint64, error) {
var creation, exit, kernel, user winapi.Filetime
if err := winapi.GetProcessTimes(handle, &creation, &exit, &kernel, &user); err != nil {
return 0, err
}
return uint64(creation.HighDateTime)<<32 | uint64(creation.LowDateTime), nil
}
func verifyLauncherPipePeer(handle winapi.Handle, expected launcherPeer) error {
var pid uint32
if result, _, callErr := procGetNamedPipeClientProcessID.Call(uintptr(handle), uintptr(unsafe.Pointer(&pid))); result == 0 {
if callErr == syscall.Errno(0) {
callErr = syscall.GetLastError()
}
return fmt.Errorf("query launcher pipe client PID: %w", callErr)
}
if pid != expected.PID {
return fmt.Errorf("launcher pipe client PID %d does not match %d", pid, expected.PID)
}
var sessionID uint32
if result, _, callErr := procGetNamedPipeClientSessionID.Call(uintptr(handle), uintptr(unsafe.Pointer(&sessionID))); result == 0 {
if callErr == syscall.Errno(0) {
callErr = syscall.GetLastError()
}
return fmt.Errorf("query launcher pipe client session: %w", callErr)
}
if sessionID != expected.SessionID {
return fmt.Errorf("launcher pipe client session %d does not match %d", sessionID, expected.SessionID)
}
process, err := winapi.OpenProcess(winapi.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return fmt.Errorf("open launcher pipe client process: %w", err)
}
defer winapi.CloseHandle(process)
creation, err := processCreation(process)
if err != nil {
return fmt.Errorf("query launcher process creation time: %w", err)
}
if creation != expected.Creation {
return errors.New("launcher pipe client creation time does not match")
}
var token winapi.Token
if err := winapi.OpenProcessToken(process, winapi.TOKEN_QUERY, &token); err != nil {
return fmt.Errorf("open launcher client token: %w", err)
}
defer token.Close()
user, err := token.GetTokenUser()
if err != nil || user.User.Sid == nil {
if err != nil {
return fmt.Errorf("query launcher client SID: %w", err)
}
return errors.New("launcher client token has no SID")
}
if user.User.Sid.String() != expected.UserSID {
return fmt.Errorf("launcher client SID %q does not match %q", user.User.Sid.String(), expected.UserSID)
}
return nil
}
func connectLauncherPipe(ctx context.Context, pipe launcherPipe, expected launcherPeer) error {
handle := winapi.Handle(pipe.file.Fd())
connected := make(chan error, 1)
go func() {
err := winapi.ConnectNamedPipe(handle, nil)
if err != nil && !errors.Is(err, winapi.ERROR_PIPE_CONNECTED) {
connected <- err
return
}
connected <- verifyLauncherPipePeer(handle, expected)
}()
select {
case <-ctx.Done():
return ctx.Err()
case err := <-connected:
if err != nil {
return fmt.Errorf("connect launcher pipe %s: %w", pipe.name, err)
}
return nil
}
}
func acceptLauncherPipes(ctx context.Context, pipes *launcherPipeSet, expected launcherPeer) error {
results := make(chan error, 4)
for _, pipe := range []launcherPipe{pipes.control, pipes.stdin, pipes.stdout, pipes.stderr} {
go func(pipe launcherPipe) { results <- connectLauncherPipe(ctx, pipe, expected) }(pipe)
}
for range 4 {
if err := <-results; err != nil {
pipes.closeAll()
return err
}
}
return nil
}
func launcherGeneration(channel string) ([16]byte, error) {
var generation [16]byte
if len(channel) != hex.EncodedLen(len(generation)) {
return generation, errors.New("invalid launcher channel length")
}
if _, err := hex.Decode(generation[:], []byte(channel)); err != nil {
return generation, errors.New("invalid launcher channel encoding")
}
return generation, nil
}
func openLauncherPipe(channel, suffix string, access uint32) (*os.File, error) {
generation, err := launcherGeneration(channel)
if err != nil {
return nil, err
}
_ = generation
name, err := winapi.UTF16PtrFromString(launcherPipePrefix + strings.ToLower(channel) + "-" + suffix)
if err != nil {
return nil, err
}
deadline := time.Now().Add(launcherHandshakeTimeout)
for {
handle, openErr := winapi.CreateFile(name, access, 0, nil, winapi.OPEN_EXISTING, 0, 0)
if openErr == nil {
return os.NewFile(uintptr(handle), "rvbox-launch-"+suffix), nil
}
if time.Now().After(deadline) {
return nil, fmt.Errorf("open launcher pipe %s: %w", suffix, openErr)
}
time.Sleep(10 * time.Millisecond)
}
}
func (manager *execSupervisor) startViaLauncher(ctx context.Context, spec supervisor.StartSpec, token winapi.Token, identity supervisor.EffectiveIdentity, launch LaunchPlan, cleanup func()) (supervisor.Process, error) {
pipes, err := newLauncherPipes(identity.UserSID)
if err != nil {
if cleanup != nil {
cleanup()
}
return nil, err
}
job, err := createKillOnCloseJob()
if err != nil {
pipes.closeAll()
if cleanup != nil {
cleanup()
}
return nil, fmt.Errorf("create command Job: %w", err)
}
if err := applyJobProfiles(job, manager.options.JobProfiles, spec.ExecutionProfiles); err != nil {
_ = winapi.CloseHandle(job)
pipes.closeAll()
if cleanup != nil {
cleanup()
}
return nil, err
}
closeOnFailure := true
defer func() {
if closeOnFailure {
_ = winapi.TerminateJobObject(job, 1)
_ = winapi.CloseHandle(job)
pipes.closeAll()
if cleanup != nil {
cleanup()
}
}
}()
executable := manager.options.ExecutablePath
if executable == "" {
executable, err = os.Executable()
if err != nil {
return nil, fmt.Errorf("resolve launcher executable: %w", err)
}
}
if err := verifyExecutable(executable); err != nil {
return nil, fmt.Errorf("verify launcher executable: %w", err)
}
launcherApplication, err := winapi.UTF16PtrFromString(executable)
if err != nil {
return nil, err
}
launcherCommand, err := BuildCommandLine([]string{executable, "--launcher", "--channel", pipes.channel})
if err != nil {
return nil, err
}
launcherCommandUTF16, err := winapi.UTF16FromString(launcherCommand)
if err != nil {
return nil, err
}
startup := winapi.StartupInfoEx{StartupInfo: winapi.StartupInfo{Cb: uint32(unsafe.Sizeof(winapi.StartupInfoEx{}))}}
var launcherInfo winapi.ProcessInformation
flags := uint32(winapi.CREATE_SUSPENDED | winapi.CREATE_UNICODE_ENVIRONMENT | winapi.EXTENDED_STARTUPINFO_PRESENT | winapi.CREATE_NO_WINDOW)
if err := winapi.CreateProcessAsUser(token, launcherApplication, &launcherCommandUTF16[0], nil, nil, false, flags, nil, nil, &startup.StartupInfo, &launcherInfo); err != nil {
return nil, fmt.Errorf("create suspended launcher: %w", err)
}
launcherCreated := true
defer func() {
if launcherCreated {
_ = winapi.TerminateProcess(launcherInfo.Process, 1)
_ = winapi.CloseHandle(launcherInfo.Process)
_ = winapi.CloseHandle(launcherInfo.Thread)
}
}()
if err := winapi.AssignProcessToJobObject(job, launcherInfo.Process); err != nil {
return nil, fmt.Errorf("assign launcher to Job: %w", err)
}
launcherBirth, err := processCreation(launcherInfo.Process)
if err != nil {
return nil, fmt.Errorf("query launcher creation time: %w", err)
}
if _, err := winapi.ResumeThread(launcherInfo.Thread); err != nil {
return nil, fmt.Errorf("resume launcher: %w", err)
}
_ = winapi.CloseHandle(launcherInfo.Thread)
expected := launcherPeer{PID: launcherInfo.ProcessId, Creation: launcherBirth, SessionID: identity.SessionID, UserSID: identity.UserSID}
handshakeCtx, cancel := context.WithTimeout(ctx, launcherHandshakeTimeout)
defer cancel()
if err := acceptLauncherPipes(handshakeCtx, pipes, expected); err != nil {
return nil, err
}
frame, err := readLauncherFrameContext(handshakeCtx, pipes.control.file, pipes.generation)
if err != nil {
return nil, fmt.Errorf("read launcher hello: %w", err)
}
if frame.Kind != launcherFrameHello {
return nil, errors.New("launcher did not send hello first")
}
var hello launcherHello
if err := unmarshalLauncherPayload(frame.Payload, &hello); err != nil {
return nil, fmt.Errorf("decode launcher hello: %w", err)
}
if hello.PID != expected.PID || hello.Creation != expected.Creation || hello.SessionID != expected.SessionID || hello.Effective != expected.UserSID {
return nil, errors.New("launcher hello identity mismatch")
}
requestPayload, err := marshalLauncherPayload(launcherRequest{ApplicationName: launch.ApplicationName, CommandLine: launch.CommandLine, WorkingDirectory: launch.WorkingDirectory, Environment: launch.Environment})
if err != nil {
return nil, err
}
if err := writeLauncherFrame(pipes.control.file, launcherFrame{Kind: launcherFrameLaunch, Generation: pipes.generation, Payload: requestPayload}); err != nil {
return nil, fmt.Errorf("send launcher request: %w", err)
}
frame, err = readLauncherFrameContext(handshakeCtx, pipes.control.file, pipes.generation)
if err != nil {
return nil, fmt.Errorf("read shell preparation: %w", err)
}
if frame.Kind != launcherFrameShellPrepared {
return nil, errors.New("launcher did not prepare a shell")
}
var prepared launcherShellPrepared
if err := unmarshalLauncherPayload(frame.Payload, &prepared); err != nil {
return nil, fmt.Errorf("decode shell preparation: %w", err)
}
if prepared.PID == 0 || prepared.Creation == 0 {
return nil, errors.New("launcher returned incomplete shell identity")
}
shellHandle, err := winapi.OpenProcess(winapi.PROCESS_QUERY_LIMITED_INFORMATION, false, prepared.PID)
if err != nil {
return nil, fmt.Errorf("open prepared shell: %w", err)
}
actualShellBirth, birthErr := processCreation(shellHandle)
if birthErr != nil || actualShellBirth != prepared.Creation {
_ = winapi.CloseHandle(shellHandle)
if birthErr != nil {
return nil, fmt.Errorf("verify prepared shell creation time: %w", birthErr)
}
return nil, errors.New("prepared shell creation time changed")
}
var inJob bool
if result, _, callErr := procIsProcessInJob.Call(uintptr(shellHandle), uintptr(job), uintptr(unsafe.Pointer(&inJob))); result == 0 {
_ = winapi.CloseHandle(shellHandle)
if callErr == syscall.Errno(0) {
callErr = syscall.GetLastError()
}
return nil, fmt.Errorf("verify prepared shell Job membership: %w", callErr)
}
_ = winapi.CloseHandle(shellHandle)
if !inJob {
return nil, errors.New("prepared shell is not in the command Job")
}
started := manager.options.Now()
var resourceClose sync.Once
closeResources := func() {
resourceClose.Do(func() {
_ = pipes.control.file.Close()
_ = pipes.stdin.file.Close()
_ = winapi.CloseHandle(launcherInfo.Process)
_ = winapi.CloseHandle(job)
})
}
var signalToken winapi.Token
if err := winapi.DuplicateTokenEx(token, winapi.TOKEN_ALL_ACCESS, nil, winapi.SecurityImpersonation, winapi.TokenPrimary, &signalToken); err != nil {
// A signal helper is an optional control path. The command itself is
// already fully prepared; Signal falls back to the verified direct
// console attach path if Windows refuses this duplicate.
signalToken = 0
}
var signalTokenClose sync.Once
releaseFn := func() error {
if err := writeLauncherFrame(pipes.control.file, launcherFrame{Kind: launcherFrameRelease, Generation: pipes.generation}); err != nil {
return fmt.Errorf("send launcher release: %w", err)
}
ack, err := readLauncherFrame(pipes.control.file, pipes.generation)
if err != nil {
return fmt.Errorf("read launcher release acknowledgement: %w", err)
}
if ack.Kind != launcherFrameReleased {
return errors.New("launcher did not acknowledge release")
}
return nil
}
waitFn := func() (int32, bool, error) {
_, waitErr := winapi.WaitForSingleObject(launcherInfo.Process, winapi.INFINITE)
var code uint32
if err := winapi.GetExitCodeProcess(launcherInfo.Process, &code); err != nil && waitErr == nil {
waitErr = err
}
signalTokenClose.Do(func() {
if signalToken != 0 {
_ = signalToken.Close()
}
})
closeResources()
return int32(code), false, waitErr
}
killFn := func(code uint32) error {
err := winapi.TerminateJobObject(job, code)
return err
}
command := &exec.Cmd{Process: osProcess(prepared.PID)}
process := manager.registerProcess(spec.IssueUUID, identity, command, pipes.stdin.file, pipes.stdout.file, pipes.stderr.file, started, waitFn, killFn, releaseFn, cleanup)
process.creation = prepared.Creation
if signalToken != 0 {
process.signalFn = func(signalContext context.Context) (bool, error) {
return runSignalHelper(signalContext, manager.options.ExecutablePath, signalToken, identity, prepared.PID, prepared.Creation)
}
}
process.snapshotFn = func() (supervisor.ResourceSnapshot, error) {
return queryJobSnapshot(job, manager.options.Now())
}
closeOnFailure = false
launcherCreated = false
return process, nil
}
// RunLauncher is the only entry point for --launcher. It opens the four
// private pipes, reports its immutable identity, creates the requested shell
// suspended, and waits for the service's durable release frame before running
// any command code.
func RunLauncher(_ context.Context, channel string) error {
generation, err := launcherGeneration(channel)
if err != nil {
return err
}
control, err := openLauncherPipe(channel, "control", winapi.GENERIC_READ|winapi.GENERIC_WRITE)
if err != nil {
return err
}
defer control.Close()
stdin, err := openLauncherPipe(channel, "stdin", winapi.GENERIC_READ)
if err != nil {
return err
}
defer stdin.Close()
stdout, err := openLauncherPipe(channel, "stdout", winapi.GENERIC_WRITE)
if err != nil {
return err
}
defer stdout.Close()
stderr, err := openLauncherPipe(channel, "stderr", winapi.GENERIC_WRITE)
if err != nil {
return err
}
defer stderr.Close()
var current winapi.Token
err = winapi.OpenProcessToken(winapi.CurrentProcess(), winapi.TOKEN_QUERY, &current)
if err != nil {
return err
}
defer current.Close()
user, err := current.GetTokenUser()
if err != nil || user.User.Sid == nil {
return errors.New("launcher token has no user SID")
}
sessionID, err := tokenInformationUint32(current, winapi.TokenSessionId)
if err != nil {
return err
}
creation, err := processCreation(winapi.CurrentProcess())
if err != nil {
return err
}
helloPayload, err := marshalLauncherPayload(launcherHello{PID: winapi.GetCurrentProcessId(), Creation: creation, SessionID: sessionID, Effective: user.User.Sid.String()})
if err != nil {
return err
}
if err := writeLauncherFrame(control, launcherFrame{Kind: launcherFrameHello, Generation: generation, Payload: helloPayload}); err != nil {
return err
}
frame, err := readLauncherFrame(control, generation)
if err != nil {
return err
}
if frame.Kind != launcherFrameLaunch {
return errors.New("launcher received an unexpected first command")
}
var request launcherRequest
if err := unmarshalLauncherPayload(frame.Payload, &request); err != nil {
return err
}
if err := ValidateWindowsExecutablePath(request.ApplicationName); err != nil || request.CommandLine == "" || !ValidAbsoluteWindowsPath(request.WorkingDirectory) || len(request.Environment) < 2 || request.Environment[len(request.Environment)-1] != 0 || request.Environment[len(request.Environment)-2] != 0 {
return errors.New("launcher request failed validation")
}
if err := verifyExecutable(request.ApplicationName); err != nil {
return err
}
for _, handle := range []winapi.Handle{winapi.Handle(stdin.Fd()), winapi.Handle(stdout.Fd()), winapi.Handle(stderr.Fd())} {
if err := winapi.SetHandleInformation(handle, winapi.HANDLE_FLAG_INHERIT, winapi.HANDLE_FLAG_INHERIT); err != nil {
return err
}
}
application, err := winapi.UTF16PtrFromString(request.ApplicationName)
if err != nil {
return err
}
commandLine, err := winapi.UTF16FromString(request.CommandLine)
if err != nil {
return err
}
workingDirectory, err := winapi.UTF16PtrFromString(request.WorkingDirectory)
if err != nil {
return err
}
attributes, err := winapi.NewProcThreadAttributeList(1)
if err != nil {
return err
}
defer attributes.Delete()
childHandles := []winapi.Handle{winapi.Handle(stdin.Fd()), winapi.Handle(stdout.Fd()), winapi.Handle(stderr.Fd())}
if err := attributes.Update(winapi.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, unsafe.Pointer(&childHandles[0]), uintptr(len(childHandles))*unsafe.Sizeof(childHandles[0])); err != nil {
return err
}
startup := winapi.StartupInfoEx{StartupInfo: winapi.StartupInfo{Cb: uint32(unsafe.Sizeof(winapi.StartupInfoEx{})), Flags: winapi.STARTF_USESTDHANDLES | winapi.STARTF_USESHOWWINDOW, ShowWindow: winapi.SW_HIDE, StdInput: childHandles[0], StdOutput: childHandles[1], StdErr: childHandles[2]}}
startup.ProcThreadAttributeList = attributes.List()
var shellInfo winapi.ProcessInformation
var environment *uint16
if len(request.Environment) > 0 {
environment = &request.Environment[0]
}
flags := uint32(winapi.CREATE_NEW_CONSOLE | winapi.CREATE_SUSPENDED | winapi.CREATE_UNICODE_ENVIRONMENT | winapi.EXTENDED_STARTUPINFO_PRESENT)
if err := winapi.CreateProcess(application, &commandLine[0], nil, nil, true, flags, environment, workingDirectory, &startup.StartupInfo, &shellInfo); err != nil {
return fmt.Errorf("create suspended shell: %w", err)
}
threadClosed := false
closeThread := func() {
if !threadClosed {
threadClosed = true
_ = winapi.CloseHandle(shellInfo.Thread)
}
}
defer func() {
closeThread()
_ = winapi.CloseHandle(shellInfo.Process)
}()
shellCreation, err := processCreation(shellInfo.Process)
if err != nil {
return err
}
preparedPayload, err := marshalLauncherPayload(launcherShellPrepared{PID: shellInfo.ProcessId, Creation: shellCreation})
if err != nil {
return err
}
if err := writeLauncherFrame(control, launcherFrame{Kind: launcherFrameShellPrepared, Generation: generation, Payload: preparedPayload}); err != nil {
return err
}
frame, err = readLauncherFrame(control, generation)
if err != nil {
_ = winapi.TerminateProcess(shellInfo.Process, 1)
return err
}
if frame.Kind != launcherFrameRelease {
_ = winapi.TerminateProcess(shellInfo.Process, 1)
return errors.New("launcher release was not authorized")
}
if _, err := winapi.ResumeThread(shellInfo.Thread); err != nil {
_ = winapi.TerminateProcess(shellInfo.Process, 1)
return err
}
closeThread()
if err := writeLauncherFrame(control, launcherFrame{Kind: launcherFrameReleased, Generation: generation}); err != nil {
_ = winapi.TerminateProcess(shellInfo.Process, 1)
return err
}
_, _ = winapi.WaitForSingleObject(shellInfo.Process, winapi.INFINITE)
var exitCode uint32
if err := winapi.GetExitCodeProcess(shellInfo.Process, &exitCode); err != nil {
return err
}
winapi.ExitProcess(exitCode)
return nil
}
const signalPipePrefix = `\\.\pipe\rvbox-signal-`
func newSignalPipe(effectiveSID string) (string, [16]byte, *os.File, error) {
var generation [16]byte
if effectiveSID == "" {
return "", generation, nil, errors.New("signal pipe ACL requires an effective SID")
}
if _, err := winapi.StringToSid(effectiveSID); err != nil {
return "", generation, nil, err
}
if _, err := io.ReadFull(cryptorand.Reader, generation[:]); err != nil {
return "", generation, nil, err
}
channel := hex.EncodeToString(generation[:])
sd, err := winapi.SecurityDescriptorFromString(fmt.Sprintf("O:SYD:(A;;GA;;;SY)(A;;GA;;;%s)", effectiveSID))
if err != nil {
return "", generation, nil, err
}
security := &winapi.SecurityAttributes{Length: uint32(unsafe.Sizeof(winapi.SecurityAttributes{})), SecurityDescriptor: sd}
name := signalPipePrefix + channel
namePtr, err := winapi.UTF16PtrFromString(name)
if err != nil {
return "", generation, nil, err
}
mode := uint32(winapi.PIPE_TYPE_BYTE | winapi.PIPE_READMODE_BYTE | winapi.PIPE_WAIT | winapi.PIPE_REJECT_REMOTE_CLIENTS)
handle, err := winapi.CreateNamedPipe(namePtr, winapi.PIPE_ACCESS_DUPLEX, mode, 1, launcherPipeBuffer, launcherPipeBuffer, 0, security)
if err != nil {
return "", generation, nil, err
}
return channel, generation, os.NewFile(uintptr(handle), "rvbox-signal"), nil
}
func openSignalPipe(channel string) (*os.File, error) {
if _, err := launcherGeneration(channel); err != nil {
return nil, err
}
name, err := winapi.UTF16PtrFromString(signalPipePrefix + strings.ToLower(channel))
if err != nil {
return nil, err
}
deadline := time.Now().Add(launcherHandshakeTimeout)
for {
handle, openErr := winapi.CreateFile(name, winapi.GENERIC_READ|winapi.GENERIC_WRITE, 0, nil, winapi.OPEN_EXISTING, 0, 0)
if openErr == nil {
return os.NewFile(uintptr(handle), "rvbox-signal"), nil
}
if time.Now().After(deadline) {
return nil, openErr
}
time.Sleep(10 * time.Millisecond)
}
}
func runSignalHelper(ctx context.Context, executable string, token winapi.Token, identity supervisor.EffectiveIdentity, targetPID uint32, targetCreation uint64) (bool, error) {
if executable == "" {
var err error
executable, err = os.Executable()
if err != nil {
return false, err
}
}
if err := verifyExecutable(executable); err != nil {
return false, err
}
serverChannel, serverGeneration, server, err := newSignalPipe(identity.UserSID)
if err != nil {
return false, err
}
defer server.Close()
// Use the generated helper channel, not the command launcher channel. The
// target identity is carried only after this helper's peer is verified.
channel := serverChannel
application, err := winapi.UTF16PtrFromString(executable)
if err != nil {
return false, err
}
command, err := BuildCommandLine([]string{executable, "--signal-helper", "--channel", channel})
if err != nil {
return false, err
}
commandUTF16, err := winapi.UTF16FromString(command)
if err != nil {
return false, err
}
startup := winapi.StartupInfo{Cb: uint32(unsafe.Sizeof(winapi.StartupInfo{}))}
var info winapi.ProcessInformation
flags := uint32(winapi.CREATE_NO_WINDOW | winapi.CREATE_UNICODE_ENVIRONMENT)
if err := winapi.CreateProcessAsUser(token, application, &commandUTF16[0], nil, nil, false, flags, nil, nil, &startup, &info); err != nil {
return false, err
}
defer func() {
_ = winapi.CloseHandle(info.Process)
_ = winapi.CloseHandle(info.Thread)
}()
creation, err := processCreation(info.Process)
if err != nil {
return false, err
}
if _, err := winapi.ResumeThread(info.Thread); err != nil {
return false, err
}
pipe := launcherPipe{name: signalPipePrefix + channel, file: server}
peerCtx, cancel := context.WithTimeout(ctx, launcherHandshakeTimeout)
defer cancel()
if err := connectLauncherPipe(peerCtx, pipe, launcherPeer{PID: info.ProcessId, Creation: creation, SessionID: identity.SessionID, UserSID: identity.UserSID}); err != nil {
_ = winapi.TerminateProcess(info.Process, 1)
return false, err
}
frame, err := readLauncherFrameContext(peerCtx, server, serverGeneration)
if err != nil {
return false, err
}
if frame.Kind != launcherFrameHello {
return false, errors.New("signal helper did not send hello")
}
var hello launcherHello
if err := unmarshalLauncherPayload(frame.Payload, &hello); err != nil {
return false, err
}
if hello.PID != info.ProcessId || hello.Creation != creation || hello.SessionID != identity.SessionID || hello.Effective != identity.UserSID {
return false, errors.New("signal helper identity mismatch")
}
payload, err := marshalLauncherPayload(signalHelperRequest{PID: targetPID, Creation: targetCreation, SessionID: identity.SessionID})
if err != nil {
return false, err
}
if err := writeLauncherFrame(server, launcherFrame{Kind: launcherFrameSignalRequest, Generation: serverGeneration, Payload: payload}); err != nil {
return false, err
}
frame, err = readLauncherFrameContext(peerCtx, server, serverGeneration)
if err != nil {
return false, err
}
if frame.Kind != launcherFrameSignalResult {
return false, errors.New("signal helper returned an unexpected frame")
}
var result signalHelperResult
if err := unmarshalLauncherPayload(frame.Payload, &result); err != nil {
return false, err
}
_, _ = winapi.WaitForSingleObject(info.Process, uint32(launcherHandshakeTimeout/time.Millisecond))
return result.Delivered, nil
}
// RunSignalHelper verifies the target process identity supplied over its
// authenticated pipe before attaching to its private console. It never takes
// a PID from the command line and never receives a Job or stdio handle.
func RunSignalHelper(_ context.Context, channel string) error {
generation, err := launcherGeneration(channel)
if err != nil {
return err
}
pipe, err := openSignalPipe(channel)
if err != nil {
return err
}
defer pipe.Close()
var current winapi.Token
if err := winapi.OpenProcessToken(winapi.CurrentProcess(), winapi.TOKEN_QUERY, &current); err != nil {
return err
}
defer current.Close()
user, err := current.GetTokenUser()
if err != nil || user.User.Sid == nil {
return errors.New("signal helper token has no user SID")
}
sessionID, err := tokenInformationUint32(current, winapi.TokenSessionId)
if err != nil {
return err
}
creation, err := processCreation(winapi.CurrentProcess())
if err != nil {
return err
}
hello, err := marshalLauncherPayload(launcherHello{PID: winapi.GetCurrentProcessId(), Creation: creation, SessionID: sessionID, Effective: user.User.Sid.String()})
if err != nil {
return err
}
if err := writeLauncherFrame(pipe, launcherFrame{Kind: launcherFrameHello, Generation: generation, Payload: hello}); err != nil {
return err
}
frame, err := readLauncherFrame(pipe, generation)
if err != nil {
return err
}
if frame.Kind != launcherFrameSignalRequest {
return errors.New("signal helper received an unexpected frame")
}
var request signalHelperRequest
if err := unmarshalLauncherPayload(frame.Payload, &request); err != nil {
return err
}
if request.PID == 0 || request.Creation == 0 || request.SessionID != sessionID {
return errors.New("signal helper target identity is invalid")
}
target, err := winapi.OpenProcess(winapi.PROCESS_QUERY_LIMITED_INFORMATION, false, request.PID)
if err != nil {
return err
}
actualCreation, err := processCreation(target)
if err != nil {
_ = winapi.CloseHandle(target)
return err
}
if actualCreation != request.Creation {
_ = winapi.CloseHandle(target)
return errors.New("signal helper target creation time mismatch")
}
var targetToken winapi.Token
if err := winapi.OpenProcessToken(target, winapi.TOKEN_QUERY, &targetToken); err != nil {
_ = winapi.CloseHandle(target)
return err
}
defer targetToken.Close()
targetUser, err := targetToken.GetTokenUser()
if err != nil || targetUser.User.Sid == nil || targetUser.User.Sid.String() != user.User.Sid.String() {
_ = winapi.CloseHandle(target)
return errors.New("signal helper target token SID mismatch")
}
targetSession, err := tokenInformationUint32(targetToken, winapi.TokenSessionId)
_ = winapi.CloseHandle(target)
if err != nil || targetSession != sessionID {
return errors.New("signal helper target session mismatch")
}
delivered, deliveryErr := sendControlBreak(request.PID)
detail := "CTRL_BREAK delivered"
if deliveryErr != nil {
detail = deliveryErr.Error()
}
result, err := marshalLauncherPayload(signalHelperResult{Delivered: delivered, Detail: detail})
if err != nil {
return err
}
if err := writeLauncherFrame(pipe, launcherFrame{Kind: launcherFrameSignalResult, Generation: generation, Payload: result}); err != nil {
return err
}
return nil
}