Monorepo for Tangled tangled.org
4

Configure Feed

Select the types of activity you want to include in your feed.

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