carlos / platform Public

Add strict pinned cgroup containment for maintenance commands

Paul Campbell pushed by paul@keymail.dev cd7e91775061e57b2e5636cf796599954cbfafba parent 760edce
5 files changed, +972 −0
  • internal/cgroup/strict.go +10 −0
  • internal/cgroup/strict_kernel_linux_test.go +343 −0
  • internal/cgroup/strict_linux.go +356 −0
  • internal/cgroup/strict_linux_test.go +246 −0
  • internal/cgroup/strict_other.go +17 −0
diff --git a/internal/cgroup/strict.go b/internal/cgroup/strict.go
new file mode 100644
index 000000000..f8e071ece
--- /dev/null
+++ b/internal/cgroup/strict.go
@@ -0,0 +1,10 @@
+package cgroup
+
+import "errors"
+
+var (
+ ErrStrictUnsupported = errors.New("cgroup: strict containment unsupported")
+ ErrStrictRefused = errors.New("cgroup: strict containment refused")
+ ErrStrictState = errors.New("cgroup: strict containment state refused")
+ ErrStrictDeadline = errors.New("cgroup: strict containment requires a live deadline")
+)
diff --git a/internal/cgroup/strict_kernel_linux_test.go b/internal/cgroup/strict_kernel_linux_test.go
new file mode 100644
index 000000000..922e09764
--- /dev/null
+++ b/internal/cgroup/strict_kernel_linux_test.go
@@ -0,0 +1,343 @@
+//go:build linux
+
+package cgroup
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+type strictKernelReceipt struct {
+ PID int `json:"pid"`
+ Group string `json:"group"`
+ Child int `json:"child,omitempty"`
+}
+
+func strictKernelGroup(t *testing.T) string {
+ t.Helper()
+ raw, e := os.ReadFile("/proc/self/cgroup")
+ if e != nil {
+ t.Fatal(e)
+ }
+ group, ok := ParseSelfCgroup(string(raw))
+ if !ok {
+ t.Fatal("no unified group")
+ }
+ return "/" + group
+}
+func strictKernelSave(t *testing.T, name string, child int) {
+ t.Helper()
+ raw, e := json.Marshal(strictKernelReceipt{os.Getpid(), strictKernelGroup(t), child})
+ if e != nil {
+ t.Fatal(e)
+ }
+ if e = os.WriteFile(filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), name+".json"), raw, 0600); e != nil {
+ t.Fatal(e)
+ }
+}
+func strictKernelCommand(mode string) *exec.Cmd {
+ cmd := exec.Command(os.Args[0], "-test.run=^TestStrictKernelChild$", "-test.count=1")
+ cmd.Env = []string{"CARLOS_STRICT_CHILD=" + mode, "CARLOS_STRICT_PROOF_DIR=" + os.Getenv("CARLOS_STRICT_PROOF_DIR")}
+ return cmd
+}
+func TestStrictKernelChild(t *testing.T) {
+ mode := os.Getenv("CARLOS_STRICT_CHILD")
+ if mode == "" {
+ t.Skip("synthetic child only")
+ }
+ if mode == "grandchild" {
+ strictKernelSave(t, "grandchild", 0)
+ time.Sleep(time.Minute)
+ return
+ }
+ if mode == "marker" {
+ strictKernelSave(t, "marker", 0)
+ return
+ }
+ child := strictKernelCommand("grandchild")
+ child.Stdout = os.Stdout
+ child.Stderr = os.Stderr
+ if e := child.Start(); e != nil {
+ t.Fatal(e)
+ }
+ strictKernelSave(t, "worker", child.Process.Pid)
+ if mode == "exit-with-descendant" {
+ return
+ }
+ time.Sleep(time.Minute)
+}
+func strictKernelAwait(t *testing.T, name string) strictKernelReceipt {
+ t.Helper()
+ path := filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), name+".json")
+ deadline := time.Now().Add(3 * time.Second)
+ for time.Now().Before(deadline) {
+ raw, e := os.ReadFile(path)
+ if e == nil {
+ var r strictKernelReceipt
+ if json.Unmarshal(raw, &r) == nil {
+ return r
+ }
+ }
+ time.Sleep(5 * time.Millisecond)
+ }
+ t.Fatal("kernel child receipt missing", name)
+ return strictKernelReceipt{}
+}
+
+// The local user unit delegates memory but not io. This scaffold moves the
+// probe into a supervisor leaf and enables only the controller strict mode uses.
+func strictKernelManager(t *testing.T) *Manager {
+ t.Helper()
+ root := filepath.Join("/sys/fs/cgroup", strictKernelGroup(t))
+ leaf := filepath.Join(root, "supervise")
+ if e := os.Mkdir(leaf, 0755); e != nil {
+ return nil
+ }
+ if e := os.WriteFile(filepath.Join(leaf, "cgroup.procs"), []byte(strconv.Itoa(os.Getpid())), 0); e != nil {
+ return nil
+ }
+ if e := os.WriteFile(filepath.Join(root, "cgroup.subtree_control"), []byte("+memory"), 0); e != nil {
+ return nil
+ }
+ return &Manager{root: root, logf: t.Logf}
+}
+
+func TestStrictKernel(t *testing.T) {
+ mode := os.Getenv("CARLOS_STRICT_KERNEL")
+ if mode == "" {
+ t.Skip("requires disposable delegated user-systemd service")
+ }
+ m := strictKernelManager(t)
+ if mode == "permission-denied" {
+ if m != nil {
+ t.Fatal("read-only cgroup delegation accepted")
+ }
+ if p, e := m.PrepareStrict("denied.example", TierDefault); e == nil || p != nil {
+ t.Fatal("missing delegation became ordinary spawn")
+ }
+ return
+ }
+ if m == nil {
+ t.Fatal("disposable service has no cgroup delegation")
+ }
+ if mode == "unsupported" {
+ p, e := m.PrepareStrict("unsupported.example", TierDefault)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if e = p.Start(strictKernelCommand("marker")); e != ErrStrictUnsupported {
+ t.Fatal("blocked clone3 was not unsupported", e)
+ }
+ if e = p.Start(strictKernelCommand("marker")); e != ErrStrictState {
+ t.Fatal("blocked clone retried", e)
+ }
+ if _, e = os.Stat(filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), "marker.json")); e == nil {
+ t.Fatal("fallback executed marker")
+ }
+ if e = p.Close(); e != nil {
+ t.Fatal(e)
+ }
+ return
+ }
+ t.Run("concurrent-reservation", func(t *testing.T) {
+ type result struct {
+ p *Prepared
+ err error
+ }
+ results := make(chan result, 12)
+ begin := make(chan struct{})
+ for range 12 {
+ go func() {
+ <-begin
+ p, err := (&Manager{root: m.root}).PrepareStrict("concurrent.example", TierDefault)
+ results <- result{p, err}
+ }()
+ }
+ close(begin)
+ var winner *Prepared
+ for range 12 {
+ r := <-results
+ if r.err == nil {
+ if winner != nil {
+ t.Fatal("two concurrent preparations won")
+ }
+ winner = r.p
+ } else if r.err != ErrStrictState {
+ t.Fatal(r.err)
+ }
+ }
+ if winner == nil {
+ t.Fatal("no preparation won")
+ }
+ if err := winner.Close(); err != nil {
+ t.Fatal(err)
+ }
+ })
+ t.Run("descendant-pipe-and-reservation", func(t *testing.T) {
+ host := "tree.example"
+ p, e := m.PrepareStrict(host, TierDefault)
+ if e != nil {
+ t.Fatal(e)
+ }
+ other := &Manager{root: m.root}
+ if q, e := other.PrepareStrict(host, TierDefault); e != ErrStrictState || q != nil {
+ t.Fatal("second manager reserved same group", e)
+ }
+ read, write, e := os.Pipe()
+ if e != nil {
+ t.Fatal(e)
+ }
+ defer read.Close()
+ defer write.Close()
+ cmd := strictKernelCommand("exit-with-descendant")
+ cmd.Stdout = write
+ if e = p.Start(cmd); e != nil {
+ t.Fatal(e)
+ }
+ write.Close()
+ worker := strictKernelAwait(t, "worker")
+ child := strictKernelAwait(t, "grandchild")
+ expected := strings.TrimPrefix(m.Root(), "/sys/fs/cgroup") + "/" + ExecName(host)
+ if worker.Group != expected || child.Group != expected || worker.Child != child.PID {
+ t.Fatal("child escaped prepared cgroup")
+ }
+ if e = cmd.Wait(); e != nil {
+ t.Fatal(e)
+ }
+ drained := make(chan struct{})
+ go func() { io.Copy(io.Discard, read); close(drained) }()
+ select {
+ case <-drained:
+ t.Fatal("descendant did not retain output pipe")
+ case <-time.After(20 * time.Millisecond):
+ }
+ if e = p.Close(); e != ErrStrictRefused {
+ t.Fatal("live descendant handle discarded", e)
+ }
+ ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
+ defer cancel()
+ if e = p.KillAndEmpty(ctx); e != nil {
+ t.Fatal(e)
+ }
+ select {
+ case <-drained:
+ case <-ctx.Done():
+ t.Fatal("empty proof left descendant output blocked")
+ }
+ if populated, e := strictPopulation(p.events); e != nil || populated {
+ t.Fatal("kernel population remained", e)
+ }
+ if e = p.Close(); e != nil {
+ t.Fatal(e)
+ }
+ q, e := other.PrepareStrict(host, TierDefault)
+ if e != nil {
+ t.Fatal("closed reservation was not released", e)
+ }
+ if e = q.Close(); e != nil {
+ t.Fatal(e)
+ }
+ })
+ t.Run("kernel-limit-readback", func(t *testing.T) {
+ for i, limit := range []Limit{TierDefault, TierChildren} {
+ p, e := m.PrepareStrict("limit"+strconv.Itoa(i)+".example", limit)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if e = p.limitsOK(); e != nil {
+ t.Fatal(e)
+ }
+ if e = strictWrite(p.limits["memory.oom.group"], "0"); e != nil {
+ t.Fatal(e)
+ }
+ if e = p.Start(strictKernelCommand("marker")); e != ErrStrictRefused {
+ t.Fatal("changed actual kernel limit reached spawn", e)
+ }
+ if e = p.Close(); e != nil {
+ t.Fatal(e)
+ }
+ }
+ })
+
+ t.Run("permission-denied-controls", func(t *testing.T) {
+ host := "permissions.example"
+ p, e := m.PrepareStrict(host, TierDefault)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if e = p.Close(); e != nil {
+ t.Fatal(e)
+ }
+ for _, name := range []string{"cgroup.kill", "cgroup.events", "memory.max"} {
+ path := filepath.Join(m.root, ExecName(host), name)
+ info, e := os.Stat(path)
+ if e != nil {
+ t.Fatal(e)
+ }
+ if e = os.Chmod(path, 0); e != nil {
+ t.Fatal(e)
+ }
+ candidate, prepareErr := m.PrepareStrict(host, TierDefault)
+ if e = os.Chmod(path, info.Mode().Perm()); e != nil {
+ t.Fatal(e)
+ }
+ if prepareErr == nil || candidate != nil {
+ t.Fatal("unusable required control accepted", name)
+ }
+ }
+ })
+ t.Run("missing-memory-controller", func(t *testing.T) {
+ path := filepath.Join(m.root, "cgroup.subtree_control")
+ if e := os.WriteFile(path, []byte("-memory"), 0); e != nil {
+ t.Fatal(e)
+ }
+ defer func() {
+ if e := os.WriteFile(path, []byte("+memory"), 0); e != nil {
+ t.Error(e)
+ }
+ }()
+ if p, e := m.PrepareStrict("missing.example", TierDefault); e == nil || p != nil {
+ t.Fatal("missing memory limit interface accepted")
+ }
+ })
+}
+func TestStrictKernelSupervisor(t *testing.T) {
+ if os.Getenv("CARLOS_STRICT_SUPERVISOR") != "1" {
+ t.Skip("disposable unit parent-death proof")
+ }
+ if _, e := os.Stat(filepath.Join(os.Getenv("CARLOS_STRICT_PROOF_DIR"), "supervisor.json")); e == nil {
+ strictKernelSave(t, "restarted", 0)
+ time.Sleep(time.Minute)
+ return
+ }
+ m := strictKernelManager(t)
+ if m == nil {
+ t.Fatal("no delegation")
+ }
+ p, e := m.PrepareStrict("parent.example", TierDefault)
+ if e != nil {
+ t.Fatal(e)
+ }
+ cmd := strictKernelCommand("tree")
+ read, write, e := os.Pipe()
+ if e != nil {
+ t.Fatal(e)
+ }
+ defer read.Close()
+ cmd.Stdout = write
+ if e = p.Start(cmd); e != nil {
+ t.Fatal(e)
+ }
+ write.Close()
+ strictKernelSave(t, "supervisor", cmd.Process.Pid)
+ io.Copy(io.Discard, read)
+ t.Fatal("parent-death probe returned before supervisor was killed")
+}
diff --git a/internal/cgroup/strict_linux.go b/internal/cgroup/strict_linux.go
new file mode 100644
index 000000000..7d365038e
--- /dev/null
+++ b/internal/cgroup/strict_linux.go
@@ -0,0 +1,356 @@
+//go:build linux
+
+package cgroup
+
+import (
+ "context"
+ "errors"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/carlosframework/platform/internal/controlname"
+ "golang.org/x/sys/unix"
+)
+
+type strictIdentity struct{ device, inode uint64 }
+
+var strictReservations = struct {
+ sync.Mutex
+ held map[strictIdentity]bool
+}{held: make(map[strictIdentity]bool)}
+
+// Prepared pins one empty host cgroup. It grants no lease, fence or database
+// authority. The caller must also obtain Cmd.Wait evidence after cleanup.
+type Prepared struct{ *strictPreparedState }
+
+type strictPreparedState struct {
+ mu sync.Mutex
+ root, dir, kill, events *os.File
+ limits map[string]*os.File
+ expected map[string]string
+ identity strictIdentity
+ host string
+ attempted, empty, closed bool
+ start func(*exec.Cmd) error
+}
+
+func strictOpen(dir *os.File, name string, flags int) (*os.File, error) {
+ fd, e := unix.Openat(int(dir.Fd()), name, flags|unix.O_CLOEXEC|unix.O_NOFOLLOW, 0)
+ if e != nil {
+ return nil, e
+ }
+ return os.NewFile(uintptr(fd), name), nil
+}
+func strictID(f *os.File) (strictIdentity, error) {
+ var st unix.Stat_t
+ if e := unix.Fstat(int(f.Fd()), &st); e != nil {
+ return strictIdentity{}, e
+ }
+ return strictIdentity{uint64(st.Dev), st.Ino}, nil
+}
+func strictRead(f *os.File) (string, error) {
+ if _, e := f.Seek(0, 0); e != nil {
+ return "", e
+ }
+ b, e := io.ReadAll(io.LimitReader(f, 4097))
+ if e != nil || len(b) > 4096 {
+ return "", ErrStrictRefused
+ }
+ return strings.TrimSpace(string(b)), nil
+}
+func strictWrite(f *os.File, value string) error {
+ if _, e := f.Seek(0, 0); e != nil {
+ return e
+ }
+ n, e := f.WriteString(value)
+ if e != nil {
+ return e
+ }
+ if n != len(value) {
+ return io.ErrShortWrite
+ }
+ return nil
+}
+func strictPopulation(f *os.File) (bool, error) {
+ raw, e := strictRead(f)
+ if e != nil {
+ return false, e
+ }
+ seen := false
+ populated := false
+ for _, line := range strings.Split(raw, "\n") {
+ parts := strings.Fields(line)
+ if len(parts) != 2 {
+ return false, ErrStrictRefused
+ }
+ if parts[0] == "populated" {
+ if seen || (parts[1] != "0" && parts[1] != "1") {
+ return false, ErrStrictRefused
+ }
+ seen = true
+ populated = parts[1] == "1"
+ }
+ }
+ if !seen {
+ return false, ErrStrictRefused
+ }
+ return populated, nil
+}
+func (p *Prepared) identityOK() error {
+ if p.closed {
+ return ErrStrictState
+ }
+ id, e := strictID(p.dir)
+ if e != nil || id != p.identity {
+ return ErrStrictRefused
+ }
+ var st unix.Stat_t
+ if e = unix.Fstatat(int(p.root.Fd()), ExecName(p.host), &st, unix.AT_SYMLINK_NOFOLLOW); e != nil || st.Mode&unix.S_IFMT != unix.S_IFDIR || (strictIdentity{uint64(st.Dev), st.Ino}) != p.identity {
+ return ErrStrictRefused
+ }
+ return nil
+}
+func (p *Prepared) limitsOK() error {
+ for name, want := range p.expected {
+ got, e := strictRead(p.limits[name])
+ if e != nil || got != want {
+ return ErrStrictRefused
+ }
+ }
+ return nil
+}
+func (p *Prepared) closeFiles() {
+ for _, f := range p.limits {
+ f.Close()
+ }
+ for _, f := range []*os.File{p.events, p.kill, p.dir, p.root} {
+ if f != nil {
+ f.Close()
+ }
+ }
+}
+
+// PrepareStrict never uses accounting's fallback. Only the two existing tiers
+// are accepted; a zero or missing limit cannot create an uncapped command.
+func (m *Manager) PrepareStrict(host string, limit Limit) (out *Prepared, err error) {
+ if m == nil || m.root == "" {
+ return nil, ErrStrictUnsupported
+ }
+ if !controlname.ValidHostShape(host) || (limit != TierDefault && limit != TierChildren) {
+ return nil, ErrStrictRefused
+ }
+ own, e := os.ReadFile("/proc/self/cgroup")
+ rel, ok := ParseSelfCgroup(string(own))
+ if e != nil || !ok {
+ return nil, ErrStrictUnsupported
+ }
+ current := filepath.Join("/sys/fs/cgroup", rel)
+ rootPath := filepath.Clean(m.root)
+ if current != rootPath && current != filepath.Join(rootPath, "supervise") {
+ return nil, ErrStrictRefused
+ }
+ root, e := os.OpenFile(filepath.Clean(m.root), os.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW, 0)
+ if e != nil {
+ return nil, ErrStrictUnsupported
+ }
+ p := &Prepared{strictPreparedState: &strictPreparedState{root: root, host: host, limits: map[string]*os.File{}, start: func(cmd *exec.Cmd) error { return cmd.Start() }}}
+ reserved := false
+ defer func() {
+ if err != nil {
+ p.closeFiles()
+ if reserved {
+ strictReservations.Lock()
+ delete(strictReservations.held, p.identity)
+ strictReservations.Unlock()
+ }
+ }
+ }()
+ var fs unix.Statfs_t
+ if e = unix.Fstatfs(int(root.Fd()), &fs); e != nil || uint64(fs.Type) != uint64(unix.CGROUP2_SUPER_MAGIC) {
+ return nil, ErrStrictUnsupported
+ }
+ if e = unix.Mkdirat(int(root.Fd()), ExecName(host), 0755); e != nil && !errors.Is(e, unix.EEXIST) {
+ return nil, ErrStrictRefused
+ }
+ p.dir, e = strictOpen(root, ExecName(host), unix.O_RDONLY|unix.O_DIRECTORY)
+ if e != nil {
+ return nil, ErrStrictRefused
+ }
+ p.identity, e = strictID(p.dir)
+ if e != nil {
+ return nil, ErrStrictRefused
+ }
+ strictReservations.Lock()
+ if strictReservations.held[p.identity] {
+ strictReservations.Unlock()
+ return nil, ErrStrictState
+ }
+ strictReservations.held[p.identity] = true
+ reserved = true
+ strictReservations.Unlock()
+ p.events, e = strictOpen(p.dir, "cgroup.events", unix.O_RDONLY)
+ if e != nil {
+ return nil, ErrStrictUnsupported
+ }
+ p.kill, e = strictOpen(p.dir, "cgroup.kill", unix.O_WRONLY)
+ if e != nil {
+ return nil, ErrStrictUnsupported
+ }
+ if populated, e := strictPopulation(p.events); e != nil || populated {
+ return nil, ErrStrictRefused
+ }
+ high := highOf(limit.MemoryMax) / int64(os.Getpagesize()) * int64(os.Getpagesize())
+ p.expected = map[string]string{"memory.max": strconv.FormatInt(limit.MemoryMax, 10), "memory.high": strconv.FormatInt(high, 10), "memory.swap.max": strconv.FormatInt(limit.MemoryMax, 10), "memory.oom.group": "1"}
+ for name := range p.expected {
+ p.limits[name], e = strictOpen(p.dir, name, unix.O_RDWR)
+ if e != nil {
+ return nil, ErrStrictUnsupported
+ }
+ }
+ for _, name := range []string{"memory.max", "memory.high", "memory.swap.max", "memory.oom.group"} {
+ if e = strictWrite(p.limits[name], p.expected[name]); e != nil {
+ return nil, ErrStrictRefused
+ }
+ }
+ if e = p.limitsOK(); e != nil {
+ return nil, e
+ }
+ if e = p.identityOK(); e != nil {
+ return nil, e
+ }
+ return p, nil
+}
+
+// Start consumes one attempt even if exec or containment fails. The caller
+// supplies the complete command, including its approved executable descriptor.
+func (p *Prepared) Start(cmd *exec.Cmd) error {
+ if p == nil || p.strictPreparedState == nil {
+ return ErrStrictState
+ }
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ if p.closed || p.attempted || p.empty {
+ return ErrStrictState
+ }
+ p.attempted = true
+ if cmd == nil || cmd.Process != nil || cmd.ProcessState != nil {
+ return ErrStrictState
+ }
+ if e := p.identityOK(); e != nil {
+ return e
+ }
+ if e := p.limitsOK(); e != nil {
+ return e
+ }
+ if populated, e := strictPopulation(p.events); e != nil || populated {
+ return ErrStrictRefused
+ }
+ setInto(cmd, int(p.dir.Fd()))
+ cmd.SysProcAttr.Pdeathsig = syscall.SIGKILL
+ if e := p.start(cmd); e != nil {
+ if shouldLatch(e) {
+ return ErrStrictUnsupported
+ }
+ return ErrStrictRefused
+ }
+ if cmd.Process == nil {
+ return ErrStrictRefused
+ }
+ return nil
+}
+
+// KillAndEmpty proves kernel population is zero before the caller's deadline.
+// Failure retains the pinned cleanup handle and permits another bounded attempt.
+func (p *Prepared) KillAndEmpty(ctx context.Context) error {
+ if p == nil || p.strictPreparedState == nil {
+ return ErrStrictState
+ }
+ if ctx == nil {
+ return ErrStrictDeadline
+ }
+ deadline, ok := ctx.Deadline()
+ if !ok || ctx.Err() != nil || !time.Now().Before(deadline) {
+ return ErrStrictDeadline
+ }
+ lockTick := time.NewTicker(time.Millisecond)
+ defer lockTick.Stop()
+ for !p.mu.TryLock() {
+ select {
+ case <-ctx.Done():
+ return ErrStrictDeadline
+ case <-lockTick.C:
+ }
+ }
+ defer p.mu.Unlock()
+ if p.closed || p.empty {
+ return ErrStrictState
+ }
+ if ctx.Err() != nil {
+ return ErrStrictDeadline
+ }
+ if e := p.identityOK(); e != nil {
+ return e
+ }
+ p.attempted = true
+ if e := strictWrite(p.kill, "1"); e != nil {
+ return ErrStrictRefused
+ }
+ timer := time.NewTicker(5 * time.Millisecond)
+ defer timer.Stop()
+ for {
+ if ctx.Err() != nil || !time.Now().Before(deadline) {
+ return ErrStrictDeadline
+ }
+ if e := p.identityOK(); e != nil {
+ return e
+ }
+ populated, e := strictPopulation(p.events)
+ if e != nil {
+ return ErrStrictRefused
+ }
+ if !populated {
+ if ctx.Err() != nil || !time.Now().Before(deadline) {
+ return ErrStrictDeadline
+ }
+ p.empty = true
+ return nil
+ }
+ select {
+ case <-ctx.Done():
+ return ErrStrictDeadline
+ case <-timer.C:
+ }
+ }
+}
+
+// Close never removes a path. A populated or unverifiable group retains its
+// descriptors and reservation so the same handle can still attempt cleanup.
+func (p *Prepared) Close() error {
+ if p == nil || p.strictPreparedState == nil {
+ return ErrStrictState
+ }
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ if p.closed {
+ return ErrStrictState
+ }
+ if e := p.identityOK(); e != nil {
+ return e
+ }
+ if populated, e := strictPopulation(p.events); e != nil || populated {
+ return ErrStrictRefused
+ }
+ p.closed = true
+ p.closeFiles()
+ strictReservations.Lock()
+ delete(strictReservations.held, p.identity)
+ strictReservations.Unlock()
+ return nil
+}
diff --git a/internal/cgroup/strict_linux_test.go b/internal/cgroup/strict_linux_test.go
new file mode 100644
index 000000000..0d6124a94
--- /dev/null
+++ b/internal/cgroup/strict_linux_test.go
@@ -0,0 +1,246 @@
+//go:build linux
+
+package cgroup
+
+import (
+ "context"
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sync/atomic"
+ "syscall"
+ "testing"
+ "time"
+)
+
+// Filesystem fixtures test handle state only. PrepareStrict must reject them;
+// the delegated-kernel suite separately proves actual containment and controls.
+func strictFixture(t *testing.T) *Prepared {
+ t.Helper()
+ root := t.TempDir()
+ host := "fixture.example"
+ dir := filepath.Join(root, ExecName(host))
+ if e := os.Mkdir(dir, 0700); e != nil {
+ t.Fatal(e)
+ }
+ for name, value := range map[string]string{"cgroup.events": "populated 0\nfrozen 0\n", "cgroup.kill": "", "memory.max": "4096", "memory.high": "4096", "memory.swap.max": "4096", "memory.oom.group": "1"} {
+ if e := os.WriteFile(filepath.Join(dir, name), []byte(value), 0600); e != nil {
+ t.Fatal(e)
+ }
+ }
+ open := func(path string) *os.File {
+ flags := os.O_RDWR
+ if info, e := os.Stat(path); e == nil && info.IsDir() {
+ flags = os.O_RDONLY
+ }
+ f, e := os.OpenFile(path, flags, 0)
+ if e != nil {
+ t.Fatal(e)
+ }
+ return f
+ }
+ p := &Prepared{strictPreparedState: &strictPreparedState{root: open(root), dir: open(dir), host: host, limits: map[string]*os.File{}, expected: map[string]string{}, start: func(*exec.Cmd) error { return syscall.ENOSYS }}}
+ p.kill = open(filepath.Join(dir, "cgroup.kill"))
+ p.events = open(filepath.Join(dir, "cgroup.events"))
+ p.identity, _ = strictID(p.dir)
+ for _, name := range []string{"memory.max", "memory.high", "memory.swap.max", "memory.oom.group"} {
+ p.limits[name] = open(filepath.Join(dir, name))
+ p.expected[name], _ = strictRead(p.limits[name])
+ }
+ strictReservations.Lock()
+ strictReservations.held[p.identity] = true
+ strictReservations.Unlock()
+ t.Cleanup(func() {
+ p.closeFiles()
+ strictReservations.Lock()
+ delete(strictReservations.held, p.identity)
+ strictReservations.Unlock()
+ })
+ return p
+}
+func strictContext(t *testing.T, d time.Duration) context.Context {
+ t.Helper()
+ ctx, cancel := context.WithTimeout(t.Context(), d)
+ t.Cleanup(cancel)
+ return ctx
+}
+func TestStrictRejectsUnavailableManagerHostAndTier(t *testing.T) {
+ for _, m := range []*Manager{nil, {root: t.TempDir()}} {
+ for _, host := range []string{"ok.example", "../outside", "Upper.example", "", "a/b"} {
+ if p, e := m.PrepareStrict(host, TierDefault); e == nil || p != nil {
+ t.Fatal("unsupported filesystem or host accepted")
+ }
+ }
+ }
+ m := &Manager{root: "/sys/fs/cgroup"}
+ for _, limit := range []Limit{{}, {MemoryMax: -1}, {MemoryMax: 1}} {
+ if p, e := m.PrepareStrict("x.example", limit); e == nil || p != nil {
+ t.Fatal("missing or foreign tier accepted")
+ }
+ }
+}
+func TestStrictStartConsumesOneAttemptWithoutFallback(t *testing.T) {
+ p := strictFixture(t)
+ var calls atomic.Int32
+ p.start = func(cmd *exec.Cmd) error {
+ calls.Add(1)
+ if cmd.SysProcAttr == nil || !cmd.SysProcAttr.UseCgroupFD || cmd.SysProcAttr.CgroupFD != int(p.dir.Fd()) || cmd.SysProcAttr.Pdeathsig != syscall.SIGKILL || !cmd.SysProcAttr.Setpgid {
+ t.Error("strict spawn attributes missing or clobbered")
+ }
+ return syscall.ENOSYS
+ }
+ copied := *p
+ cmd := exec.Command("/not-executed")
+ cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
+ if e := p.Start(cmd); !errors.Is(e, ErrStrictUnsupported) {
+ t.Fatal("unsupported clone accepted", e)
+ }
+ if e := p.Start(exec.Command("/not-executed")); !errors.Is(e, ErrStrictState) {
+ t.Fatal("second spawn accepted", e)
+ }
+ if e := copied.Start(exec.Command("/not-executed")); !errors.Is(e, ErrStrictState) {
+ t.Fatal("copied handle restored spawn authority", e)
+ }
+ if calls.Load() != 1 {
+ t.Fatal("strict start retried", calls.Load())
+ }
+ if raw, e := strictRead(p.kill); e != nil || raw != "" {
+ t.Fatal("failed spawn invented cleanup", raw, e)
+ }
+}
+func TestStrictCleanupRequiresDeadlineAndSurvivesTimeout(t *testing.T) {
+ p := strictFixture(t)
+ for _, ctx := range []context.Context{nil, context.Background(), strictContext(t, -time.Second)} {
+ if e := p.KillAndEmpty(ctx); !errors.Is(e, ErrStrictDeadline) {
+ t.Fatal("unbounded/expired cleanup accepted", e)
+ }
+ }
+ if e := os.WriteFile(filepath.Join(p.root.Name(), ExecName(p.host), "cgroup.events"), []byte("populated 1\n"), 0600); e != nil {
+ t.Fatal(e)
+ }
+ if e := p.Close(); !errors.Is(e, ErrStrictRefused) {
+ t.Fatal("populated group handle discarded", e)
+ }
+ if e := p.KillAndEmpty(strictContext(t, 15*time.Millisecond)); !errors.Is(e, ErrStrictDeadline) {
+ t.Fatal("populated group claimed empty", e)
+ }
+ if e := p.Start(exec.Command("/not-executed")); !errors.Is(e, ErrStrictState) {
+ t.Fatal("cleanup timeout restored spawn authority", e)
+ }
+ if e := os.WriteFile(filepath.Join(p.root.Name(), ExecName(p.host), "cgroup.events"), []byte("populated 0\n"), 0600); e != nil {
+ t.Fatal(e)
+ }
+ if e := p.KillAndEmpty(strictContext(t, time.Second)); e != nil {
+ t.Fatal("same-handle cleanup retry failed", e)
+ }
+ if e := p.KillAndEmpty(strictContext(t, time.Second)); !errors.Is(e, ErrStrictState) {
+ t.Fatal("second cleanup reused proof", e)
+ }
+ if e := p.Close(); e != nil {
+ t.Fatal(e)
+ }
+ if e := p.Close(); !errors.Is(e, ErrStrictState) {
+ t.Fatal("double close accepted")
+ }
+ if e := p.Start(exec.Command("/not-executed")); !errors.Is(e, ErrStrictState) {
+ t.Fatal("closed spawn accepted")
+ }
+ if e := p.KillAndEmpty(strictContext(t, time.Second)); !errors.Is(e, ErrStrictState) {
+ t.Fatal("closed cleanup accepted")
+ }
+}
+func TestStrictCleanupMutexWaitHonorsDeadline(t *testing.T) {
+ p := strictFixture(t)
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ start := time.Now()
+ if e := p.KillAndEmpty(strictContext(t, 15*time.Millisecond)); !errors.Is(e, ErrStrictDeadline) {
+ t.Fatal(e)
+ }
+ if time.Since(start) > 250*time.Millisecond {
+ t.Fatal("mutex wait escaped caller deadline")
+ }
+}
+func TestStrictStaleHandleCannotKillReplacement(t *testing.T) {
+ p := strictFixture(t)
+ old := filepath.Join(p.root.Name(), ExecName(p.host))
+ if e := os.Rename(old, old+"-old"); e != nil {
+ t.Fatal(e)
+ }
+ if e := os.Mkdir(old, 0700); e != nil {
+ t.Fatal(e)
+ }
+ kill := filepath.Join(old, "cgroup.kill")
+ if e := os.WriteFile(kill, []byte("replacement"), 0600); e != nil {
+ t.Fatal(e)
+ }
+ if e := p.KillAndEmpty(strictContext(t, time.Second)); !errors.Is(e, ErrStrictRefused) {
+ t.Fatal("wrong identity accepted", e)
+ }
+ if e := p.Close(); !errors.Is(e, ErrStrictRefused) {
+ t.Fatal("stale close claimed proof", e)
+ }
+ raw, e := os.ReadFile(kill)
+ if e != nil || string(raw) != "replacement" {
+ t.Fatal("replacement group touched", e)
+ }
+ if e := p.Start(exec.Command("/not-executed")); !errors.Is(e, ErrStrictRefused) {
+ t.Fatal("stale spawn accepted", e)
+ }
+}
+func TestStrictChangedLimitsAndMalformedEventsRefuse(t *testing.T) {
+ for _, name := range []string{"memory.max", "memory.high", "memory.swap.max", "memory.oom.group", "cgroup.events"} {
+ t.Run(name, func(t *testing.T) {
+ p := strictFixture(t)
+ if e := os.WriteFile(filepath.Join(p.root.Name(), ExecName(p.host), name), []byte("missing\n"), 0600); e != nil {
+ t.Fatal(e)
+ }
+ called := false
+ p.start = func(*exec.Cmd) error { called = true; return nil }
+ if e := p.Start(exec.Command("/not-executed")); e == nil || called {
+ t.Fatal("unchecked controls reached spawn", e)
+ }
+ if name == "cgroup.events" {
+ if e := p.Close(); e == nil {
+ t.Fatal("unknown emptiness discarded handle")
+ }
+ }
+ })
+ }
+}
+
+func TestStrictConcurrentAndInvalidStartsConsumeAttempt(t *testing.T) {
+ p := strictFixture(t)
+ var calls atomic.Int32
+ p.start = func(*exec.Cmd) error { calls.Add(1); return syscall.ENOSYS }
+ results := make(chan error, 16)
+ for range 16 {
+ go func() { results <- p.Start(exec.Command("/not-executed")) }()
+ }
+ unsupported := 0
+ for range 16 {
+ switch err := <-results; err {
+ case ErrStrictUnsupported:
+ unsupported++
+ case ErrStrictState:
+ default:
+ t.Fatal(err)
+ }
+ }
+ if calls.Load() != 1 || unsupported != 1 {
+ t.Fatal("concurrent starts repeated attempt")
+ }
+ q := strictFixture(t)
+ if err := q.Start(nil); err != ErrStrictState {
+ t.Fatal(err)
+ }
+ if err := q.Start(exec.Command("/not-executed")); err != ErrStrictState {
+ t.Fatal("invalid command restored attempt", err)
+ }
+ for _, zero := range []*Prepared{nil, {}} {
+ if zero.Start(nil) != ErrStrictState || zero.Close() != ErrStrictState || zero.KillAndEmpty(context.Background()) != ErrStrictState {
+ t.Fatal("zero handle accepted")
+ }
+ }
+}
diff --git a/internal/cgroup/strict_other.go b/internal/cgroup/strict_other.go
new file mode 100644
index 000000000..0a1b0ed76
--- /dev/null
+++ b/internal/cgroup/strict_other.go
@@ -0,0 +1,17 @@
+//go:build !linux
+
+package cgroup
+
+import (
+ "context"
+ "os/exec"
+)
+
+type Prepared struct{}
+
+func (m *Manager) PrepareStrict(host string, limit Limit) (*Prepared, error) {
+ return nil, ErrStrictUnsupported
+}
+func (p *Prepared) Start(cmd *exec.Cmd) error { return ErrStrictUnsupported }
+func (p *Prepared) KillAndEmpty(ctx context.Context) error { return ErrStrictUnsupported }
+func (p *Prepared) Close() error { return ErrStrictUnsupported }