//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, logf: t.Logf}).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, logf: t.Logf} 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") }