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  }