123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191 |
- package cgroups
- import (
- "bytes"
- "errors"
- "fmt"
- "os"
- "path"
- "strconv"
- "strings"
- "sync"
- "github.com/opencontainers/runc/libcontainer/utils"
- "github.com/sirupsen/logrus"
- "golang.org/x/sys/unix"
- )
- // OpenFile opens a cgroup file in a given dir with given flags.
- // It is supposed to be used for cgroup files only, and returns
- // an error if the file is not a cgroup file.
- //
- // Arguments dir and file are joined together to form an absolute path
- // to a file being opened.
- func OpenFile(dir, file string, flags int) (*os.File, error) {
- if dir == "" {
- return nil, fmt.Errorf("no directory specified for %s", file)
- }
- return openFile(dir, file, flags)
- }
- // ReadFile reads data from a cgroup file in dir.
- // It is supposed to be used for cgroup files only.
- func ReadFile(dir, file string) (string, error) {
- fd, err := OpenFile(dir, file, unix.O_RDONLY)
- if err != nil {
- return "", err
- }
- defer fd.Close()
- var buf bytes.Buffer
- _, err = buf.ReadFrom(fd)
- return buf.String(), err
- }
- // WriteFile writes data to a cgroup file in dir.
- // It is supposed to be used for cgroup files only.
- func WriteFile(dir, file, data string) error {
- fd, err := OpenFile(dir, file, unix.O_WRONLY)
- if err != nil {
- return err
- }
- defer fd.Close()
- if err := retryingWriteFile(fd, data); err != nil {
- // Having data in the error message helps in debugging.
- return fmt.Errorf("failed to write %q: %w", data, err)
- }
- return nil
- }
- func retryingWriteFile(fd *os.File, data string) error {
- for {
- _, err := fd.Write([]byte(data))
- if errors.Is(err, unix.EINTR) {
- logrus.Infof("interrupted while writing %s to %s", data, fd.Name())
- continue
- }
- return err
- }
- }
- const (
- cgroupfsDir = "/sys/fs/cgroup"
- cgroupfsPrefix = cgroupfsDir + "/"
- )
- var (
- // TestMode is set to true by unit tests that need "fake" cgroupfs.
- TestMode bool
- cgroupFd int = -1
- prepOnce sync.Once
- prepErr error
- resolveFlags uint64
- )
- func prepareOpenat2() error {
- prepOnce.Do(func() {
- fd, err := unix.Openat2(-1, cgroupfsDir, &unix.OpenHow{
- Flags: unix.O_DIRECTORY | unix.O_PATH,
- })
- if err != nil {
- prepErr = &os.PathError{Op: "openat2", Path: cgroupfsDir, Err: err}
- if err != unix.ENOSYS { //nolint:errorlint // unix errors are bare
- logrus.Warnf("falling back to securejoin: %s", prepErr)
- } else {
- logrus.Debug("openat2 not available, falling back to securejoin")
- }
- return
- }
- var st unix.Statfs_t
- if err = unix.Fstatfs(fd, &st); err != nil {
- prepErr = &os.PathError{Op: "statfs", Path: cgroupfsDir, Err: err}
- logrus.Warnf("falling back to securejoin: %s", prepErr)
- return
- }
- cgroupFd = fd
- resolveFlags = unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS
- if st.Type == unix.CGROUP2_SUPER_MAGIC {
- // cgroupv2 has a single mountpoint and no "cpu,cpuacct" symlinks
- resolveFlags |= unix.RESOLVE_NO_XDEV | unix.RESOLVE_NO_SYMLINKS
- }
- })
- return prepErr
- }
- func openFile(dir, file string, flags int) (*os.File, error) {
- mode := os.FileMode(0)
- if TestMode && flags&os.O_WRONLY != 0 {
- // "emulate" cgroup fs for unit tests
- flags |= os.O_TRUNC | os.O_CREATE
- mode = 0o600
- }
- path := path.Join(dir, utils.CleanPath(file))
- if prepareOpenat2() != nil {
- return openFallback(path, flags, mode)
- }
- relPath := strings.TrimPrefix(path, cgroupfsPrefix)
- if len(relPath) == len(path) { // non-standard path, old system?
- return openFallback(path, flags, mode)
- }
- fd, err := unix.Openat2(cgroupFd, relPath,
- &unix.OpenHow{
- Resolve: resolveFlags,
- Flags: uint64(flags) | unix.O_CLOEXEC,
- Mode: uint64(mode),
- })
- if err != nil {
- err = &os.PathError{Op: "openat2", Path: path, Err: err}
- // Check if cgroupFd is still opened to cgroupfsDir
- // (happens when this package is incorrectly used
- // across the chroot/pivot_root/mntns boundary, or
- // when /sys/fs/cgroup is remounted).
- //
- // TODO: if such usage will ever be common, amend this
- // to reopen cgroupFd and retry openat2.
- fdStr := strconv.Itoa(cgroupFd)
- fdDest, _ := os.Readlink("/proc/self/fd/" + fdStr)
- if fdDest != cgroupfsDir {
- // Wrap the error so it is clear that cgroupFd
- // is opened to an unexpected/wrong directory.
- err = fmt.Errorf("cgroupFd %s unexpectedly opened to %s != %s: %w",
- fdStr, fdDest, cgroupfsDir, err)
- }
- return nil, err
- }
- return os.NewFile(uintptr(fd), path), nil
- }
- var errNotCgroupfs = errors.New("not a cgroup file")
- // Can be changed by unit tests.
- var openFallback = openAndCheck
- // openAndCheck is used when openat2(2) is not available. It checks the opened
- // file is on cgroupfs, returning an error otherwise.
- func openAndCheck(path string, flags int, mode os.FileMode) (*os.File, error) {
- fd, err := os.OpenFile(path, flags, mode)
- if err != nil {
- return nil, err
- }
- if TestMode {
- return fd, nil
- }
- // Check this is a cgroupfs file.
- var st unix.Statfs_t
- if err := unix.Fstatfs(int(fd.Fd()), &st); err != nil {
- _ = fd.Close()
- return nil, &os.PathError{Op: "statfs", Path: path, Err: err}
- }
- if st.Type != unix.CGROUP_SUPER_MAGIC && st.Type != unix.CGROUP2_SUPER_MAGIC {
- _ = fd.Close()
- return nil, &os.PathError{Op: "open", Path: path, Err: errNotCgroupfs}
- }
- return fd, nil
- }
|