github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/libcontainer/cgroups/v1_utils.go (about) 1 package cgroups 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 "sync" 10 "syscall" 11 12 securejoin "github.com/cyphar/filepath-securejoin" 13 "github.com/moby/sys/mountinfo" 14 "golang.org/x/sys/unix" 15 ) 16 17 // Code in this source file are specific to cgroup v1, 18 // and must not be used from any cgroup v2 code. 19 20 const ( 21 CgroupNamePrefix = "name=" 22 defaultPrefix = "/sys/fs/cgroup" 23 ) 24 25 var ( 26 errUnified = errors.New("not implemented for cgroup v2 unified hierarchy") 27 ErrV1NoUnified = errors.New("invalid configuration: cannot use unified on cgroup v1") 28 29 readMountinfoOnce sync.Once 30 readMountinfoErr error 31 cgroupMountinfo []*mountinfo.Info 32 ) 33 34 type NotFoundError struct { 35 Subsystem string 36 } 37 38 func (e *NotFoundError) Error() string { 39 return fmt.Sprintf("mountpoint for %s not found", e.Subsystem) 40 } 41 42 func NewNotFoundError(sub string) error { 43 return &NotFoundError{ 44 Subsystem: sub, 45 } 46 } 47 48 func IsNotFound(err error) bool { 49 var nfErr *NotFoundError 50 return errors.As(err, &nfErr) 51 } 52 53 func tryDefaultPath(cgroupPath, subsystem string) string { 54 if !strings.HasPrefix(defaultPrefix, cgroupPath) { 55 return "" 56 } 57 58 // remove possible prefix 59 subsystem = strings.TrimPrefix(subsystem, CgroupNamePrefix) 60 61 // Make sure we're still under defaultPrefix, and resolve 62 // a possible symlink (like cpu -> cpu,cpuacct). 63 path, err := securejoin.SecureJoin(defaultPrefix, subsystem) 64 if err != nil { 65 return "" 66 } 67 68 // (1) path should be a directory. 69 st, err := os.Lstat(path) 70 if err != nil || !st.IsDir() { 71 return "" 72 } 73 74 // (2) path should be a mount point. 75 pst, err := os.Lstat(filepath.Dir(path)) 76 if err != nil { 77 return "" 78 } 79 80 if st.Sys().(*syscall.Stat_t).Dev == pst.Sys().(*syscall.Stat_t).Dev { 81 // parent dir has the same dev -- path is not a mount point 82 return "" 83 } 84 85 // (3) path should have 'cgroup' fs type. 86 fst := unix.Statfs_t{} 87 err = unix.Statfs(path, &fst) 88 if err != nil || fst.Type != unix.CGROUP_SUPER_MAGIC { 89 return "" 90 } 91 92 return path 93 } 94 95 // readCgroupMountinfo returns a list of cgroup v1 mounts (i.e. the ones 96 // with fstype of "cgroup") for the current running process. 97 // 98 // The results are cached (to avoid re-reading mountinfo which is relatively 99 // expensive), so it is assumed that cgroup mounts are not being changed. 100 func readCgroupMountinfo() ([]*mountinfo.Info, error) { 101 readMountinfoOnce.Do(func() { 102 // mountinfo.GetMounts uses /proc/thread-self, so we can use it without 103 // issues. 104 cgroupMountinfo, readMountinfoErr = mountinfo.GetMounts( 105 mountinfo.FSTypeFilter("cgroup"), 106 ) 107 }) 108 return cgroupMountinfo, readMountinfoErr 109 } 110 111 // https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt 112 func FindCgroupMountpoint(cgroupPath, subsystem string) (string, error) { 113 if IsCgroup2UnifiedMode() { 114 return "", errUnified 115 } 116 117 // If subsystem is empty, we look for the cgroupv2 hybrid path. 118 if len(subsystem) == 0 { 119 return hybridMountpoint, nil 120 } 121 122 // Avoid parsing mountinfo by trying the default path first, if possible. 123 if path := tryDefaultPath(cgroupPath, subsystem); path != "" { 124 return path, nil 125 } 126 127 mnt, _, err := FindCgroupMountpointAndRoot(cgroupPath, subsystem) 128 return mnt, err 129 } 130 131 func FindCgroupMountpointAndRoot(cgroupPath, subsystem string) (string, string, error) { 132 if IsCgroup2UnifiedMode() { 133 return "", "", errUnified 134 } 135 136 mi, err := readCgroupMountinfo() 137 if err != nil { 138 return "", "", err 139 } 140 141 return findCgroupMountpointAndRootFromMI(mi, cgroupPath, subsystem) 142 } 143 144 func findCgroupMountpointAndRootFromMI(mounts []*mountinfo.Info, cgroupPath, subsystem string) (string, string, error) { 145 for _, mi := range mounts { 146 if strings.HasPrefix(mi.Mountpoint, cgroupPath) { 147 for _, opt := range strings.Split(mi.VFSOptions, ",") { 148 if opt == subsystem { 149 return mi.Mountpoint, mi.Root, nil 150 } 151 } 152 } 153 } 154 155 return "", "", NewNotFoundError(subsystem) 156 } 157 158 func (m Mount) GetOwnCgroup(cgroups map[string]string) (string, error) { 159 if len(m.Subsystems) == 0 { 160 return "", errors.New("no subsystem for mount") 161 } 162 163 return getControllerPath(m.Subsystems[0], cgroups) 164 } 165 166 func getCgroupMountsHelper(ss map[string]bool, mounts []*mountinfo.Info, all bool) ([]Mount, error) { 167 res := make([]Mount, 0, len(ss)) 168 numFound := 0 169 for _, mi := range mounts { 170 m := Mount{ 171 Mountpoint: mi.Mountpoint, 172 Root: mi.Root, 173 } 174 for _, opt := range strings.Split(mi.VFSOptions, ",") { 175 seen, known := ss[opt] 176 if !known || (!all && seen) { 177 continue 178 } 179 ss[opt] = true 180 opt = strings.TrimPrefix(opt, CgroupNamePrefix) 181 m.Subsystems = append(m.Subsystems, opt) 182 numFound++ 183 } 184 if len(m.Subsystems) > 0 || all { 185 res = append(res, m) 186 } 187 if !all && numFound >= len(ss) { 188 break 189 } 190 } 191 return res, nil 192 } 193 194 func getCgroupMountsV1(all bool) ([]Mount, error) { 195 mi, err := readCgroupMountinfo() 196 if err != nil { 197 return nil, err 198 } 199 200 // We don't need to use /proc/thread-self here because runc always runs 201 // with every thread in the same cgroup. This lets us avoid having to do 202 // runtime.LockOSThread. 203 allSubsystems, err := ParseCgroupFile("/proc/self/cgroup") 204 if err != nil { 205 return nil, err 206 } 207 208 allMap := make(map[string]bool) 209 for s := range allSubsystems { 210 allMap[s] = false 211 } 212 213 return getCgroupMountsHelper(allMap, mi, all) 214 } 215 216 // GetOwnCgroup returns the relative path to the cgroup docker is running in. 217 func GetOwnCgroup(subsystem string) (string, error) { 218 if IsCgroup2UnifiedMode() { 219 return "", errUnified 220 } 221 222 // We don't need to use /proc/thread-self here because runc always runs 223 // with every thread in the same cgroup. This lets us avoid having to do 224 // runtime.LockOSThread. 225 cgroups, err := ParseCgroupFile("/proc/self/cgroup") 226 if err != nil { 227 return "", err 228 } 229 230 return getControllerPath(subsystem, cgroups) 231 } 232 233 func GetOwnCgroupPath(subsystem string) (string, error) { 234 cgroup, err := GetOwnCgroup(subsystem) 235 if err != nil { 236 return "", err 237 } 238 239 // If subsystem is empty, we look for the cgroupv2 hybrid path. 240 if len(subsystem) == 0 { 241 return hybridMountpoint, nil 242 } 243 244 return getCgroupPathHelper(subsystem, cgroup) 245 } 246 247 func getCgroupPathHelper(subsystem, cgroup string) (string, error) { 248 mnt, root, err := FindCgroupMountpointAndRoot("", subsystem) 249 if err != nil { 250 return "", err 251 } 252 253 // This is needed for nested containers, because in /proc/self/cgroup we 254 // see paths from host, which don't exist in container. 255 relCgroup, err := filepath.Rel(root, cgroup) 256 if err != nil { 257 return "", err 258 } 259 260 return filepath.Join(mnt, relCgroup), nil 261 } 262 263 func getControllerPath(subsystem string, cgroups map[string]string) (string, error) { 264 if IsCgroup2UnifiedMode() { 265 return "", errUnified 266 } 267 268 if p, ok := cgroups[subsystem]; ok { 269 return p, nil 270 } 271 272 if p, ok := cgroups[CgroupNamePrefix+subsystem]; ok { 273 return p, nil 274 } 275 276 return "", NewNotFoundError(subsystem) 277 }