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