This repository has no description
1//go:build linux
2
3package microvm
4
5import (
6 "errors"
7 "fmt"
8 "log/slog"
9 "os"
10 "path/filepath"
11 "regexp"
12 "strconv"
13 "strings"
14 "syscall"
15
16 cgroups "github.com/containerd/cgroups/v3"
17 "github.com/containerd/cgroups/v3/cgroup2"
18 "github.com/prometheus/procfs"
19)
20
21var (
22 cgroupInvalidChar = regexp.MustCompile(`[^a-zA-Z0-9\-_.]`)
23 cgroupConsecutiveSep = regexp.MustCompile(`[-_.]{2,}`)
24)
25
26const (
27 cgroupParentSelf = "self"
28 supervisorCgroupName = "supervisor"
29)
30
31type CgroupLimits struct {
32 Enabled bool
33 Parent *CgroupParent
34 Name string
35 MemoryMaxMiB int64
36 SwapMaxMiB *int64
37 PidsMax int64
38 // cpu.max quota as a percentage of one core (100 = one core). <= 0
39 // leaves cpu unlimited
40 CPUQuotaPercent int64
41 // io.weight (1-10000). 0 leaves io unlimited, written directly since
42 // this cgroup2 lib only models io.bfq.weight/io.max
43 IOWeight uint64
44}
45
46type CgroupParent struct {
47 root *cgroup2.Manager
48 mountpoint string
49 group string
50}
51
52type CgroupHandle struct {
53 manager *cgroup2.Manager
54}
55
56func initCgroupParent(parent string, supervisorMemoryMinMiB int64, logger *slog.Logger) (*CgroupParent, error) {
57 if parent == "" {
58 parent = cgroupParentSelf
59 }
60 if cgroups.Mode() != cgroups.Unified {
61 return nil, fmt.Errorf("microVM cgroups require cgroup v2 unified mode")
62 }
63
64 mountpoint, group, err := resolveCgroupParent(parent)
65 if err != nil {
66 return nil, err
67 }
68 if _, err := os.Stat(filepath.Join(mountpoint, strings.TrimPrefix(group, "/"))); err != nil {
69 return nil, fmt.Errorf("stat cgroup parent %q:%q: %w", mountpoint, group, err)
70 }
71
72 root, err := cgroup2.Load(group, cgroup2.WithMountpoint(mountpoint))
73 if err != nil {
74 return nil, fmt.Errorf("load cgroup parent %q:%q: %w", mountpoint, group, err)
75 }
76
77 if group != "/" {
78 if err := moveParentProcesses(root, supervisorMemoryMinMiB, logger); err != nil {
79 return nil, err
80 }
81 } else if err := probeRootSubtreeControl(mountpoint); err != nil {
82 if !errors.Is(err, syscall.EBUSY) {
83 return nil, fmt.Errorf("enable controllers in subtree_control of cgroup root %q: %w", mountpoint, err)
84 }
85 // populated namespace root, not the real root: vacate it too
86 if err := moveParentProcesses(root, supervisorMemoryMinMiB, logger); err != nil {
87 return nil, err
88 }
89 }
90
91 if logger != nil {
92 logger.Info("initialized microVM cgroup parent", "mountpoint", mountpoint, "group", group)
93 }
94 return &CgroupParent{root: root, mountpoint: mountpoint, group: group}, nil
95}
96
97func prepareCgroup(limits CgroupLimits, logger *slog.Logger) (*CgroupHandle, error) {
98 if !limits.Enabled {
99 return nil, nil
100 }
101 if limits.Parent == nil || limits.Parent.root == nil {
102 return nil, fmt.Errorf("cgroup parent is not initialized")
103 }
104 name := sanitizeCgroupName(limits.Name)
105 if name == "" {
106 return nil, fmt.Errorf("cgroup name is empty")
107 }
108
109 manager, err := limits.Parent.root.NewChild(name, cgroupResources(limits))
110 if err != nil {
111 return nil, fmt.Errorf("create cgroup %q: %w", name, err)
112 }
113
114 if limits.IOWeight > 0 {
115 if err := manager.ToggleControllers([]string{"io"}, cgroup2.Enable); err != nil {
116 _ = manager.Delete()
117 return nil, fmt.Errorf("enable io controller for cgroup %q: %w", name, err)
118 }
119 ioWeightPath := filepath.Join(limits.Parent.mountpoint, strings.TrimPrefix(limits.Parent.group, "/"), name, "io.weight")
120 if err := os.WriteFile(ioWeightPath, []byte(strconv.FormatUint(limits.IOWeight, 10)), 0); err != nil {
121 _ = manager.Delete()
122 return nil, fmt.Errorf("write io.weight for cgroup %q: %w", name, err)
123 }
124 }
125
126 if logger != nil {
127 logger.Info("created microVM cgroup", "name", name, "parentGroup", limits.Parent.group)
128 }
129 return &CgroupHandle{manager: manager}, nil
130}
131
132func cgroupResources(limits CgroupLimits) *cgroup2.Resources {
133 resources := &cgroup2.Resources{}
134 if limits.MemoryMaxMiB > 0 || limits.SwapMaxMiB != nil {
135 memory := &cgroup2.Memory{}
136 if limits.MemoryMaxMiB > 0 {
137 maxBytes := limits.MemoryMaxMiB * 1024 * 1024
138 memory.Max = &maxBytes
139 }
140 if limits.SwapMaxMiB != nil {
141 swapBytes := *limits.SwapMaxMiB * 1024 * 1024
142 memory.Swap = &swapBytes
143 }
144 oomGroup := true
145 memory.OOMGroup = &oomGroup
146 resources.Memory = memory
147 }
148 if limits.PidsMax > 0 {
149 resources.Pids = &cgroup2.Pids{Max: limits.PidsMax}
150 }
151 if limits.CPUQuotaPercent > 0 {
152 quota := limits.CPUQuotaPercent * 1000 // 100% of one 100000us period
153 resources.CPU = &cgroup2.CPU{Max: cgroup2.NewCPUMax("a, nil)}
154 }
155 return resources
156}
157
158func supervisorResources(memoryMinMiB int64) *cgroup2.Resources {
159 if memoryMinMiB <= 0 {
160 return nil
161 }
162 minBytes := memoryMinMiB * 1024 * 1024
163 return &cgroup2.Resources{
164 Memory: &cgroup2.Memory{Min: &minBytes},
165 }
166}
167
168func (h *CgroupHandle) AddProcess(pid int, logger *slog.Logger) error {
169 if h == nil || h.manager == nil {
170 return nil
171 }
172 if pid <= 0 {
173 return fmt.Errorf("invalid pid %d", pid)
174 }
175 if err := h.manager.AddProc(uint64(pid)); err != nil {
176 return fmt.Errorf("add pid %d to cgroup: %w", pid, err)
177 }
178 if logger != nil {
179 logger.Info("added process to microVM cgroup", "pid", pid)
180 }
181 return nil
182}
183
184func (h *CgroupHandle) Close() error {
185 if h == nil || h.manager == nil {
186 return nil
187 }
188 return h.manager.Delete()
189}
190
191func (h *CgroupHandle) OOMKilled() bool {
192 if h == nil || h.manager == nil {
193 return false
194 }
195 metrics, err := h.manager.Stat()
196 if err != nil || metrics == nil || metrics.MemoryEvents == nil {
197 return false
198 }
199 return metrics.MemoryEvents.OomKill > 0
200}
201
202// probeRootSubtreeControl enables the domain controllers the engine needs
203// in the "/" parent's subtree. this fails EBUSY at a populated cgroup
204// namespace root (no-internal-process constraint); the real root is exempt.
205func probeRootSubtreeControl(mountpoint string) error {
206 // cpu/io may be unavailable (eg. no CONFIG_BLK_CGROUP), memory/pids
207 // are the hard requirement
208 if err := os.WriteFile(filepath.Join(mountpoint, "cgroup.subtree_control"), []byte("+memory +pids +cpu +io"), 0); err == nil {
209 return nil
210 }
211 return os.WriteFile(filepath.Join(mountpoint, "cgroup.subtree_control"), []byte("+memory +pids"), 0)
212}
213
214func resolveCgroupParent(parent string) (string, string, error) {
215 mountpoint, err := cgroup2Mountpoint()
216 if err != nil {
217 return "", "", err
218 }
219
220 if parent == "" || parent == cgroupParentSelf {
221 group, err := selfCgroupV2Path()
222 if err != nil {
223 return "", "", err
224 }
225 return mountpoint, group, nil
226 }
227 if !filepath.IsAbs(parent) {
228 return "", "", fmt.Errorf("cgroup parent must be %q or an absolute delegated cgroupfs path: %q", cgroupParentSelf, parent)
229 }
230
231 cleanParent := filepath.Clean(parent)
232 rel, err := filepath.Rel(mountpoint, cleanParent)
233 if err != nil {
234 return "", "", fmt.Errorf("resolve cgroup parent %q relative to cgroup2 mount %q: %w", cleanParent, mountpoint, err)
235 }
236 if rel == ".." || strings.HasPrefix(rel, "../") {
237 return "", "", fmt.Errorf("cgroup parent %q is outside cgroup2 mount %q", cleanParent, mountpoint)
238 }
239 if rel == "." {
240 return mountpoint, "/", nil
241 }
242
243 group := "/" + filepath.ToSlash(rel)
244 if err := cgroup2.VerifyGroupPath(group); err != nil {
245 return "", "", fmt.Errorf("invalid cgroup parent path %q: %w", group, err)
246 }
247 return mountpoint, group, nil
248}
249
250func cgroup2Mountpoint() (string, error) {
251 mounts, err := procfs.GetMounts()
252 if err != nil {
253 return "", fmt.Errorf("read procfs mountinfo: %w", err)
254 }
255 for _, mount := range mounts {
256 if mount.FSType == "cgroup2" {
257 return mount.MountPoint, nil
258 }
259 }
260 return "", fmt.Errorf("cgroup v2 mountpoint not found")
261}
262
263func selfCgroupV2Path() (string, error) {
264 self, err := procfs.Self()
265 if err != nil {
266 return "", fmt.Errorf("open procfs self: %w", err)
267 }
268 groups, err := self.Cgroups()
269 if err != nil {
270 return "", fmt.Errorf("read procfs self cgroups: %w", err)
271 }
272 for _, group := range groups {
273 if group.HierarchyID != 0 {
274 continue
275 }
276 path := group.Path
277 if path == "" {
278 path = "/"
279 }
280 if err := cgroup2.VerifyGroupPath(path); err != nil {
281 return "", fmt.Errorf("invalid self cgroup path %q: %w", path, err)
282 }
283 return path, nil
284 }
285 return "", fmt.Errorf("current process has no cgroup v2 hierarchy entry")
286}
287
288func moveParentProcesses(parent *cgroup2.Manager, supervisorMemoryMinMiB int64, logger *slog.Logger) error {
289 procs, err := parent.Procs(false)
290 if err != nil {
291 return fmt.Errorf("list parent cgroup processes: %w", err)
292 }
293
294 // first create with empty resources
295 supervisor, err := parent.NewChild(supervisorCgroupName, &cgroup2.Resources{})
296 if err != nil {
297 return fmt.Errorf("create supervisor cgroup: %w", err)
298 }
299
300 // move procs
301 for _, pid := range procs {
302 if err := supervisor.AddProc(pid); err != nil {
303 return fmt.Errorf("move pid %d to supervisor cgroup: %w", pid, err)
304 }
305 }
306
307 // now apply resources. we can't do this while parent has procs still
308 if res := supervisorResources(supervisorMemoryMinMiB); res != nil {
309 // we use a "new" parent here, this is so we enable subtree_control.
310 // .Update() does not work here...
311 if _, err = parent.NewChild(supervisorCgroupName, res); err != nil {
312 return fmt.Errorf("apply supervisor cgroup resources: %w", err)
313 }
314 }
315
316 if logger != nil && len(procs) > 0 {
317 logger.Info("moved spindle processes to supervisor cgroup", "processes", len(procs))
318 }
319 return nil
320}
321
322func sanitizeCgroupName(name string) string {
323 name = cgroupInvalidChar.ReplaceAllLiteralString(name, "-")
324 name = cgroupConsecutiveSep.ReplaceAllLiteralString(name, "-")
325 return strings.Trim(name, "-_.")
326}