Files
CyberStrikeAI/internal/processguard/platform_windows.go
T

184 lines
5.3 KiB
Go

//go:build windows
package processguard
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"runtime"
"sync"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
func configurePlatform(o *Options) error {
if o.CgroupRoot != "" {
return fmt.Errorf("cgroups are Linux-only")
}
if o.Mode == "process_group" {
return fmt.Errorf("Windows tasks require Job Object containment")
}
return nil
}
func configureGuardian(cmd *exec.Cmd) {
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP}
}
func gatedChildMain(args []string) error {
return fmt.Errorf("Unix launch gates are unavailable on Windows")
}
type jobGroup struct {
mu sync.Mutex
job windows.Handle
parent windows.Handle
watcher *watchdog
closed bool
broken bool
}
func newPlatformGroup(id string, o Options) (Group, error) {
if err := configurePlatform(&o); err != nil {
return nil, err
}
name := "Local\\CyberStrikeAI-" + id
g := &jobGroup{}
w, err := startWatchdog(watchRequest{Name: name, Options: o}, func() {
g.mu.Lock()
defer g.mu.Unlock()
g.broken = true
if !g.closed && g.job != 0 {
_ = windows.TerminateJobObject(g.job, 1)
}
})
if err != nil {
return nil, err
}
fail := func(err error) (Group, error) { w.close(); return nil, err }
namePtr, err := windows.UTF16PtrFromString(name)
if err != nil {
return fail(err)
}
proc := windows.NewLazySystemDLL("kernel32.dll").NewProc("OpenJobObjectW")
h, _, callErr := proc.Call(0x0004|0x0008, 0, uintptr(unsafe.Pointer(namePtr)))
if h == 0 {
return fail(callErr)
}
parent, err := windows.OpenProcess(windows.PROCESS_CREATE_PROCESS|windows.PROCESS_DUP_HANDLE, false, uint32(w.cmd.Process.Pid))
if err != nil {
windows.CloseHandle(windows.Handle(h))
return fail(err)
}
g.mu.Lock()
defer g.mu.Unlock()
if g.broken {
windows.CloseHandle(parent)
windows.CloseHandle(windows.Handle(h))
return fail(fmt.Errorf("job guardian exited during setup"))
}
g.job = windows.Handle(h)
g.parent = parent
g.watcher = w
return g, nil
}
func (g *jobGroup) Name() string { return "windows_job" }
func (g *jobGroup) Prepare(cmd *exec.Cmd) (*Launch, error) {
g.mu.Lock()
defer g.mu.Unlock()
if g.closed || g.broken {
return nil, fmt.Errorf("job is closed")
}
if _, err := g.watcher.send(watchRequest{Op: "ping"}); err != nil {
return nil, err
}
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
// Windows inherits the job at CreateProcess time from this parent. The
// guardian joined the job BEFORE acknowledging readiness, closing the
// Start-then-Assign race and its suspended-process crash window.
cmd.SysProcAttr.ParentProcess = syscall.Handle(g.parent)
return &Launch{Commit: func() error { return nil }, Dispose: func() {}}, nil
}
func (g *jobGroup) Release(pid int) error { return nil }
func (g *jobGroup) Close(ctx context.Context) error {
g.mu.Lock()
defer g.mu.Unlock()
if g.closed {
return nil
}
if err := windows.TerminateJobObject(g.job, 1); err != nil {
return err
}
type accounting struct {
TotalUser, TotalKernel, PeriodUser, PeriodKernel int64
PageFaults, TotalProcesses, ActiveProcesses, Terminated uint32
}
for {
var info accounting
if err := windows.QueryInformationJobObject(g.job, windows.JobObjectBasicAccountingInformation, uintptr(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info)), nil); err != nil {
return err
}
if info.ActiveProcesses == 0 {
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(10 * time.Millisecond):
}
}
g.closed = true
return errors.Join(g.watcher.close(), windows.CloseHandle(g.parent), windows.CloseHandle(g.job))
}
func guardianMain(dec *json.Decoder, enc *json.Encoder) error {
var req watchRequest
if err := dec.Decode(&req); err != nil {
return err
}
name, err := windows.UTF16PtrFromString(req.Name)
if err != nil {
return err
}
job, err := windows.CreateJobObject(nil, name)
if err != nil {
return err
}
defer windows.CloseHandle(job)
limits := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{}
limits.BasicLimitInformation.LimitFlags = windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | windows.JOB_OBJECT_LIMIT_ACTIVE_PROCESS | windows.JOB_OBJECT_LIMIT_JOB_MEMORY
limits.BasicLimitInformation.ActiveProcessLimit = uint32(req.Options.MaxProcesses + 1)
limits.JobMemoryLimit = uintptr(req.Options.MemoryMaxBytes)
if _, err = windows.SetInformationJobObject(job, windows.JobObjectExtendedLimitInformation, uintptr(unsafe.Pointer(&limits)), uint32(unsafe.Sizeof(limits))); err != nil {
return err
}
if req.Options.CPUQuotaMicros > 0 {
rate := req.Options.CPUQuotaMicros / 10 / int64(runtime.NumCPU())
if rate < 1 {
rate = 1
}
if rate > 10000 {
rate = 10000
}
cpu := struct{ Flags, Rate uint32 }{Flags: 1 | 4, Rate: uint32(rate)}
if _, err = windows.SetInformationJobObject(job, windows.JobObjectCpuRateControlInformation, uintptr(unsafe.Pointer(&cpu)), uint32(unsafe.Sizeof(cpu))); err != nil {
return err
}
}
if err = windows.AssignProcessToJobObject(job, windows.CurrentProcess()); err != nil {
return err
}
return serveGuardian(dec, enc, func(req watchRequest) error {
if req.Op != "ping" {
return fmt.Errorf("unknown guardian command")
}
return nil
}, func() error { return windows.TerminateJobObject(job, uint32(os.Getpid())) })
}