Monorepo for Tangled
tangled.org
1package microvm
2
3import (
4 "log/slog"
5 "os"
6 "os/exec"
7 "runtime"
8 "testing"
9 "time"
10
11 cgroups "github.com/containerd/cgroups/v3"
12)
13
14const memhogEnv = "SPINDLE_CGROUP_MEMHOG"
15
16func TestMain(m *testing.M) {
17 if os.Getenv(memhogEnv) == "1" {
18 runMemhogChild()
19 return
20 }
21 os.Exit(m.Run())
22}
23
24// this will allocate memory in steps until either the cgroup kills the process
25// this is running on, or if the limit is reached. the limit is there so that if
26// the cgroup somehow does not work, we don't kill the host and can observe that
27// failure.
28func runMemhogChild() {
29 var b [1]byte
30 _, _ = os.Stdin.Read(b[:])
31
32 const chunk = 4 << 20 // 4 MiB
33 const limit = 512 << 20 // safety cap
34 hold := make([][]byte, 0, limit/chunk)
35 for total := 0; total < limit; total += chunk {
36 c := make([]byte, chunk)
37 for i := range c {
38 c[i] = 1 // fault the pages in so they count against memory.current
39 }
40 hold = append(hold, c)
41 time.Sleep(5 * time.Millisecond)
42 }
43 runtime.KeepAlive(hold)
44 os.Exit(0)
45}
46
47// creates a cgroup parent, adds a memory limited child to it, and creates a
48// process that hogs memory and observes if it OOMs or not.
49//
50// run with:
51//
52// SPINDLE_CGROUP_INTEGRATION=1 systemd-run --user --scope -p Delegate=yes \
53// go test -run TestCgroupOOMEnforcement ./spindle/engines/microvm/
54func TestCgroupOOMEnforcement(t *testing.T) {
55 if os.Getenv("SPINDLE_CGROUP_INTEGRATION") != "1" {
56 t.Skip("see test doc comment on how to run")
57 }
58 if cgroups.Mode() != cgroups.Unified {
59 t.Skip("requires cgroup v2 unified mode")
60 }
61
62 logger := slog.Default()
63
64 parent, err := initCgroupParent(cgroupParentSelf, 0, logger)
65 if err != nil {
66 t.Skipf("cannot initialize cgroup parent (need cgroup v2 delegation): %v", err)
67 }
68
69 swap := int64(0) // disable swap so the limit forces an OOM promptly
70 handle, err := prepareCgroup(CgroupLimits{
71 Enabled: true,
72 Parent: parent,
73 Name: "cgtest-oom",
74 MemoryMaxMiB: 64,
75 SwapMaxMiB: &swap,
76 PidsMax: 256,
77 }, logger)
78 if err != nil {
79 t.Skipf("cannot create a memory-limited child cgroup (need the memory controller delegated): %v", err)
80 }
81 if handle == nil {
82 t.Fatal("prepareCgroup returned a nil handle for enabled limits")
83 }
84 t.Cleanup(func() { _ = handle.Close() })
85
86 cmd := exec.Command(os.Args[0])
87 cmd.Env = append(os.Environ(), memhogEnv+"=1")
88 stdin, err := cmd.StdinPipe()
89 if err != nil {
90 t.Fatal(err)
91 }
92 if err := cmd.Start(); err != nil {
93 t.Fatal(err)
94 }
95 defer func() {
96 _ = cmd.Process.Kill()
97 _ = cmd.Wait()
98 }()
99
100 if err := handle.AddProcess(cmd.Process.Pid, logger); err != nil {
101 t.Fatalf("add memhog to cgroup: %v", err)
102 }
103
104 // let the child process start allocating memory
105 if _, err := stdin.Write([]byte("g")); err != nil {
106 t.Fatalf("release memhog: %v", err)
107 }
108 _ = stdin.Close()
109
110 waitErr := make(chan error, 1)
111 go func() { waitErr <- cmd.Wait() }()
112
113 select {
114 case err := <-waitErr:
115 if err == nil {
116 t.Fatal("memhog exited cleanly: the cgroup memory limit was not enforced")
117 }
118 t.Logf("memhog died as expected: %v", err)
119 case <-time.After(30 * time.Second):
120 t.Fatal("memhog did not die within 30s, cgroup memory limit not enforced")
121 }
122
123 if !handle.OOMKilled() {
124 t.Fatal("OOMKilled() is false after the memhog was killed, memory.events oom_kill was not observed")
125 }
126}