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