feat: authenticate Windows command launcher and signal helper

This commit is contained in:
2026-09-06 14:38:16 +00:00
parent 158fd8f3aa
commit 0383155b86
13 changed files with 1270 additions and 189 deletions
@@ -0,0 +1,139 @@
package windows
// The launcher wire format is deliberately private to one service/launcher
// pair. It is not a second public protocol: the channel name is random, the
// generation is bound into every frame, and every frame is length- and
// checksum-validated before its payload is decoded.
import (
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
)
const (
launcherMagic uint32 = 0x52564c31 // RVL1
launcherVersion uint16 = 1
launcherHeaderBytes = 4 + 2 + 2 + 16 + 4 + sha256.Size
launcherMaxFrameBytes = 4 << 20
)
type launcherFrameKind uint16
const (
launcherFrameHello launcherFrameKind = iota + 1
launcherFrameLaunch
launcherFrameShellPrepared
launcherFrameRelease
launcherFrameReleased
launcherFrameAbort
launcherFrameSignalRequest
launcherFrameSignalResult
)
type launcherFrame struct {
Kind launcherFrameKind
Generation [16]byte
Payload []byte
}
func marshalLauncherFrame(frame launcherFrame) ([]byte, error) {
if frame.Kind == 0 || frame.Kind > launcherFrameSignalResult {
return nil, errors.New("launcher frame kind is invalid")
}
if uint64(len(frame.Payload)) > launcherMaxFrameBytes {
return nil, fmt.Errorf("launcher frame payload exceeds %d bytes", launcherMaxFrameBytes)
}
encoded := make([]byte, launcherHeaderBytes+len(frame.Payload))
binary.LittleEndian.PutUint32(encoded[0:4], launcherMagic)
binary.LittleEndian.PutUint16(encoded[4:6], launcherVersion)
binary.LittleEndian.PutUint16(encoded[6:8], uint16(frame.Kind))
copy(encoded[8:24], frame.Generation[:])
binary.LittleEndian.PutUint32(encoded[24:28], uint32(len(frame.Payload)))
copy(encoded[launcherHeaderBytes:], frame.Payload)
check := make([]byte, 28+len(frame.Payload))
copy(check, encoded[:28])
copy(check[28:], frame.Payload)
digest := sha256.Sum256(check)
copy(encoded[28:launcherHeaderBytes], digest[:])
return encoded, nil
}
func readLauncherFrame(reader io.Reader, expected [16]byte) (launcherFrame, error) {
header := make([]byte, launcherHeaderBytes)
if _, err := io.ReadFull(reader, header); err != nil {
return launcherFrame{}, err
}
if binary.LittleEndian.Uint32(header[0:4]) != launcherMagic {
return launcherFrame{}, errors.New("launcher frame magic mismatch")
}
if binary.LittleEndian.Uint16(header[4:6]) != launcherVersion {
return launcherFrame{}, errors.New("launcher frame version mismatch")
}
kind := launcherFrameKind(binary.LittleEndian.Uint16(header[6:8]))
if kind == 0 || kind > launcherFrameSignalResult {
return launcherFrame{}, errors.New("launcher frame kind is invalid")
}
var generation [16]byte
copy(generation[:], header[8:24])
if generation != expected {
return launcherFrame{}, errors.New("launcher frame generation mismatch")
}
length := binary.LittleEndian.Uint32(header[24:28])
if length > launcherMaxFrameBytes {
return launcherFrame{}, fmt.Errorf("launcher frame payload exceeds %d bytes", launcherMaxFrameBytes)
}
payload := make([]byte, int(length))
if _, err := io.ReadFull(reader, payload); err != nil {
return launcherFrame{}, err
}
check := make([]byte, 28+len(payload))
copy(check, header[:28])
copy(check[28:], payload)
digest := sha256.Sum256(check)
if string(digest[:]) != string(header[28:launcherHeaderBytes]) {
return launcherFrame{}, errors.New("launcher frame checksum mismatch")
}
return launcherFrame{Kind: kind, Generation: generation, Payload: payload}, nil
}
func readLauncherFrameContext(ctx context.Context, reader io.Reader, expected [16]byte) (launcherFrame, error) {
result := make(chan struct {
frame launcherFrame
err error
}, 1)
go func() {
frame, err := readLauncherFrame(reader, expected)
result <- struct {
frame launcherFrame
err error
}{frame: frame, err: err}
}()
select {
case <-ctx.Done():
return launcherFrame{}, ctx.Err()
case value := <-result:
return value.frame, value.err
}
}
func writeLauncherFrame(writer io.Writer, frame launcherFrame) error {
encoded, err := marshalLauncherFrame(frame)
if err != nil {
return err
}
for len(encoded) > 0 {
written, err := writer.Write(encoded)
if err != nil {
return err
}
if written <= 0 || written > len(encoded) {
return errors.New("launcher frame writer made no progress")
}
encoded = encoded[written:]
}
return nil
}
@@ -0,0 +1,57 @@
package windows
import (
"bytes"
"testing"
)
func TestLauncherFrameRoundTripAndGenerationFence_HP_LAUNCH_06(t *testing.T) {
var generation [16]byte
for index := range generation {
generation[index] = byte(index + 1)
}
original := launcherFrame{Kind: launcherFrameLaunch, Generation: generation, Payload: []byte("bounded launch request")}
encoded, err := marshalLauncherFrame(original)
if err != nil {
t.Fatal(err)
}
decoded, err := readLauncherFrame(bytes.NewReader(encoded), generation)
if err != nil {
t.Fatal(err)
}
if decoded.Kind != original.Kind || !bytes.Equal(decoded.Payload, original.Payload) {
t.Fatalf("decoded frame = %#v, want %#v", decoded, original)
}
wrong := generation
wrong[0]++
if _, err := readLauncherFrame(bytes.NewReader(encoded), wrong); err == nil {
t.Fatal("frame with a different generation was accepted")
}
}
func TestLauncherFrameRejectsChecksumLengthAndKind_BH_LAUNCH_06(t *testing.T) {
var generation [16]byte
encoded, err := marshalLauncherFrame(launcherFrame{Kind: launcherFrameHello, Generation: generation, Payload: []byte("hello")})
if err != nil {
t.Fatal(err)
}
encoded[len(encoded)-1] ^= 1
if _, err := readLauncherFrame(bytes.NewReader(encoded), generation); err == nil {
t.Fatal("checksum-corrupted frame was accepted")
}
encoded, err = marshalLauncherFrame(launcherFrame{Kind: launcherFrameHello, Generation: generation, Payload: nil})
if err != nil {
t.Fatal(err)
}
encoded[6] = 0xff
encoded[7] = 0xff
if _, err := readLauncherFrame(bytes.NewReader(encoded), generation); err == nil {
t.Fatal("invalid frame kind was accepted")
}
if _, err := marshalLauncherFrame(launcherFrame{Kind: launcherFrameAbort, Generation: generation, Payload: make([]byte, launcherMaxFrameBytes+1)}); err == nil {
t.Fatal("oversized launcher payload was accepted")
}
if _, err := readLauncherFrame(bytes.NewReader(encoded[:len(encoded)-1]), generation); err == nil {
t.Fatal("truncated launcher frame was accepted")
}
}
@@ -0,0 +1,850 @@
//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
}
@@ -0,0 +1,58 @@
package windows
import (
"encoding/json"
"fmt"
)
// Wire payloads are JSON only inside the authenticated private pipe. The
// outer frame still supplies a bounded binary length, generation and digest;
// JSON keeps the launcher mode stateless and makes malformed-field rejection
// explicit rather than relying on Go's gob type registry.
type launcherHello struct {
PID uint32 `json:"pid"`
Creation uint64 `json:"creation"`
SessionID uint32 `json:"session_id"`
Effective string `json:"effective_sid"`
}
type launcherRequest struct {
ApplicationName string `json:"application_name"`
CommandLine string `json:"command_line"`
WorkingDirectory string `json:"working_directory"`
Environment []uint16 `json:"environment"`
}
type launcherShellPrepared struct {
PID uint32 `json:"pid"`
Creation uint64 `json:"creation"`
}
type signalHelperRequest struct {
PID uint32 `json:"pid"`
Creation uint64 `json:"creation"`
SessionID uint32 `json:"session_id"`
}
type signalHelperResult struct {
Delivered bool `json:"delivered"`
Detail string `json:"detail"`
}
func marshalLauncherPayload(value any) ([]byte, error) {
payload, err := json.Marshal(value)
if err != nil {
return nil, err
}
if len(payload) > launcherMaxFrameBytes {
return nil, fmt.Errorf("launcher payload exceeds %d bytes", launcherMaxFrameBytes)
}
return payload, nil
}
func unmarshalLauncherPayload(payload []byte, target any) error {
if len(payload) == 0 || len(payload) > launcherMaxFrameBytes {
return fmt.Errorf("launcher payload length %d is invalid", len(payload))
}
return json.Unmarshal(payload, target)
}
@@ -33,7 +33,11 @@ var (
// token/session selection and Job Object containment in the native build; the
// test adapter uses the same shell and output limits without OS handles.
type NativeOptions struct {
Shells ShellPaths
Shells ShellPaths
// ExecutablePath is the canonical rvbox.exe path used for private
// per-command launcher mode. An empty value is resolved from the running
// service executable on Windows.
ExecutablePath string
WorkRoot string
JobProfiles map[string]JobProfile
MaxWrapperBytes uint64
@@ -87,6 +91,7 @@ type execProcess struct {
identity supervisor.EffectiveIdentity
cmd *exec.Cmd
pid uint32
creation uint64
stdin io.WriteCloser
stdout io.ReadCloser
stderr io.ReadCloser
@@ -96,6 +101,7 @@ type execProcess struct {
waitFn func() (int32, bool, error)
killFn func(uint32) error
releaseFn func() error
signalFn func(context.Context) (bool, error)
snapshotFn func() (supervisor.ResourceSnapshot, error)
mu sync.Mutex
@@ -12,9 +12,7 @@ import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
"unsafe"
@@ -47,13 +45,6 @@ var (
func stdinLineEnding() []byte { return []byte{'\r', '\n'} }
type nativeHandles struct {
process winapi.Handle
job winapi.Handle
pid uint32
close sync.Once
}
// NewSupervisor constructs the machine-wide Windows implementation. The
// service process is expected to run as LocalSystem; token selection verifies
// that assumption when a command is started and records the selected context.
@@ -94,12 +85,14 @@ func (manager *execSupervisor) Start(ctx context.Context, spec supervisor.StartS
}
return nil, cause
}
token, identity, err := manager.selectToken(spec.Execution.GetElevated())
if err != nil {
return fail(err)
}
defer token.Close()
if err := verifyWorkingDirectory(spec.WorkingDirectory); err != nil {
return fail(err)
}
wrapperPath, cleanup, err := materializeWrapper(spec.WorkingDirectory, wrapper, manager.options.Now(), func(path string) error {
return secureWrapperFile(path, identity.UserSID)
})
@@ -118,133 +111,11 @@ func (manager *execSupervisor) Start(ctx context.Context, spec supervisor.StartS
if err != nil {
return fail(err)
}
stdinRead, stdinWrite, stdoutRead, stdoutWrite, stderrRead, stderrWrite, err := createStandardPipes()
process, err := manager.startViaLauncher(ctx, spec, token, identity, launch, cleanup)
if err != nil {
return fail(err)
}
closeFiles := func() {
for _, file := range []*os.File{stdinRead, stdinWrite, stdoutRead, stdoutWrite, stderrRead, stderrWrite} {
if file != nil {
_ = file.Close()
}
}
}
pipesTransferred := false
defer func() {
// Parent-side handles are retained only after successful process
// creation. Any error path closes both ends here.
if !pipesTransferred {
closeFiles()
}
}()
job, err := createKillOnCloseJob()
if err != nil {
closeFiles()
return fail(fmt.Errorf("create command Job: %w", err))
}
if err := applyJobProfiles(job, manager.options.JobProfiles, spec.ExecutionProfiles); err != nil {
_ = winapi.CloseHandle(job)
return fail(err)
}
cleanupJob := true
defer func() {
if cleanupJob {
_ = winapi.CloseHandle(job)
}
}()
application, err := winapi.UTF16PtrFromString(launch.ApplicationName)
if err != nil {
return fail(err)
}
commandLine, err := winapi.UTF16FromString(launch.CommandLine)
if err != nil {
return fail(err)
}
workingDirectory, err := winapi.UTF16PtrFromString(launch.WorkingDirectory)
if err != nil {
return fail(err)
}
attributeList, err := winapi.NewProcThreadAttributeList(1)
if err != nil {
return fail(err)
}
defer attributeList.Delete()
childHandles := []winapi.Handle{winapi.Handle(stdinRead.Fd()), winapi.Handle(stdoutWrite.Fd()), winapi.Handle(stderrWrite.Fd())}
if err := attributeList.Update(winapi.PROC_THREAD_ATTRIBUTE_HANDLE_LIST, unsafe.Pointer(&childHandles[0]), uintptr(len(childHandles))*unsafe.Sizeof(childHandles[0])); err != nil {
return fail(err)
}
startup := winapi.StartupInfoEx{}
startup.Cb = uint32(unsafe.Sizeof(startup))
startup.Flags = winapi.STARTF_USESTDHANDLES | winapi.STARTF_USESHOWWINDOW
startup.ShowWindow = winapi.SW_HIDE
startup.StdInput = childHandles[0]
startup.StdOutput = childHandles[1]
startup.StdErr = childHandles[2]
startup.ProcThreadAttributeList = attributeList.List()
var processInfo winapi.ProcessInformation
flags := uint32(winapi.CREATE_NEW_CONSOLE | winapi.CREATE_SUSPENDED | winapi.CREATE_UNICODE_ENVIRONMENT | winapi.EXTENDED_STARTUPINFO_PRESENT)
var environmentPointer *uint16
if len(environment) > 0 {
environmentPointer = &environment[0]
}
if err := winapi.CreateProcessAsUser(token, application, &commandLine[0], nil, nil, true, flags, environmentPointer, workingDirectory, &startup.StartupInfo, &processInfo); err != nil {
return fail(fmt.Errorf("create suspended command process: %w", err))
}
// The child owns these handles after CreateProcessAsUser returns. Keep only
// the three parent ends and the process/job handles in the daemon. The
// primary thread remains suspended until the executor has durably recorded
// launch authorization and calls Process.Release.
_ = stdinRead.Close()
_ = stdoutWrite.Close()
_ = stderrWrite.Close()
if err := winapi.AssignProcessToJobObject(job, processInfo.Process); err != nil {
_ = winapi.TerminateProcess(processInfo.Process, 1)
_ = winapi.CloseHandle(processInfo.Process)
_ = winapi.CloseHandle(processInfo.Thread)
return fail(fmt.Errorf("assign command to Job: %w", err))
}
pipesTransferred = true
var threadClosed sync.Once
closeThread := func() {
threadClosed.Do(func() { _ = winapi.CloseHandle(processInfo.Thread) })
}
releaseFn := func() error {
if _, err := winapi.ResumeThread(processInfo.Thread); err != nil {
closeThread()
return fmt.Errorf("release suspended command: %w", err)
}
closeThread()
return nil
}
started := manager.options.Now()
handles := &nativeHandles{process: processInfo.Process, job: job, pid: processInfo.ProcessId}
cleanupJob = false
command := &exec.Cmd{Process: osProcess(processInfo.ProcessId)}
waitFn := func() (int32, bool, error) {
_, waitErr := winapi.WaitForSingleObject(processInfo.Process, winapi.INFINITE)
var code uint32
if err := winapi.GetExitCodeProcess(processInfo.Process, &code); err != nil && waitErr == nil {
waitErr = err
}
closeThread()
handles.close.Do(func() {
_ = winapi.CloseHandle(processInfo.Process)
_ = winapi.CloseHandle(job)
})
return int32(code), false, waitErr
}
killFn := func(code uint32) error {
err := winapi.TerminateJobObject(job, code)
closeThread()
return err
}
process := manager.registerProcess(spec.IssueUUID, identity, command, stdinWrite, stdoutRead, stderrRead, started, waitFn, killFn, releaseFn, cleanup)
process.snapshotFn = func() (supervisor.ResourceSnapshot, error) {
return queryJobSnapshot(job, manager.options.Now())
return nil, err
}
cleanup = nil
return process, nil
}
@@ -267,6 +138,31 @@ func verifyExecutable(path string) error {
return nil
}
func verifyWorkingDirectory(path string) error {
info, err := os.Lstat(path)
if err != nil {
return fmt.Errorf("stat working directory %q: %w", path, err)
}
if !info.IsDir() {
return fmt.Errorf("working directory %q is not a directory", path)
}
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("working directory %q is a symlink", path)
}
name, err := winapi.UTF16PtrFromString(path)
if err != nil {
return err
}
attributes, err := winapi.GetFileAttributes(name)
if err != nil {
return fmt.Errorf("query working directory attributes %q: %w", path, err)
}
if attributes&winapi.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return fmt.Errorf("working directory %q is a reparse point", path)
}
return nil
}
// secureWrapperFile replaces the inherited directory ACL with a protected
// DACL. The service writes the wrapper before this call; afterward only
// LocalSystem and the selected effective token SID can read it. This is done
@@ -311,36 +207,6 @@ func secureWrapperFile(path, effectiveSID string) error {
return winapi.SetNamedSecurityInfo(path, winapi.SE_FILE_OBJECT, winapi.OWNER_SECURITY_INFORMATION|winapi.DACL_SECURITY_INFORMATION|winapi.PROTECTED_DACL_SECURITY_INFORMATION, systemSID, nil, acl, nil)
}
func createStandardPipes() (*os.File, *os.File, *os.File, *os.File, *os.File, *os.File, error) {
security := &winapi.SecurityAttributes{Length: uint32(unsafe.Sizeof(winapi.SecurityAttributes{})), InheritHandle: 1}
var stdinReadHandle, stdinWriteHandle winapi.Handle
var stdoutReadHandle, stdoutWriteHandle winapi.Handle
var stderrReadHandle, stderrWriteHandle winapi.Handle
if err := winapi.CreatePipe(&stdinReadHandle, &stdinWriteHandle, security, 0); err != nil {
return nil, nil, nil, nil, nil, nil, err
}
if err := winapi.CreatePipe(&stdoutReadHandle, &stdoutWriteHandle, security, 0); err != nil {
_ = winapi.CloseHandle(stdinReadHandle)
_ = winapi.CloseHandle(stdinWriteHandle)
return nil, nil, nil, nil, nil, nil, err
}
if err := winapi.CreatePipe(&stderrReadHandle, &stderrWriteHandle, security, 0); err != nil {
for _, handle := range []winapi.Handle{stdinReadHandle, stdinWriteHandle, stdoutReadHandle, stdoutWriteHandle} {
_ = winapi.CloseHandle(handle)
}
return nil, nil, nil, nil, nil, nil, err
}
for _, handle := range []winapi.Handle{stdinWriteHandle, stdoutReadHandle, stderrReadHandle} {
if err := winapi.SetHandleInformation(handle, winapi.HANDLE_FLAG_INHERIT, 0); err != nil {
for _, closeHandle := range []winapi.Handle{stdinReadHandle, stdinWriteHandle, stdoutReadHandle, stdoutWriteHandle, stderrReadHandle, stderrWriteHandle} {
_ = winapi.CloseHandle(closeHandle)
}
return nil, nil, nil, nil, nil, nil, err
}
}
return os.NewFile(uintptr(stdinReadHandle), "rvbox-stdin-read"), os.NewFile(uintptr(stdinWriteHandle), "rvbox-stdin-write"), os.NewFile(uintptr(stdoutReadHandle), "rvbox-stdout-read"), os.NewFile(uintptr(stdoutWriteHandle), "rvbox-stdout-write"), os.NewFile(uintptr(stderrReadHandle), "rvbox-stderr-read"), os.NewFile(uintptr(stderrWriteHandle), "rvbox-stderr-write"), nil
}
func createKillOnCloseJob() (winapi.Handle, error) {
job, err := winapi.CreateJobObject(nil, nil)
if err != nil {
@@ -900,18 +766,32 @@ func (manager *execSupervisor) Signal(ctx context.Context, process supervisor.Pr
return supervisor.SignalOutcome{}, errors.New("unsupported signal")
}
if signal == supervisor.SignalTerm {
// Each command has its own hidden console. The helper path is kept in
// this short-lived call and is deliberately best-effort: a session that
// has already exited or a policy that denies AttachConsole is recorded,
// then the bounded grace period ends in an explicit Job kill.
breakDelivered, breakErr := sendControlBreak(native.pid)
// Each command has its own hidden console. The short-lived canonical
// helper is preferred; the direct attach path remains a last-resort
// diagnostic fallback when helper creation is unavailable.
breakDelivered, breakErr := false, error(nil)
helperUsed := native.signalFn != nil
if native.signalFn != nil {
breakDelivered, breakErr = native.signalFn(ctx)
if breakErr != nil && ctx.Err() == nil {
// Keep TERM best-effort if the helper itself cannot be started;
// the fallback still checks the immutable PID/creation evidence.
breakDelivered, breakErr = sendControlBreakVerified(native.pid, native.creation)
}
} else {
breakDelivered, breakErr = sendControlBreakVerified(native.pid, native.creation)
}
if breakErr != nil && ctx.Err() != nil {
return supervisor.SignalOutcome{}, ctx.Err()
}
if breakDelivered {
select {
case <-native.done:
return supervisor.SignalOutcome{Delivered: true, Detail: "CTRL_BREAK delivered", ObservedAt: manager.options.Now()}, nil
detail := "CTRL_BREAK delivered"
if helperUsed {
detail = "CTRL_BREAK delivered by authenticated signal helper"
}
return supervisor.SignalOutcome{Delivered: true, Detail: detail, ObservedAt: manager.options.Now()}, nil
default:
}
}
@@ -933,11 +813,9 @@ func (manager *execSupervisor) Signal(ctx context.Context, process supervisor.Pr
return supervisor.SignalOutcome{Delivered: true, Escalated: signal == supervisor.SignalTerm, Detail: detail, ObservedAt: manager.options.Now()}, nil
}
// sendControlBreak is the native equivalent of the signal-helper mode. The
// production helper is normally a separate short-lived rvbox.exe invocation;
// this direct implementation keeps the same verified PID/console boundary
// for the first service build and never addresses a process by a caller-
// supplied PID. The PID comes only from execProcess metadata.
// sendControlBreak is the last-resort local fallback for the authenticated
// signal-helper path. It never addresses a caller-supplied PID: the PID comes
// only from immutable execProcess metadata captured during preparation.
func sendControlBreak(pid uint32) (bool, error) {
if pid == 0 {
return false, errors.New("command has no verified console PID")
@@ -962,6 +840,25 @@ func sendControlBreak(pid uint32) (bool, error) {
return true, nil
}
func sendControlBreakVerified(pid uint32, creation uint64) (bool, error) {
if creation == 0 {
return false, errors.New("command has no verified process creation time")
}
process, err := winapi.OpenProcess(winapi.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
if err != nil {
return false, err
}
actual, err := processCreation(process)
_ = winapi.CloseHandle(process)
if err != nil {
return false, err
}
if actual != creation {
return false, errors.New("command PID was reused")
}
return sendControlBreak(pid)
}
func (manager *execSupervisor) Snapshot(ctx context.Context, process supervisor.Process) (supervisor.ResourceSnapshot, error) {
if process == nil {
return supervisor.ResourceSnapshot{}, ErrProcessNotFound