feat: complete Windows client control and recovery paths

This commit is contained in:
2026-09-06 13:37:32 +00:00
parent 56b15c7f4f
commit 486894557d
38 changed files with 2188 additions and 106 deletions
+72 -6
View File
@@ -17,6 +17,29 @@ var (
ErrUnsupported = errors.New("supervisor operation is unsupported")
)
// StartError carries durable context-selection evidence when a command is
// rejected before a process exists. The runtime records the evidence with
// the rejection event without treating the error as a second public protocol
// type. Unwrap keeps ordinary retry/error classification working.
type StartError struct {
Cause error
WindowsIdentity *rvboxv1.WindowsExecutionIdentity
}
func (err *StartError) Error() string {
if err == nil || err.Cause == nil {
return "supervisor start failed"
}
return err.Cause.Error()
}
func (err *StartError) Unwrap() error {
if err == nil {
return nil
}
return err.Cause
}
type SignalKind uint8
const (
@@ -53,12 +76,55 @@ func (spec StartSpec) Validate() error {
// EffectiveIdentity is immutable process evidence captured before launch.
// Empty user/session fields mean a Session 0 service context.
type EffectiveIdentity struct {
Context string
SessionID uint32
UserSID string
LogonSID string
Elevated bool
Integrity string
Context string
SessionID uint32
SessionUserSID string
UserSID string
LogonSID string
Elevated bool
Integrity string
AttemptedContexts []string
SelectionDetail string
}
// WindowsIdentity converts the platform-neutral evidence to the public
// immutable status/event shape. Unknown contexts are deliberately omitted so
// the portable test supervisor never pretends to be a Windows launch.
func (identity EffectiveIdentity) WindowsIdentity() *rvboxv1.WindowsExecutionIdentity {
result := &rvboxv1.WindowsExecutionIdentity{SessionUserSid: identity.SessionUserSID, EffectiveUserSid: identity.UserSID, SelectionDetail: identity.SelectionDetail}
for _, contextName := range identity.AttemptedContexts {
if context, ok := windowsExecutionContext(contextName); ok {
result.AttemptedContexts = append(result.AttemptedContexts, context)
}
}
if context, ok := windowsExecutionContext(identity.Context); ok {
result.EffectiveContext = &context
if context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER || context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED || context == rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM {
session := identity.SessionID
result.SessionId = &session
}
}
if result.EffectiveContext == nil && len(result.AttemptedContexts) == 0 && result.SelectionDetail == "" && result.SessionUserSid == "" && result.EffectiveUserSid == "" {
return nil
}
return result
}
func windowsExecutionContext(value string) (rvboxv1.WindowsExecutionContext, bool) {
switch value {
case "LOCAL_SERVICE":
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SERVICE, true
case "LOCAL_SYSTEM":
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_LOCAL_SYSTEM, true
case "ACTIVE_USER":
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER, true
case "ACTIVE_USER_ELEVATED":
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED, true
case "ACTIVE_SYSTEM":
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM, true
default:
return rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_UNSPECIFIED, false
}
}
type Process interface {
@@ -30,3 +30,25 @@ func TestStartSpecValidation_BH_SUPERVISOR_01(t *testing.T) {
t.Fatal(err)
}
}
func TestWindowsIdentityMapsSelectionEvidence_HP_SUPERVISOR_05(t *testing.T) {
t.Parallel()
identity := EffectiveIdentity{
Context: "ACTIVE_SYSTEM",
SessionID: 7,
SessionUserSID: "S-1-5-21-user",
UserSID: "S-1-5-18",
AttemptedContexts: []string{"ACTIVE_USER_ELEVATED", "ACTIVE_SYSTEM"},
SelectionDetail: "ACTIVE_USER_ELEVATED: ELEVATION_UNAVAILABLE",
}
encoded := identity.WindowsIdentity()
if encoded == nil || encoded.GetEffectiveContext() != rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_SYSTEM || encoded.GetSessionId() != 7 || encoded.GetSessionUserSid() != "S-1-5-21-user" || encoded.GetEffectiveUserSid() != "S-1-5-18" {
t.Fatalf("mapped identity = %#v", encoded)
}
if len(encoded.GetAttemptedContexts()) != 2 || encoded.GetAttemptedContexts()[0] != rvboxv1.WindowsExecutionContext_WINDOWS_EXECUTION_CONTEXT_ACTIVE_USER_ELEVATED {
t.Fatalf("attempted contexts = %v", encoded.GetAttemptedContexts())
}
if got := (EffectiveIdentity{AttemptedContexts: []string{"not-a-context"}}).WindowsIdentity(); got != nil {
t.Fatalf("unknown context evidence = %#v", got)
}
}
@@ -35,12 +35,26 @@ var (
type NativeOptions struct {
Shells ShellPaths
WorkRoot string
JobProfiles map[string]JobProfile
MaxWrapperBytes uint64
MaxOutputChunk uint64
WindowsTermGrace time.Duration
Now func() time.Time
}
// JobProfile is the validated administrator policy for one protocol profile.
// The native Windows adapter applies only controls it can set and read back
// atomically on a Job Object; a requested unsupported required control rejects
// the command before any child is created.
type JobProfile struct {
RequiredControls []string
CPUPercent uint64
MemoryMaxBytes uint64
PIDsMax uint64
IOReadBPS uint64
IOWriteBPS uint64
}
func (options NativeOptions) withDefaults() NativeOptions {
if options.MaxWrapperBytes == 0 {
options.MaxWrapperBytes = 10 << 20
@@ -69,17 +83,19 @@ func newExecSupervisor(options NativeOptions) *execSupervisor {
}
type execProcess struct {
issue domain.UUID
identity supervisor.EffectiveIdentity
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
stderr io.ReadCloser
outputs chan outputResult
done chan struct{}
started time.Time
waitFn func() (int32, bool, error)
killFn func(uint32) error
issue domain.UUID
identity supervisor.EffectiveIdentity
cmd *exec.Cmd
pid uint32
stdin io.WriteCloser
stdout io.ReadCloser
stderr io.ReadCloser
outputs chan outputResult
done chan struct{}
started time.Time
waitFn func() (int32, bool, error)
killFn func(uint32) error
snapshotFn func() (supervisor.ResourceSnapshot, error)
mu sync.Mutex
finished bool
@@ -219,7 +235,7 @@ func (process *execProcess) startReaders(maxChunk uint64, remove func()) {
}
func materializeWrapper(directory string, wrapper Wrapper, now time.Time) (string, func(), error) {
if directory == "" || !now.IsZero() && now.Location() == nil {
if directory == "" {
return "", nil, ErrInvalidWorkingDirectory
}
if err := os.MkdirAll(directory, 0o700); err != nil {
@@ -441,7 +457,11 @@ func (manager *execSupervisor) startCommand(ctx context.Context, spec supervisor
}
func (manager *execSupervisor) registerProcess(issue domain.UUID, identity supervisor.EffectiveIdentity, command *exec.Cmd, stdin io.WriteCloser, stdout, stderr io.ReadCloser, started time.Time, waitFn func() (int32, bool, error), killFn func(uint32) error, cleanup func()) *execProcess {
process := &execProcess{issue: issue, identity: identity, cmd: command, stdin: stdin, stdout: stdout, stderr: stderr, outputs: make(chan outputResult, 32), done: make(chan struct{}), started: started, waitFn: waitFn, killFn: killFn}
var pid uint32
if command != nil && command.Process != nil && command.Process.Pid > 0 {
pid = uint32(command.Process.Pid)
}
process := &execProcess{issue: issue, identity: identity, cmd: command, pid: pid, stdin: stdin, stdout: stdout, stderr: stderr, outputs: make(chan outputResult, 32), done: make(chan struct{}), started: started, waitFn: waitFn, killFn: killFn}
manager.mu.Lock()
manager.active[issue] = process
manager.mu.Unlock()
@@ -13,6 +13,7 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
@@ -27,11 +28,18 @@ const (
logon32LogonService = 5
logon32ProviderDefault = 0
securitySystemRID = "S-1-5-18"
disableMaxPrivilege = 0x1
)
var (
advapi32 = syscall.NewLazyDLL("advapi32.dll")
procLogonUserW = advapi32.NewProc("LogonUserW")
advapi32 = syscall.NewLazyDLL("advapi32.dll")
procLogonUserW = advapi32.NewProc("LogonUserW")
procCreateRestrictedToken = advapi32.NewProc("CreateRestrictedToken")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procAttachConsole = kernel32.NewProc("AttachConsole")
procFreeConsole = kernel32.NewProc("FreeConsole")
procGenerateCtrlEvent = kernel32.NewProc("GenerateConsoleCtrlEvent")
procSetCtrlHandler = kernel32.NewProc("SetConsoleCtrlHandler")
)
func stdinLineEnding() []byte { return []byte{'\r', '\n'} }
@@ -130,6 +138,10 @@ func (manager *execSupervisor) Start(ctx context.Context, spec supervisor.StartS
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 {
@@ -214,6 +226,9 @@ func (manager *execSupervisor) Start(ctx context.Context, spec supervisor.StartS
return winapi.TerminateJobObject(job, code)
}
process := manager.registerProcess(spec.IssueUUID, identity, command, stdinWrite, stdoutRead, stderrRead, started, waitFn, killFn, cleanup)
process.snapshotFn = func() (supervisor.ResourceSnapshot, error) {
return queryJobSnapshot(job, manager.options.Now())
}
return process, nil
}
@@ -280,6 +295,120 @@ func createKillOnCloseJob() (winapi.Handle, error) {
return job, nil
}
// applyJobProfiles combines the requested dimensions and applies them before
// process creation. The profile names originate from the validated protobuf
// ExecutionSpec; unknown names and any required control without a native
// implementation are permanent pre-launch failures.
func applyJobProfiles(job winapi.Handle, configured map[string]JobProfile, requested []string) error {
if len(requested) == 0 {
return nil
}
var combined JobProfile
for _, name := range requested {
profile, ok := configured[name]
if !ok {
return fmt.Errorf("%w: execution profile %q is not configured", supervisor.ErrUnsupported, name)
}
for _, required := range profile.RequiredControls {
switch required {
case "cpu", "memory", "pids":
case "io":
return fmt.Errorf("%w: Windows Job I/O rate control is not available in this build", supervisor.ErrUnsupported)
default:
return fmt.Errorf("%w: unknown required Job control %q", supervisor.ErrUnsupported, required)
}
}
if profile.CPUPercent > combined.CPUPercent {
combined.CPUPercent = profile.CPUPercent
}
if profile.MemoryMaxBytes > 0 && (combined.MemoryMaxBytes == 0 || profile.MemoryMaxBytes < combined.MemoryMaxBytes) {
combined.MemoryMaxBytes = profile.MemoryMaxBytes
}
if profile.PIDsMax > 0 && (combined.PIDsMax == 0 || profile.PIDsMax < combined.PIDsMax) {
combined.PIDsMax = profile.PIDsMax
}
if profile.IOReadBPS > 0 || profile.IOWriteBPS > 0 {
return fmt.Errorf("%w: Windows Job I/O rate control is not available in this build", supervisor.ErrUnsupported)
}
}
limits := winapi.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{}
limits.BasicLimitInformation.LimitFlags = winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
if combined.MemoryMaxBytes > 0 {
if uint64(uintptr(combined.MemoryMaxBytes)) != combined.MemoryMaxBytes {
return fmt.Errorf("%w: memory profile exceeds native pointer size", supervisor.ErrUnsupported)
}
limits.ProcessMemoryLimit = uintptr(combined.MemoryMaxBytes)
limits.BasicLimitInformation.LimitFlags |= winapi.JOB_OBJECT_LIMIT_PROCESS_MEMORY
}
if combined.PIDsMax > 0 {
if combined.PIDsMax > uint64(^uint32(0)) {
return fmt.Errorf("%w: process-count profile exceeds Windows limit", supervisor.ErrUnsupported)
}
limits.BasicLimitInformation.ActiveProcessLimit = uint32(combined.PIDsMax)
limits.BasicLimitInformation.LimitFlags |= winapi.JOB_OBJECT_LIMIT_ACTIVE_PROCESS
}
if _, err := winapi.SetInformationJobObject(job, winapi.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limits)), uint32(unsafe.Sizeof(limits))); err != nil {
return fmt.Errorf("apply Windows Job limits: %w", err)
}
if combined.CPUPercent > 0 {
// The config contract expresses CPU allowance as a percentage of one
// logical CPU (for example 200 means two logical CPUs). Windows Job
// CpuRate is hundredths of a percentage of the whole machine, so scale
// by the active processor count before applying the hard cap. A profile
// larger than this host is intentionally capped at the host capacity,
// which means it imposes no additional CPU restriction but remains a
// valid, atomically verified profile.
processors := uint64(winapi.GetActiveProcessorCount(winapi.ALL_PROCESSOR_GROUPS))
if processors == 0 {
return fmt.Errorf("%w: Windows did not report an active processor count", supervisor.ErrUnsupported)
}
if combined.CPUPercent > (^uint64(0)-processors+1)/100 {
return fmt.Errorf("%w: CPU profile %d%% overflows Windows Job rate conversion", supervisor.ErrUnsupported, combined.CPUPercent)
}
cpuRate := (combined.CPUPercent*100 + processors - 1) / processors
if cpuRate > 10000 {
cpuRate = 10000
}
cpu := struct {
ControlFlags uint32
CPUrate uint32
Weight uint32
}{ControlFlags: 0x1 | 0x4 /* ENABLE | HARD_CAP */, CPUrate: uint32(cpuRate)}
if _, err := winapi.SetInformationJobObject(job, winapi.JobObjectCpuRateControlInformation, uintptr(unsafe.Pointer(&cpu)), uint32(unsafe.Sizeof(cpu))); err != nil {
return fmt.Errorf("apply Windows Job CPU limit: %w", err)
}
var cpuReadback struct {
ControlFlags uint32
CPUrate uint32
Weight uint32
}
var cpuReturned uint32
if err := winapi.QueryInformationJobObject(job, int32(winapi.JobObjectCpuRateControlInformation), uintptr(unsafe.Pointer(&cpuReadback)), uint32(unsafe.Sizeof(cpuReadback)), &cpuReturned); err != nil {
return fmt.Errorf("verify Windows Job CPU limit: %w", err)
}
if cpuReadback.CPUrate != cpu.CPUrate || cpuReadback.ControlFlags&0x5 != 0x5 {
return errors.New("Windows Job CPU limit did not read back as requested")
}
}
// Read back every requested limit before authorization. This catches
// policy restrictions and unsupported Job implementations early.
var readback winapi.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
var returned uint32
if err := winapi.QueryInformationJobObject(job, int32(winapi.JobObjectExtendedLimitInformation), uintptr(unsafe.Pointer(&readback)), uint32(unsafe.Sizeof(readback)), &returned); err != nil {
return fmt.Errorf("verify Windows Job limits: %w", err)
}
if readback.BasicLimitInformation.LimitFlags&winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE == 0 {
return errors.New("Windows Job lost kill-on-close protection")
}
if combined.MemoryMaxBytes > 0 && uint64(readback.ProcessMemoryLimit) != combined.MemoryMaxBytes {
return errors.New("Windows Job memory limit did not read back as requested")
}
if combined.PIDsMax > 0 && uint64(readback.BasicLimitInformation.ActiveProcessLimit) != combined.PIDsMax {
return errors.New("Windows Job process limit did not read back as requested")
}
return nil
}
func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervisor.EffectiveIdentity, error) {
candidates, err := DiscoverActiveSessions()
if err != nil {
@@ -288,11 +417,39 @@ func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervi
candidates = nil
}
selection := Select(SelectionInput{Elevated: elevated, ActiveSessions: candidates, ActiveSystemAvailable: true, LocalServiceAvailable: true, LocalSystemAvailable: true})
attempted := make([]string, 0, len(selection.Attempts)+1)
details := make([]string, 0, len(selection.Attempts)+1)
addAttempt := func(contextName ExecutionContext, detail string) {
for _, existing := range attempted {
if existing == string(contextName) {
if detail != "" {
details = append(details, string(contextName)+": "+detail)
}
return
}
}
attempted = append(attempted, string(contextName))
if detail != "" {
details = append(details, string(contextName)+": "+detail)
}
}
for _, attempt := range selection.Attempts {
addAttempt(attempt.Context, string(attempt.Reason))
}
withEvidence := func(identity supervisor.EffectiveIdentity) supervisor.EffectiveIdentity {
identity.AttemptedContexts = append([]string(nil), attempted...)
identity.SelectionDetail = boundSelectionDetail(strings.Join(details, "; "))
return identity
}
rejection := func(cause error) error {
identity := withEvidence(supervisor.EffectiveIdentity{})
return &supervisor.StartError{Cause: cause, WindowsIdentity: identity.WindowsIdentity()}
}
if selection.Effective == nil {
if selection.Error != nil {
return 0, supervisor.EffectiveIdentity{}, selection.Error
return 0, supervisor.EffectiveIdentity{}, rejection(selection.Error)
}
return 0, supervisor.EffectiveIdentity{}, errors.New("Windows execution context selection failed")
return 0, supervisor.EffectiveIdentity{}, rejection(errors.New("Windows execution context selection failed"))
}
var selected *SessionCandidate
if selection.Effective.SessionID != nil {
@@ -306,10 +463,11 @@ func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervi
for _, attempt := range selection.Attempts {
token, identity, err := openTokenForAttempt(attempt.Context, selected)
if err == nil {
return token, identity, nil
return token, withEvidence(identity), nil
}
addAttempt(attempt.Context, "native preparation failed: "+err.Error())
if !elevated {
return 0, supervisor.EffectiveIdentity{}, err
return 0, supervisor.EffectiveIdentity{}, rejection(err)
}
}
// The pure selector stops as soon as ACTIVE_SYSTEM is available. A native
@@ -317,11 +475,22 @@ func (manager *execSupervisor) selectToken(elevated bool) (winapi.Token, supervi
// removed), so the final LOCAL_SYSTEM fallback is attempted here before
// launch preparation, never by retrying a created process.
if elevated {
addAttempt(ContextLocalSystem, "fallback")
if token, identity, err := openTokenForAttempt(ContextLocalSystem, nil); err == nil {
return token, identity, nil
return token, withEvidence(identity), nil
} else {
addAttempt(ContextLocalSystem, "native preparation failed: "+err.Error())
return 0, supervisor.EffectiveIdentity{}, rejection(err)
}
}
return 0, supervisor.EffectiveIdentity{}, errors.New("all Windows execution contexts failed before launch preparation")
return 0, supervisor.EffectiveIdentity{}, rejection(errors.New("all Windows execution contexts failed before launch preparation"))
}
func boundSelectionDetail(detail string) string {
if len(detail) <= 4096 {
return detail
}
return detail[:4096]
}
func openTokenForAttempt(contextName ExecutionContext, candidate *SessionCandidate) (winapi.Token, supervisor.EffectiveIdentity, error) {
@@ -350,12 +519,27 @@ func openTokenForAttempt(contextName ExecutionContext, candidate *SessionCandida
return 0, supervisor.EffectiveIdentity{}, err
}
identity.Context = string(ContextActiveSystem)
identity.SessionUserSID = candidate.UserSID
identity.LogonSID = candidate.LogonSID
return serviceToken, identity, nil
} else if token.IsElevated() {
// A full administrator token can be returned when UAC is disabled or
// policy supplies an already-unfiltered token. Normal commands must
// still run as that user without administrator authority; create a
// restricted medium token instead of silently falling back to a
// service identity.
restricted, err := createRestrictedMediumToken(token)
_ = token.Close()
return 0, supervisor.EffectiveIdentity{}, errors.New("active-user token is elevated and no restricted medium token was available")
if err != nil {
return 0, supervisor.EffectiveIdentity{}, err
}
token = restricted
}
identity := supervisor.EffectiveIdentity{Context: string(contextName), SessionID: candidate.SessionID, UserSID: candidate.UserSID, Elevated: contextName != ContextActiveUser, Integrity: map[bool]string{true: "high", false: "medium"}[contextName != ContextActiveUser]}
if err := verifyUserToken(token, candidate, contextName == ContextActiveUser); err != nil {
_ = token.Close()
return 0, supervisor.EffectiveIdentity{}, err
}
identity := supervisor.EffectiveIdentity{Context: string(contextName), SessionID: candidate.SessionID, SessionUserSID: candidate.UserSID, UserSID: candidate.UserSID, LogonSID: candidate.LogonSID, Elevated: contextName != ContextActiveUser, Integrity: map[bool]string{true: "high", false: "medium"}[contextName != ContextActiveUser]}
return token, identity, nil
case ContextLocalService:
token, err := logonLocalService()
@@ -370,6 +554,81 @@ func openTokenForAttempt(contextName ExecutionContext, candidate *SessionCandida
}
}
// createRestrictedMediumToken turns a full administrator token into the
// normal active-user token required for elevated=false. It disables all
// privileges, disables the built-in Administrators SID, and verifies medium
// integrity before the token is returned to the launch path.
func createRestrictedMediumToken(source winapi.Token) (winapi.Token, error) {
adminSID, err := winapi.CreateWellKnownSid(winapi.WinBuiltinAdministratorsSid)
if err != nil {
return 0, err
}
disabled := winapi.SIDAndAttributes{Sid: adminSID}
var restricted winapi.Token
result, _, callErr := procCreateRestrictedToken.Call(
uintptr(source), disableMaxPrivilege,
1, uintptr(unsafe.Pointer(&disabled)),
0, 0,
0, 0,
uintptr(unsafe.Pointer(&restricted)),
)
if result == 0 {
if callErr != syscall.Errno(0) {
return 0, callErr
}
return 0, syscall.GetLastError()
}
if err := setMediumIntegrity(restricted); err != nil {
_ = restricted.Close()
return 0, err
}
if restricted.IsElevated() {
_ = restricted.Close()
return 0, errors.New("restricted active-user token remained elevated")
}
return restricted, nil
}
func setMediumIntegrity(token winapi.Token) error {
mediumSID, err := winapi.StringToSid("S-1-16-8192")
if err != nil {
return err
}
sidLength := winapi.GetLengthSid(mediumSID)
headerSize := uint32(unsafe.Sizeof(winapi.Tokenmandatorylabel{}))
buffer := make([]byte, headerSize+sidLength)
label := (*winapi.Tokenmandatorylabel)(unsafe.Pointer(&buffer[0]))
label.Label.Sid = (*winapi.SID)(unsafe.Pointer(&buffer[headerSize]))
label.Label.Attributes = winapi.SE_GROUP_INTEGRITY | winapi.SE_GROUP_INTEGRITY_ENABLED
copy(buffer[headerSize:], unsafe.Slice((*byte)(unsafe.Pointer(mediumSID)), sidLength))
return winapi.SetTokenInformation(token, winapi.TokenIntegrityLevel, &buffer[0], uint32(len(buffer)))
}
func verifyUserToken(token winapi.Token, candidate *SessionCandidate, normal bool) error {
user, err := token.GetTokenUser()
if err != nil || user.User.Sid == nil {
if err != nil {
return err
}
return errors.New("active token has no user SID")
}
if user.User.Sid.String() != candidate.UserSID {
return errors.New("active token user SID changed during launch selection")
}
var sessionID uint32
var returned uint32
if err := winapi.GetTokenInformation(token, winapi.TokenSessionId, (*byte)(unsafe.Pointer(&sessionID)), uint32(unsafe.Sizeof(sessionID)), &returned); err != nil {
return err
}
if returned != uint32(unsafe.Sizeof(sessionID)) || sessionID != candidate.SessionID {
return errors.New("active token session changed during launch selection")
}
if normal && token.IsElevated() {
return errors.New("normal active-user token is elevated")
}
return nil
}
func duplicateServiceToken() (winapi.Token, supervisor.EffectiveIdentity, error) {
return duplicateServiceTokenForSession(0)
}
@@ -427,19 +686,66 @@ func (manager *execSupervisor) Signal(ctx context.Context, process supervisor.Pr
return supervisor.SignalOutcome{}, errors.New("unsupported signal")
}
if signal == supervisor.SignalTerm {
// The command has its own hidden console. A full AttachConsole/control
// helper is intentionally isolated from the Job kill path; if it is not
// available, the bounded grace period ends in an explicit Job kill.
// 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)
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
default:
}
}
select {
case <-ctx.Done():
return supervisor.SignalOutcome{}, ctx.Err()
case <-native.done:
return supervisor.SignalOutcome{Delivered: breakDelivered, Detail: "command exited after TERM", ObservedAt: manager.options.Now()}, nil
case <-time.After(manager.options.WindowsTermGrace):
}
}
if err := native.killFn(1); err != nil {
return supervisor.SignalOutcome{}, err
}
return supervisor.SignalOutcome{Delivered: true, Escalated: signal == supervisor.SignalTerm, Detail: "Windows Job terminated", ObservedAt: manager.options.Now()}, nil
detail := "Windows Job terminated"
if signal == supervisor.SignalTerm {
detail = "CTRL_BREAK grace expired; Windows Job terminated"
}
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.
func sendControlBreak(pid uint32) (bool, error) {
if pid == 0 {
return false, errors.New("command has no verified console PID")
}
if result, _, err := procAttachConsole.Call(uintptr(pid)); result == 0 {
if err == syscall.Errno(0) {
err = syscall.GetLastError()
}
return false, err
}
defer procFreeConsole.Call()
// Prevent the service/helper itself from acting on the generated event.
procSetCtrlHandler.Call(0, 1)
defer procSetCtrlHandler.Call(0, 0)
result, _, err := procGenerateCtrlEvent.Call(1 /* CTRL_BREAK_EVENT */, 0)
if result == 0 {
if err == syscall.Errno(0) {
err = syscall.GetLastError()
}
return false, err
}
return true, nil
}
func (manager *execSupervisor) Snapshot(ctx context.Context, process supervisor.Process) (supervisor.ResourceSnapshot, error) {
@@ -450,6 +756,14 @@ func (manager *execSupervisor) Snapshot(ctx context.Context, process supervisor.
if !ok || native.cmd == nil || native.cmd.Process == nil {
return supervisor.ResourceSnapshot{}, ErrProcessNotFound
}
if native.snapshotFn != nil {
select {
case <-ctx.Done():
return supervisor.ResourceSnapshot{}, ctx.Err()
default:
}
return native.snapshotFn()
}
select {
case <-ctx.Done():
return supervisor.ResourceSnapshot{}, ctx.Err()
@@ -458,6 +772,39 @@ func (manager *execSupervisor) Snapshot(ctx context.Context, process supervisor.
return supervisor.ResourceSnapshot{ProcessCount: 1, ObservedAt: manager.options.Now(), Complete: false, Detail: "Windows Job accounting is available after native completion integration"}, nil
}
type jobBasicAndIOAccounting struct {
TotalUserTime int64
TotalKernelTime int64
ThisPeriodTotalUserTime int64
ThisPeriodTotalKernelTime int64
TotalPageFaultCount uint32
TotalProcesses uint32
ActiveProcesses uint32
TotalTerminatedProcesses uint32
IO winapi.IO_COUNTERS
}
func queryJobSnapshot(job winapi.Handle, now time.Time) (supervisor.ResourceSnapshot, error) {
if job == 0 || job == winapi.InvalidHandle {
return supervisor.ResourceSnapshot{}, ErrProcessNotFound
}
var accounting jobBasicAndIOAccounting
var returned uint32
if err := winapi.QueryInformationJobObject(job, int32(winapi.JobObjectBasicAndIoAccountingInformation), uintptr(unsafe.Pointer(&accounting)), uint32(unsafe.Sizeof(accounting)), &returned); err != nil {
return supervisor.ResourceSnapshot{}, err
}
var limits winapi.JOBOBJECT_EXTENDED_LIMIT_INFORMATION
if err := winapi.QueryInformationJobObject(job, int32(winapi.JobObjectExtendedLimitInformation), uintptr(unsafe.Pointer(&limits)), uint32(unsafe.Sizeof(limits)), &returned); err != nil {
return supervisor.ResourceSnapshot{}, err
}
userKernel := accounting.TotalUserTime + accounting.TotalKernelTime
var cpu time.Duration
if userKernel > 0 && userKernel <= int64(^uint64(0)>>1)/100 {
cpu = time.Duration(userKernel) * 100 * time.Nanosecond
}
return supervisor.ResourceSnapshot{CPUTime: cpu, ResidentBytes: uint64(limits.PeakJobMemoryUsed), IOReadBytes: accounting.IO.ReadTransferCount, IOWriteBytes: accounting.IO.WriteTransferCount, ProcessCount: uint64(accounting.ActiveProcesses), ObservedAt: now, Complete: accounting.ActiveProcesses == 0, Detail: "Windows Job accounting"}, nil
}
func (manager *execSupervisor) StopAll(_ context.Context) error {
manager.mu.Lock()
processes := make([]*execProcess, 0, len(manager.active))