github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/cgroups/v1_utils.go (about)

     1  package cgroups
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  	"syscall"
    11  
    12  	securejoin "github.com/cyphar/filepath-securejoin"
    13  	"github.com/moby/sys/mountinfo"
    14  	"golang.org/x/sys/unix"
    15  )
    16  
    17  // Code in this source file are specific to cgroup v1,
    18  // and must not be used from any cgroup v2 code.
    19  
    20  const (
    21  	CgroupNamePrefix = "name="
    22  	defaultPrefix    = "/sys/fs/cgroup"
    23  )
    24  
    25  var (
    26  	errUnified     = errors.New("not implemented for cgroup v2 unified hierarchy")
    27  	ErrV1NoUnified = errors.New("invalid configuration: cannot use unified on cgroup v1")
    28  
    29  	readMountinfoOnce sync.Once
    30  	readMountinfoErr  error
    31  	cgroupMountinfo   []*mountinfo.Info
    32  )
    33  
    34  type NotFoundError struct {
    35  	Subsystem string
    36  }
    37  
    38  func (e *NotFoundError) Error() string {
    39  	return fmt.Sprintf("mountpoint for %s not found", e.Subsystem)
    40  }
    41  
    42  func NewNotFoundError(sub string) error {
    43  	return &NotFoundError{
    44  		Subsystem: sub,
    45  	}
    46  }
    47  
    48  func IsNotFound(err error) bool {
    49  	var nfErr *NotFoundError
    50  	return errors.As(err, &nfErr)
    51  }
    52  
    53  func tryDefaultPath(cgroupPath, subsystem string) string {
    54  	if !strings.HasPrefix(defaultPrefix, cgroupPath) {
    55  		return ""
    56  	}
    57  
    58  	// remove possible prefix
    59  	subsystem = strings.TrimPrefix(subsystem, CgroupNamePrefix)
    60  
    61  	// Make sure we're still under defaultPrefix, and resolve
    62  	// a possible symlink (like cpu -> cpu,cpuacct).
    63  	path, err := securejoin.SecureJoin(defaultPrefix, subsystem)
    64  	if err != nil {
    65  		return ""
    66  	}
    67  
    68  	// (1) path should be a directory.
    69  	st, err := os.Lstat(path)
    70  	if err != nil || !st.IsDir() {
    71  		return ""
    72  	}
    73  
    74  	// (2) path should be a mount point.
    75  	pst, err := os.Lstat(filepath.Dir(path))
    76  	if err != nil {
    77  		return ""
    78  	}
    79  
    80  	if st.Sys().(*syscall.Stat_t).Dev == pst.Sys().(*syscall.Stat_t).Dev {
    81  		// parent dir has the same dev -- path is not a mount point
    82  		return ""
    83  	}
    84  
    85  	// (3) path should have 'cgroup' fs type.
    86  	fst := unix.Statfs_t{}
    87  	err = unix.Statfs(path, &fst)
    88  	if err != nil || fst.Type != unix.CGROUP_SUPER_MAGIC {
    89  		return ""
    90  	}
    91  
    92  	return path
    93  }
    94  
    95  // readCgroupMountinfo returns a list of cgroup v1 mounts (i.e. the ones
    96  // with fstype of "cgroup") for the current running process.
    97  //
    98  // The results are cached (to avoid re-reading mountinfo which is relatively
    99  // expensive), so it is assumed that cgroup mounts are not being changed.
   100  func readCgroupMountinfo() ([]*mountinfo.Info, error) {
   101  	readMountinfoOnce.Do(func() {
   102  		// mountinfo.GetMounts uses /proc/thread-self, so we can use it without
   103  		// issues.
   104  		cgroupMountinfo, readMountinfoErr = mountinfo.GetMounts(
   105  			mountinfo.FSTypeFilter("cgroup"),
   106  		)
   107  	})
   108  	return cgroupMountinfo, readMountinfoErr
   109  }
   110  
   111  // https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt
   112  func FindCgroupMountpoint(cgroupPath, subsystem string) (string, error) {
   113  	if IsCgroup2UnifiedMode() {
   114  		return "", errUnified
   115  	}
   116  
   117  	// If subsystem is empty, we look for the cgroupv2 hybrid path.
   118  	if len(subsystem) == 0 {
   119  		return hybridMountpoint, nil
   120  	}
   121  
   122  	// Avoid parsing mountinfo by trying the default path first, if possible.
   123  	if path := tryDefaultPath(cgroupPath, subsystem); path != "" {
   124  		return path, nil
   125  	}
   126  
   127  	mnt, _, err := FindCgroupMountpointAndRoot(cgroupPath, subsystem)
   128  	return mnt, err
   129  }
   130  
   131  func FindCgroupMountpointAndRoot(cgroupPath, subsystem string) (string, string, error) {
   132  	if IsCgroup2UnifiedMode() {
   133  		return "", "", errUnified
   134  	}
   135  
   136  	mi, err := readCgroupMountinfo()
   137  	if err != nil {
   138  		return "", "", err
   139  	}
   140  
   141  	return findCgroupMountpointAndRootFromMI(mi, cgroupPath, subsystem)
   142  }
   143  
   144  func findCgroupMountpointAndRootFromMI(mounts []*mountinfo.Info, cgroupPath, subsystem string) (string, string, error) {
   145  	for _, mi := range mounts {
   146  		if strings.HasPrefix(mi.Mountpoint, cgroupPath) {
   147  			for _, opt := range strings.Split(mi.VFSOptions, ",") {
   148  				if opt == subsystem {
   149  					return mi.Mountpoint, mi.Root, nil
   150  				}
   151  			}
   152  		}
   153  	}
   154  
   155  	return "", "", NewNotFoundError(subsystem)
   156  }
   157  
   158  func (m Mount) GetOwnCgroup(cgroups map[string]string) (string, error) {
   159  	if len(m.Subsystems) == 0 {
   160  		return "", errors.New("no subsystem for mount")
   161  	}
   162  
   163  	return getControllerPath(m.Subsystems[0], cgroups)
   164  }
   165  
   166  func getCgroupMountsHelper(ss map[string]bool, mounts []*mountinfo.Info, all bool) ([]Mount, error) {
   167  	res := make([]Mount, 0, len(ss))
   168  	numFound := 0
   169  	for _, mi := range mounts {
   170  		m := Mount{
   171  			Mountpoint: mi.Mountpoint,
   172  			Root:       mi.Root,
   173  		}
   174  		for _, opt := range strings.Split(mi.VFSOptions, ",") {
   175  			seen, known := ss[opt]
   176  			if !known || (!all && seen) {
   177  				continue
   178  			}
   179  			ss[opt] = true
   180  			opt = strings.TrimPrefix(opt, CgroupNamePrefix)
   181  			m.Subsystems = append(m.Subsystems, opt)
   182  			numFound++
   183  		}
   184  		if len(m.Subsystems) > 0 || all {
   185  			res = append(res, m)
   186  		}
   187  		if !all && numFound >= len(ss) {
   188  			break
   189  		}
   190  	}
   191  	return res, nil
   192  }
   193  
   194  func getCgroupMountsV1(all bool) ([]Mount, error) {
   195  	mi, err := readCgroupMountinfo()
   196  	if err != nil {
   197  		return nil, err
   198  	}
   199  
   200  	// We don't need to use /proc/thread-self here because runc always runs
   201  	// with every thread in the same cgroup. This lets us avoid having to do
   202  	// runtime.LockOSThread.
   203  	allSubsystems, err := ParseCgroupFile("/proc/self/cgroup")
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	allMap := make(map[string]bool)
   209  	for s := range allSubsystems {
   210  		allMap[s] = false
   211  	}
   212  
   213  	return getCgroupMountsHelper(allMap, mi, all)
   214  }
   215  
   216  // GetOwnCgroup returns the relative path to the cgroup docker is running in.
   217  func GetOwnCgroup(subsystem string) (string, error) {
   218  	if IsCgroup2UnifiedMode() {
   219  		return "", errUnified
   220  	}
   221  
   222  	// We don't need to use /proc/thread-self here because runc always runs
   223  	// with every thread in the same cgroup. This lets us avoid having to do
   224  	// runtime.LockOSThread.
   225  	cgroups, err := ParseCgroupFile("/proc/self/cgroup")
   226  	if err != nil {
   227  		return "", err
   228  	}
   229  
   230  	return getControllerPath(subsystem, cgroups)
   231  }
   232  
   233  func GetOwnCgroupPath(subsystem string) (string, error) {
   234  	cgroup, err := GetOwnCgroup(subsystem)
   235  	if err != nil {
   236  		return "", err
   237  	}
   238  
   239  	// If subsystem is empty, we look for the cgroupv2 hybrid path.
   240  	if len(subsystem) == 0 {
   241  		return hybridMountpoint, nil
   242  	}
   243  
   244  	return getCgroupPathHelper(subsystem, cgroup)
   245  }
   246  
   247  func getCgroupPathHelper(subsystem, cgroup string) (string, error) {
   248  	mnt, root, err := FindCgroupMountpointAndRoot("", subsystem)
   249  	if err != nil {
   250  		return "", err
   251  	}
   252  
   253  	// This is needed for nested containers, because in /proc/self/cgroup we
   254  	// see paths from host, which don't exist in container.
   255  	relCgroup, err := filepath.Rel(root, cgroup)
   256  	if err != nil {
   257  		return "", err
   258  	}
   259  
   260  	return filepath.Join(mnt, relCgroup), nil
   261  }
   262  
   263  func getControllerPath(subsystem string, cgroups map[string]string) (string, error) {
   264  	if IsCgroup2UnifiedMode() {
   265  		return "", errUnified
   266  	}
   267  
   268  	if p, ok := cgroups[subsystem]; ok {
   269  		return p, nil
   270  	}
   271  
   272  	if p, ok := cgroups[CgroupNamePrefix+subsystem]; ok {
   273  		return p, nil
   274  	}
   275  
   276  	return "", NewNotFoundError(subsystem)
   277  }