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 }