mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-17 23:22:27 +02:00
314 lines
8.2 KiB
Go
314 lines
8.2 KiB
Go
//go:build linux
|
|
|
|
package processguard
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
var rootLock *os.File // retained until server exit; never inherited by commands
|
|
|
|
func configurePlatform(o *Options) error {
|
|
if o.CgroupRoot == "" {
|
|
if o.Mode == "required" {
|
|
return fmt.Errorf("required isolation needs security.process_isolation.cgroup_root")
|
|
}
|
|
return nil
|
|
}
|
|
if o.Mode == "process_group" {
|
|
return fmt.Errorf("cgroup_root cannot be combined with process_group mode")
|
|
}
|
|
if o.CgroupRoot == "auto" {
|
|
data, err := os.ReadFile("/proc/self/cgroup")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
if strings.HasPrefix(line, "0::") {
|
|
o.CgroupRoot = filepath.Join("/sys/fs/cgroup", strings.TrimPrefix(line, "0::"))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
root, err := validateRoot(o.CgroupRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.CgroupRoot = root
|
|
// An exclusive host-side lock prevents one server's recovery sweep from
|
|
// killing tasks owned by another server using the same delegated root.
|
|
hash := sha256.Sum256([]byte(root))
|
|
lockPath := filepath.Join(os.TempDir(), fmt.Sprintf("cyberstrike-cgroup-%d-%x.lock", os.Getuid(), hash[:12]))
|
|
fd, err := unix.Open(lockPath, unix.O_CREAT|unix.O_RDWR|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lock := os.NewFile(uintptr(fd), lockPath)
|
|
if err = unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB); err != nil {
|
|
lock.Close()
|
|
return fmt.Errorf("cgroup root is already owned: %w", err)
|
|
}
|
|
success := false
|
|
defer func() {
|
|
if !success {
|
|
lock.Close()
|
|
}
|
|
}()
|
|
// cgroup v2 requires the delegated parent to have no processes before
|
|
// domain controllers can be enabled. Move only this server, never outsiders.
|
|
data, err := os.ReadFile(filepath.Join(root, "cgroup.procs"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, pid := range strings.Fields(string(data)) {
|
|
if pid != strconv.Itoa(os.Getpid()) {
|
|
return fmt.Errorf("delegated root contains another process %s", pid)
|
|
}
|
|
}
|
|
if len(strings.Fields(string(data))) > 0 {
|
|
supervisor := filepath.Join(root, "supervisor")
|
|
if err = os.Mkdir(supervisor, 0700); err != nil && !os.IsExist(err) {
|
|
return err
|
|
}
|
|
if err = os.WriteFile(filepath.Join(supervisor, "cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0600); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err = os.WriteFile(filepath.Join(root, "cgroup.subtree_control"), []byte("+cpu +memory +pids"), 0600); err != nil {
|
|
return fmt.Errorf("delegate cpu, memory and pids controllers: %w", err)
|
|
}
|
|
// Recover only our names under the exclusively owned root. No PID replay.
|
|
entries, err := os.ReadDir(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries {
|
|
if entry.IsDir() && validTaskName(entry.Name()) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
err = killAndRemoveCgroup(ctx, filepath.Join(root, entry.Name()))
|
|
cancel()
|
|
if err != nil {
|
|
return fmt.Errorf("recover %s: %w", entry.Name(), err)
|
|
}
|
|
}
|
|
}
|
|
rootLock = lock
|
|
success = true
|
|
return nil
|
|
}
|
|
|
|
func validateRoot(root string) (string, error) {
|
|
if !filepath.IsAbs(root) {
|
|
return "", fmt.Errorf("cgroup root must be absolute")
|
|
}
|
|
root = filepath.Clean(root)
|
|
resolved, err := filepath.EvalSymlinks(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if root != resolved || root == "/sys/fs/cgroup" || root == "/" {
|
|
return "", fmt.Errorf("use a dedicated delegated cgroup, not the hierarchy root or a symlink")
|
|
}
|
|
var st unix.Statfs_t
|
|
if err = unix.Statfs(root, &st); err != nil {
|
|
return "", err
|
|
}
|
|
if st.Type != unix.CGROUP2_SUPER_MAGIC {
|
|
return "", fmt.Errorf("%s is not cgroup v2", root)
|
|
}
|
|
return root, nil
|
|
}
|
|
func validTaskName(name string) bool {
|
|
if !strings.HasPrefix(name, "task-") || len(name) != 41 {
|
|
return false
|
|
}
|
|
for _, c := range name[5:] {
|
|
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c == '-') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type cgroupGroup struct {
|
|
mu sync.Mutex
|
|
path string
|
|
dir *os.File
|
|
watcher *watchdog
|
|
closed bool
|
|
}
|
|
|
|
func newPlatformGroup(id string, o Options) (Group, error) {
|
|
if o.CgroupRoot == "" {
|
|
if o.Mode == "required" {
|
|
return nil, fmt.Errorf("required isolation has no delegated cgroup root")
|
|
}
|
|
return newUnixGroup()
|
|
}
|
|
root, err := validateRoot(o.CgroupRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
name := "task-" + id
|
|
if !validTaskName(name) {
|
|
return nil, fmt.Errorf("invalid task run ID")
|
|
}
|
|
path := filepath.Join(root, name)
|
|
if err = os.Mkdir(path, 0700); err != nil {
|
|
return nil, err
|
|
}
|
|
success := false
|
|
defer func() {
|
|
if !success {
|
|
_ = os.Remove(path)
|
|
}
|
|
}()
|
|
limits := map[string]string{"pids.max": strconv.Itoa(o.MaxProcesses), "memory.max": strconv.FormatInt(o.MemoryMaxBytes, 10), "memory.oom.group": "1"}
|
|
if o.CPUQuotaMicros > 0 {
|
|
limits["cpu.max"] = fmt.Sprintf("%d 100000", o.CPUQuotaMicros)
|
|
}
|
|
for file, value := range limits {
|
|
if err = os.WriteFile(filepath.Join(path, file), []byte(value), 0600); err != nil {
|
|
return nil, fmt.Errorf("set %s: %w", file, err)
|
|
}
|
|
}
|
|
if _, err = os.Stat(filepath.Join(path, "cgroup.kill")); err != nil {
|
|
return nil, fmt.Errorf("cgroup.kill requires Linux 5.14+: %w", err)
|
|
}
|
|
dir, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
g := &cgroupGroup{path: path, dir: dir}
|
|
w, err := startWatchdog(watchRequest{Name: "cgroup", Path: path}, func() {
|
|
// A guardian crash is also fail-closed while the owner is still alive.
|
|
_ = os.WriteFile(filepath.Join(path, "cgroup.kill"), []byte("1"), 0600)
|
|
})
|
|
if err != nil {
|
|
dir.Close()
|
|
return nil, err
|
|
}
|
|
g.watcher = w
|
|
success = true
|
|
return g, nil
|
|
}
|
|
func (g *cgroupGroup) Name() string { return "cgroup_v2" }
|
|
func (g *cgroupGroup) Prepare(cmd *exec.Cmd) (*Launch, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if g.closed {
|
|
return nil, fmt.Errorf("cgroup is closed")
|
|
}
|
|
if _, err := g.watcher.send(watchRequest{Op: "ping"}); err != nil {
|
|
return nil, err
|
|
}
|
|
if cmd.SysProcAttr == nil {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
// clone3(CLONE_INTO_CGROUP), not a racy write of a newly started PID.
|
|
cmd.SysProcAttr.UseCgroupFD = true
|
|
cmd.SysProcAttr.CgroupFD = int(g.dir.Fd())
|
|
return &Launch{Commit: func() error { return nil }, Dispose: func() {}}, nil
|
|
}
|
|
func (g *cgroupGroup) Release(pid int) error { return nil }
|
|
func (g *cgroupGroup) Close(ctx context.Context) error {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
g.closed = true
|
|
if g.dir == nil {
|
|
return nil
|
|
}
|
|
if err := killAndRemoveCgroup(ctx, g.path); err != nil {
|
|
return err
|
|
}
|
|
watchErr := g.watcher.close()
|
|
err := errors.Join(watchErr, g.dir.Close())
|
|
g.dir = nil
|
|
return err
|
|
}
|
|
func killAndRemoveCgroup(ctx context.Context, path string) error {
|
|
if err := os.WriteFile(filepath.Join(path, "cgroup.kill"), []byte("1"), 0600); err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
for {
|
|
data, err := os.ReadFile(filepath.Join(path, "cgroup.events"))
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.Contains(string(data), "populated 0") {
|
|
return removeCgroupTree(path)
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(10 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
func removeCgroupTree(path string) error {
|
|
entries, err := os.ReadDir(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, e := range entries {
|
|
if e.IsDir() {
|
|
if err = removeCgroupTree(filepath.Join(path, e.Name())); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
err = os.Remove(path)
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
func guardianMain(dec *json.Decoder, enc *json.Encoder) error {
|
|
var req watchRequest
|
|
if err := dec.Decode(&req); err != nil {
|
|
return err
|
|
}
|
|
if req.Name == "process_group" {
|
|
return groupGuardian(dec, enc)
|
|
}
|
|
if req.Name != "cgroup" || !validTaskName(filepath.Base(req.Path)) {
|
|
return fmt.Errorf("invalid cgroup guardian")
|
|
}
|
|
if _, err := validateRoot(req.Path); err != nil {
|
|
return err
|
|
}
|
|
return serveGuardian(dec, enc, func(r watchRequest) error {
|
|
if r.Op != "ping" {
|
|
return fmt.Errorf("unknown command")
|
|
}
|
|
return nil
|
|
}, func() error {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
return killAndRemoveCgroup(ctx, req.Path)
|
|
})
|
|
}
|