//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 }