github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  }