github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/sandbox/cgroup/cgroup.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package cgroup 21 22 import ( 23 "bufio" 24 "fmt" 25 "io" 26 "os" 27 "path/filepath" 28 "strconv" 29 "strings" 30 "syscall" 31 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/strutil" 34 ) 35 36 const ( 37 // From golang.org/x/sys/unix 38 cgroup2SuperMagic = 0x63677270 39 40 // The only cgroup path we expect, for v2 this is where the unified 41 // hierarchy is mounted, for v1 this is usually a tmpfs mount, under 42 // which the controller-hierarchies are mounted 43 cgroupMountPoint = "/sys/fs/cgroup" 44 ) 45 46 var ( 47 // Filesystem root defined locally to avoid dependency on the 'dirs' 48 // package 49 rootPath = "/" 50 ) 51 52 const ( 53 // Separate block, because iota is fun 54 Unknown = iota 55 V1 56 V2 57 ) 58 59 var ( 60 probeVersion = Unknown 61 probeErr error = nil 62 ) 63 64 func init() { 65 dirs.AddRootDirCallback(func(root string) { 66 rootPath = root 67 }) 68 probeVersion, probeErr = probeCgroupVersion() 69 // handles error case gracefully 70 pickVersionSpecificImpl() 71 } 72 73 func pickVersionSpecificImpl() { 74 switch probeVersion { 75 case V1: 76 pickFreezerV1Impl() 77 case V2: 78 pickFreezerV2Impl() 79 } 80 } 81 82 var fsTypeForPath = fsTypeForPathImpl 83 84 func fsTypeForPathImpl(path string) (int64, error) { 85 var statfs syscall.Statfs_t 86 if err := syscall.Statfs(path, &statfs); err != nil { 87 return 0, fmt.Errorf("cannot statfs path: %v", err) 88 } 89 // Typs is int32 on 386, use explicit conversion to keep the code 90 // working for both 91 return int64(statfs.Type), nil 92 } 93 94 // ProcPidPath returns the path to the cgroup file under /proc for the given 95 // process id. 96 func ProcPidPath(pid int) string { 97 return filepath.Join(rootPath, fmt.Sprintf("proc/%v/cgroup", pid)) 98 } 99 100 func probeCgroupVersion() (version int, err error) { 101 cgroupMount := filepath.Join(rootPath, cgroupMountPoint) 102 typ, err := fsTypeForPath(cgroupMount) 103 if err != nil { 104 return Unknown, fmt.Errorf("cannot determine filesystem type: %v", err) 105 } 106 if typ == cgroup2SuperMagic { 107 return V2, nil 108 } 109 return V1, nil 110 } 111 112 // IsUnified returns true when a unified cgroup hierarchy is in use 113 func IsUnified() bool { 114 version, _ := Version() 115 return version == V2 116 } 117 118 // Version returns the detected cgroup version 119 func Version() (int, error) { 120 return probeVersion, probeErr 121 } 122 123 // GroupMatcher attempts to match the cgroup entry 124 type GroupMatcher interface { 125 String() string 126 // Match returns true when given tuple of hierarchy-ID and controllers is a match 127 Match(id, maybeControllers string) bool 128 } 129 130 type unified struct{} 131 132 func (u *unified) Match(id, maybeControllers string) bool { 133 return id == "0" && maybeControllers == "" 134 } 135 func (u *unified) String() string { return "unified hierarchy" } 136 137 // MatchUnifiedHierarchy provides matches for unified cgroup hierarchies 138 func MatchUnifiedHierarchy() GroupMatcher { 139 return &unified{} 140 } 141 142 type v1NamedHierarchy struct { 143 name string 144 } 145 146 func (n *v1NamedHierarchy) Match(_, maybeControllers string) bool { 147 if !strings.HasPrefix(maybeControllers, "name=") { 148 return false 149 } 150 name := strings.TrimPrefix(maybeControllers, "name=") 151 return name == n.name 152 } 153 154 func (n *v1NamedHierarchy) String() string { return fmt.Sprintf("named hierarchy %q", n.name) } 155 156 // MatchV1NamedHierarchy provides a matcher for a given named v1 hierarchy 157 func MatchV1NamedHierarchy(hierarchyName string) GroupMatcher { 158 return &v1NamedHierarchy{name: hierarchyName} 159 } 160 161 type v1Controller struct { 162 controller string 163 } 164 165 func (n *v1Controller) Match(_, maybeControllers string) bool { 166 controllerList := strings.Split(maybeControllers, ",") 167 return strutil.ListContains(controllerList, n.controller) 168 } 169 170 func (n *v1Controller) String() string { return fmt.Sprintf("controller %q", n.controller) } 171 172 // MatchV1Controller provides a matches for a given v1 controller 173 func MatchV1Controller(controller string) GroupMatcher { 174 return &v1Controller{controller: controller} 175 } 176 177 // ProcGroup finds the path of a given cgroup controller for provided process 178 // id. 179 func ProcGroup(pid int, matcher GroupMatcher) (string, error) { 180 if matcher == nil { 181 return "", fmt.Errorf("internal error: cgroup matcher is nil") 182 } 183 184 f, err := os.Open(ProcPidPath(pid)) 185 if err != nil { 186 return "", err 187 } 188 defer f.Close() 189 190 scanner := bufio.NewScanner(f) 191 for scanner.Scan() { 192 // we need to find a string like: 193 // ... 194 // <id>:<controller[,controller]>:/<path> 195 // 7:freezer:/snap.hello-world 196 // ... 197 // See cgroups(7) for details about the /proc/[pid]/cgroup 198 // format. 199 l := strings.Split(scanner.Text(), ":") 200 if len(l) < 3 { 201 continue 202 } 203 id := l[0] 204 maybeControllerList := l[1] 205 cgroupPath := l[2] 206 207 if !matcher.Match(id, maybeControllerList) { 208 continue 209 } 210 211 return cgroupPath, nil 212 } 213 if scanner.Err() != nil { 214 return "", scanner.Err() 215 } 216 217 return "", fmt.Errorf("cannot find %s cgroup path for pid %v", matcher, pid) 218 } 219 220 // MockVersion sets the reported version of cgroup support. For use in testing only 221 func MockVersion(mockVersion int, mockErr error) (restore func()) { 222 oldVersion, oldErr := probeVersion, probeErr 223 probeVersion, probeErr = mockVersion, mockErr 224 pickVersionSpecificImpl() 225 return func() { 226 probeVersion, probeErr = oldVersion, oldErr 227 } 228 } 229 230 // procInfoEntry describes a single line of /proc/PID/cgroup. 231 // 232 // CgroupID is the internal kernel identifier of a mounted cgroup. 233 // Controllers is a list of controllers in a specific cgroup 234 // Path is relative to the cgroup mount point. 235 // 236 // Cgroup mount point is not provided here. It must be derived by 237 // cross-checking with /proc/self/mountinfo. The identifier is not 238 // useful for this. 239 // 240 // Cgroup v1 have non-empty Controllers and CgroupId > 0. 241 // Cgroup v2 have empty Controllers and CgroupId == 0 242 type procInfoEntry struct { 243 CgroupID int 244 Controllers []string 245 Path string 246 } 247 248 // ProcessPathInTrackingCgroup returns the path in the hierarchy of the tracking cgroup. 249 // 250 // Tracking cgroup is whichever cgroup systemd uses for tracking processes. 251 // On modern systems this is the v2 cgroup. On older systems it is the 252 // controller-less name=systemd cgroup. 253 // 254 // This function fails on systems where systemd is not used and subsequently 255 // cgroups are not mounted. 256 func ProcessPathInTrackingCgroup(pid int) (string, error) { 257 fname := ProcPidPath(pid) 258 // Cgroup entries we're looking for look like this: 259 // 1:name=systemd:/user.slice/user-1000.slice/user@1000.service/tmux.slice/tmux@default.service 260 // 0::/user.slice/user-1000.slice/user@1000.service/tmux.slice/tmux@default.service 261 262 // It seems cgroupv2 can be "dangling" after being mounted and unmounted. 263 // It will forever stay present in the kernel but will not be present in 264 // the file-system. As such, allow v2 to register only if it is really 265 // mounted on the system. 266 var allowV2 bool 267 if ver, err := Version(); err != nil { 268 return "", err 269 } else if ver == V2 { 270 allowV2 = true 271 } 272 entry, err := scanProcCgroupFile(fname, func(e *procInfoEntry) bool { 273 if e.CgroupID == 0 && allowV2 { 274 return true 275 } 276 if len(e.Controllers) == 1 && e.Controllers[0] == "name=systemd" { 277 return true 278 } 279 return false 280 }) 281 if err != nil { 282 return "", err 283 } 284 if entry == nil { 285 return "", fmt.Errorf("cannot find tracking cgroup") 286 } 287 return entry.Path, nil 288 } 289 290 // scanProcCgroupFile scans a file for /proc/PID/cgroup entries and returns the 291 // first one matching the given predicate. 292 // 293 // If no entry matches the predicate nil is returned without errors. 294 func scanProcCgroupFile(fname string, pred func(entry *procInfoEntry) bool) (*procInfoEntry, error) { 295 f, err := os.Open(fname) 296 if err != nil { 297 return nil, err 298 } 299 defer f.Close() 300 return scanProcCgroup(f, pred) 301 } 302 303 // scanProcCgroup scans a reader for /proc/PID/cgroup entries and returns the 304 // first one matching the given predicate. 305 // 306 // If no entry matches the predicate nil is returned without errors. 307 func scanProcCgroup(reader io.Reader, pred func(entry *procInfoEntry) bool) (*procInfoEntry, error) { 308 scanner := bufio.NewScanner(reader) 309 for scanner.Scan() { 310 line := scanner.Text() 311 entry, err := parseProcCgroupEntry(line) 312 if err != nil { 313 return nil, fmt.Errorf("cannot parse proc cgroup entry %q: %s", line, err) 314 } 315 if pred(entry) { 316 return entry, nil 317 } 318 } 319 if err := scanner.Err(); err != nil { 320 return nil, err 321 } 322 return nil, nil 323 } 324 325 // parseProcCgroupEntry parses a line in format described by cgroups(7) 326 // Such files represent cgroup membership of a particular process. 327 func parseProcCgroupEntry(line string) (*procInfoEntry, error) { 328 var e procInfoEntry 329 var err error 330 fields := strings.SplitN(line, ":", 3) 331 // The format is described in cgroups(7). Field delimiter is ":" but 332 // there is no escaping. The First two fields cannot have colons, including 333 // cgroups with custom names. The last field can have colons but those are not 334 // escaped in any way. 335 if len(fields) != 3 { 336 return nil, fmt.Errorf("expected three fields") 337 } 338 // Parse cgroup ID (decimal number). 339 e.CgroupID, err = strconv.Atoi(fields[0]) 340 if err != nil { 341 return nil, fmt.Errorf("cannot parse cgroup id %q", fields[0]) 342 } 343 // Parse the comma-separated list of controllers. 344 if fields[1] != "" { 345 e.Controllers = strings.Split(fields[1], ",") 346 } 347 // The rest is the path in the hierarchy. 348 e.Path = fields[2] 349 return &e, nil 350 }