file.go 5.3 KB

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