...

Source file src/github.com/opencontainers/runc/libcontainer/cgroups/file.go

Documentation: github.com/opencontainers/runc/libcontainer/cgroups

     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 := retryingWriteFile(fd, 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  func retryingWriteFile(fd *os.File, data string) error {
    61  	for {
    62  		_, err := fd.Write([]byte(data))
    63  		if errors.Is(err, unix.EINTR) {
    64  			logrus.Infof("interrupted while writing %s to %s", data, fd.Name())
    65  			continue
    66  		}
    67  		return err
    68  	}
    69  }
    70  
    71  const (
    72  	cgroupfsDir    = "/sys/fs/cgroup"
    73  	cgroupfsPrefix = cgroupfsDir + "/"
    74  )
    75  
    76  var (
    77  	// TestMode is set to true by unit tests that need "fake" cgroupfs.
    78  	TestMode bool
    79  
    80  	cgroupRootHandle *os.File
    81  	prepOnce         sync.Once
    82  	prepErr          error
    83  	resolveFlags     uint64
    84  )
    85  
    86  func prepareOpenat2() error {
    87  	prepOnce.Do(func() {
    88  		fd, err := unix.Openat2(-1, cgroupfsDir, &unix.OpenHow{
    89  			Flags: unix.O_DIRECTORY | unix.O_PATH | unix.O_CLOEXEC,
    90  		})
    91  		if err != nil {
    92  			prepErr = &os.PathError{Op: "openat2", Path: cgroupfsDir, Err: err}
    93  			if err != unix.ENOSYS { //nolint:errorlint // unix errors are bare
    94  				logrus.Warnf("falling back to securejoin: %s", prepErr)
    95  			} else {
    96  				logrus.Debug("openat2 not available, falling back to securejoin")
    97  			}
    98  			return
    99  		}
   100  		file := os.NewFile(uintptr(fd), cgroupfsDir)
   101  
   102  		var st unix.Statfs_t
   103  		if err := unix.Fstatfs(int(file.Fd()), &st); err != nil {
   104  			prepErr = &os.PathError{Op: "statfs", Path: cgroupfsDir, Err: err}
   105  			logrus.Warnf("falling back to securejoin: %s", prepErr)
   106  			return
   107  		}
   108  
   109  		cgroupRootHandle = file
   110  		resolveFlags = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS
   111  		if st.Type == unix.CGROUP2_SUPER_MAGIC {
   112  			// cgroupv2 has a single mountpoint and no "cpu,cpuacct" symlinks
   113  			resolveFlags |= unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_SYMLINKS
   114  		}
   115  	})
   116  
   117  	return prepErr
   118  }
   119  
   120  func openFile(dir, file string, flags int) (*os.File, error) {
   121  	mode := os.FileMode(0)
   122  	if TestMode && flags&os.O_WRONLY != 0 {
   123  		// "emulate" cgroup fs for unit tests
   124  		flags |= os.O_TRUNC | os.O_CREATE
   125  		mode = 0o600
   126  	}
   127  	path := path.Join(dir, utils.CleanPath(file))
   128  	if prepareOpenat2() != nil {
   129  		return openFallback(path, flags, mode)
   130  	}
   131  	relPath := strings.TrimPrefix(path, cgroupfsPrefix)
   132  	if len(relPath) == len(path) { // non-standard path, old system?
   133  		return openFallback(path, flags, mode)
   134  	}
   135  
   136  	fd, err := unix.Openat2(int(cgroupRootHandle.Fd()), relPath,
   137  		&unix.OpenHow{
   138  			Resolve: resolveFlags,
   139  			Flags:   uint64(flags) | unix.O_CLOEXEC,
   140  			Mode:    uint64(mode),
   141  		})
   142  	if err != nil {
   143  		err = &os.PathError{Op: "openat2", Path: path, Err: err}
   144  		// Check if cgroupRootHandle is still opened to cgroupfsDir
   145  		// (happens when this package is incorrectly used
   146  		// across the chroot/pivot_root/mntns boundary, or
   147  		// when /sys/fs/cgroup is remounted).
   148  		//
   149  		// TODO: if such usage will ever be common, amend this
   150  		// to reopen cgroupRootHandle and retry openat2.
   151  		fdStr := strconv.Itoa(int(cgroupRootHandle.Fd()))
   152  		fdDest, _ := os.Readlink("/proc/self/fd/" + fdStr)
   153  		if fdDest != cgroupfsDir {
   154  			// Wrap the error so it is clear that cgroupRootHandle
   155  			// is opened to an unexpected/wrong directory.
   156  			err = fmt.Errorf("cgroupRootHandle %d unexpectedly opened to %s != %s: %w",
   157  				cgroupRootHandle.Fd(), fdDest, cgroupfsDir, err)
   158  		}
   159  		return nil, err
   160  	}
   161  
   162  	return os.NewFile(uintptr(fd), path), nil
   163  }
   164  
   165  var errNotCgroupfs = errors.New("not a cgroup file")
   166  
   167  // Can be changed by unit tests.
   168  var openFallback = openAndCheck
   169  
   170  // openAndCheck is used when openat2(2) is not available. It checks the opened
   171  // file is on cgroupfs, returning an error otherwise.
   172  func openAndCheck(path string, flags int, mode os.FileMode) (*os.File, error) {
   173  	fd, err := os.OpenFile(path, flags, mode)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  	if TestMode {
   178  		return fd, nil
   179  	}
   180  	// Check this is a cgroupfs file.
   181  	var st unix.Statfs_t
   182  	if err := unix.Fstatfs(int(fd.Fd()), &st); err != nil {
   183  		_ = fd.Close()
   184  		return nil, &os.PathError{Op: "statfs", Path: path, Err: err}
   185  	}
   186  	if st.Type != unix.CGROUP_SUPER_MAGIC && st.Type != unix.CGROUP2_SUPER_MAGIC {
   187  		_ = fd.Close()
   188  		return nil, &os.PathError{Op: "open", Path: path, Err: errNotCgroupfs}
   189  	}
   190  
   191  	return fd, nil
   192  }
   193  

View as plain text