851 lines
30 KiB
Go
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, ¤t)
|
|
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, ¤t); 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
|
|
}
|