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

     1  package cgroups
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"path"
     9  	"strconv"
    10  	"strings"
    11  	"sync"
    12  
    13  	"github.com/opencontainers/runc/libcontainer/utils"
    14  	"github.com/sirupsen/logrus"
    15  	"golang.org/x/sys/unix"
    16  )
    17  
    18  // OpenFile opens a cgroup file in a given dir with given flags.
    19  // It is supposed to be used for cgroup files only, and returns
    20  // an error if the file is not a cgroup file.
    21  //
    22  // Arguments dir and file are joined together to form an absolute path
    23  // to a file being opened.
    24  func OpenFile(dir, file string, flags int) (*os.File, error) {
    25  	if dir == "" {
    26  		return nil, fmt.Errorf("no directory specified for %s", file)
    27  	}
    28  	return openFile(dir, file, flags)
    29  }
    30  
    31  // ReadFile reads data from a cgroup file in dir.
    32  // It is supposed to be used for cgroup files only.
    33  func ReadFile(dir, file string) (string, error) {
    34  	fd, err := OpenFile(dir, file, unix.O_RDONLY)
    35  	if err != nil {
    36  		return "", err
    37  	}
    38  	defer fd.Close()
    39  	var buf bytes.Buffer
    40  
    41  	_, err = buf.ReadFrom(fd)
    42  	return buf.String(), err
    43  }
    44  
    45  // WriteFile writes data to a cgroup file in dir.
    46  // It is supposed to be used for cgroup files only.
    47  func WriteFile(dir, file, data string) error {
    48  	fd, err := OpenFile(dir, file, unix.O_WRONLY)
    49  	if err != nil {
    50  		return err
    51  	}
    52  	defer fd.Close()
    53  	if _, err := fd.WriteString(data); err != nil {
    54  		// Having data in the error message helps in debugging.
    55  		return fmt.Errorf("failed to write %q: %w", data, err)
    56  	}
    57  	return nil
    58  }
    59  
    60  const (
    61  	cgroupfsDir    = "/sys/fs/cgroup"
    62  	cgroupfsPrefix = cgroupfsDir + "/"
    63  )
    64  
    65  var (
    66  	// TestMode is set to true by unit tests that need "fake" cgroupfs.
    67  	TestMode bool
    68  
    69  	cgroupRootHandle *os.File
    70  	prepOnce         sync.Once
    71  	prepErr          error
    72  	resolveFlags     uint64
    73  )
    74  
    75  func prepareOpenat2() error {
    76  	prepOnce.Do(func() {
    77  		fd, err := unix.Openat2(-1, cgroupfsDir, &unix.OpenHow{
    78  			Flags: unix.O_DIRECTORY | unix.O_PATH | unix.O_CLOEXEC,
    79  		})
    80  		if err != nil {
    81  			prepErr = &os.PathError{Op: "openat2", Path: cgroupfsDir, Err: err}
    82  			if err != unix.ENOSYS {
    83  				logrus.Warnf("falling back to securejoin: %s", prepErr)
    84  			} else {
    85  				logrus.Debug("openat2 not available, falling back to securejoin")
    86  			}
    87  			return
    88  		}
    89  		file := os.NewFile(uintptr(fd), cgroupfsDir)
    90  
    91  		var st unix.Statfs_t
    92  		if err := unix.Fstatfs(int(file.Fd()), &st); err != nil {
    93  			prepErr = &os.PathError{Op: "statfs", Path: cgroupfsDir, Err: err}
    94  			logrus.Warnf("falling back to securejoin: %s", prepErr)
    95  			return
    96  		}
    97  
    98  		cgroupRootHandle = file
    99  		resolveFlags = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS
   100  		if st.Type == unix.CGROUP2_SUPER_MAGIC {
   101  			// cgroupv2 has a single mountpoint and no "cpu,cpuacct" symlinks
   102  			resolveFlags |= unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_SYMLINKS
   103  		}
   104  	})
   105  
   106  	return prepErr
   107  }
   108  
   109  func openFile(dir, file string, flags int) (*os.File, error) {
   110  	mode := os.FileMode(0)
   111  	if TestMode && flags&os.O_WRONLY != 0 {
   112  		// "emulate" cgroup fs for unit tests
   113  		flags |= os.O_TRUNC | os.O_CREATE
   114  		mode = 0o600
   115  	}
   116  	path := path.Join(dir, utils.CleanPath(file))
   117  	if prepareOpenat2() != nil {
   118  		return openFallback(path, flags, mode)
   119  	}
   120  	relPath := strings.TrimPrefix(path, cgroupfsPrefix)
   121  	if len(relPath) == len(path) { // non-standard path, old system?
   122  		return openFallback(path, flags, mode)
   123  	}
   124  
   125  	fd, err := unix.Openat2(int(cgroupRootHandle.Fd()), relPath,
   126  		&unix.OpenHow{
   127  			Resolve: resolveFlags,
   128  			Flags:   uint64(flags) | unix.O_CLOEXEC,
   129  			Mode:    uint64(mode),
   130  		})
   131  	if err != nil {
   132  		err = &os.PathError{Op: "openat2", Path: path, Err: err}
   133  		// Check if cgroupRootHandle is still opened to cgroupfsDir
   134  		// (happens when this package is incorrectly used
   135  		// across the chroot/pivot_root/mntns boundary, or
   136  		// when /sys/fs/cgroup is remounted).
   137  		//
   138  		// TODO: if such usage will ever be common, amend this
   139  		// to reopen cgroupRootHandle and retry openat2.
   140  		fdPath, closer := utils.ProcThreadSelf("fd/" + strconv.Itoa(int(cgroupRootHandle.Fd())))
   141  		defer closer()
   142  		fdDest, _ := os.Readlink(fdPath)
   143  		if fdDest != cgroupfsDir {
   144  			// Wrap the error so it is clear that cgroupRootHandle
   145  			// is opened to an unexpected/wrong directory.
   146  			err = fmt.Errorf("cgroupRootHandle %d unexpectedly opened to %s != %s: %w",
   147  				cgroupRootHandle.Fd(), fdDest, cgroupfsDir, err)
   148  		}
   149  		return nil, err
   150  	}
   151  
   152  	return os.NewFile(uintptr(fd), path), nil
   153  }
   154  
   155  var errNotCgroupfs = errors.New("not a cgroup file")
   156  
   157  // Can be changed by unit tests.
   158  var openFallback = openAndCheck
   159  
   160  // openAndCheck is used when openat2(2) is not available. It checks the opened
   161  // file is on cgroupfs, returning an error otherwise.
   162  func openAndCheck(path string, flags int, mode os.FileMode) (*os.File, error) {
   163  	fd, err := os.OpenFile(path, flags, mode)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  	if TestMode {
   168  		return fd, nil
   169  	}
   170  	// Check this is a cgroupfs file.
   171  	var st unix.Statfs_t
   172  	if err := unix.Fstatfs(int(fd.Fd()), &st); err != nil {
   173  		_ = fd.Close()
   174  		return nil, &os.PathError{Op: "statfs", Path: path, Err: err}
   175  	}
   176  	if st.Type != unix.CGROUP_SUPER_MAGIC && st.Type != unix.CGROUP2_SUPER_MAGIC {
   177  		_ = fd.Close()
   178  		return nil, &os.PathError{Op: "open", Path: path, Err: errNotCgroupfs}
   179  	}
   180  
   181  	return fd, nil
   182  }