github.com/kaisenlinux/docker.io@v0.0.0-20230510090727-ea55db55fac7/engine/quota/projectquota.go (about) 1 //go:build linux && !exclude_disk_quota && cgo 2 // +build linux,!exclude_disk_quota,cgo 3 4 // 5 // projectquota.go - implements XFS project quota controls 6 // for setting quota limits on a newly created directory. 7 // It currently supports the legacy XFS specific ioctls. 8 // 9 // TODO: use generic quota control ioctl FS_IOC_FS{GET,SET}XATTR 10 // for both xfs/ext4 for kernel version >= v4.5 11 // 12 13 package quota // import "github.com/docker/docker/quota" 14 15 /* 16 #include <stdlib.h> 17 #include <dirent.h> 18 #include <linux/fs.h> 19 #include <linux/quota.h> 20 #include <linux/dqblk_xfs.h> 21 22 #ifndef FS_XFLAG_PROJINHERIT 23 struct fsxattr { 24 __u32 fsx_xflags; 25 __u32 fsx_extsize; 26 __u32 fsx_nextents; 27 __u32 fsx_projid; 28 unsigned char fsx_pad[12]; 29 }; 30 #define FS_XFLAG_PROJINHERIT 0x00000200 31 #endif 32 #ifndef FS_IOC_FSGETXATTR 33 #define FS_IOC_FSGETXATTR _IOR ('X', 31, struct fsxattr) 34 #endif 35 #ifndef FS_IOC_FSSETXATTR 36 #define FS_IOC_FSSETXATTR _IOW ('X', 32, struct fsxattr) 37 #endif 38 39 #ifndef PRJQUOTA 40 #define PRJQUOTA 2 41 #endif 42 #ifndef XFS_PROJ_QUOTA 43 #define XFS_PROJ_QUOTA 2 44 #endif 45 #ifndef Q_XSETPQLIM 46 #define Q_XSETPQLIM QCMD(Q_XSETQLIM, PRJQUOTA) 47 #endif 48 #ifndef Q_XGETPQUOTA 49 #define Q_XGETPQUOTA QCMD(Q_XGETQUOTA, PRJQUOTA) 50 #endif 51 52 const int Q_XGETQSTAT_PRJQUOTA = QCMD(Q_XGETQSTAT, PRJQUOTA); 53 */ 54 import "C" 55 import ( 56 "os" 57 "path" 58 "path/filepath" 59 "sync" 60 "unsafe" 61 62 "github.com/containerd/containerd/sys" 63 "github.com/pkg/errors" 64 "github.com/sirupsen/logrus" 65 "golang.org/x/sys/unix" 66 ) 67 68 type pquotaState struct { 69 sync.Mutex 70 nextProjectID uint32 71 } 72 73 var pquotaStateInst *pquotaState 74 var pquotaStateOnce sync.Once 75 76 // getPquotaState - get global pquota state tracker instance 77 func getPquotaState() *pquotaState { 78 pquotaStateOnce.Do(func() { 79 pquotaStateInst = &pquotaState{ 80 nextProjectID: 1, 81 } 82 }) 83 return pquotaStateInst 84 } 85 86 // registerBasePath - register a new base path and update nextProjectID 87 func (state *pquotaState) updateMinProjID(minProjectID uint32) { 88 state.Lock() 89 defer state.Unlock() 90 if state.nextProjectID <= minProjectID { 91 state.nextProjectID = minProjectID + 1 92 } 93 } 94 95 // NewControl - initialize project quota support. 96 // Test to make sure that quota can be set on a test dir and find 97 // the first project id to be used for the next container create. 98 // 99 // Returns nil (and error) if project quota is not supported. 100 // 101 // First get the project id of the home directory. 102 // This test will fail if the backing fs is not xfs. 103 // 104 // xfs_quota tool can be used to assign a project id to the driver home directory, e.g.: 105 // 106 // echo 999:/var/lib/docker/overlay2 >> /etc/projects 107 // echo docker:999 >> /etc/projid 108 // xfs_quota -x -c 'project -s docker' /<xfs mount point> 109 // 110 // In that case, the home directory project id will be used as a "start offset" 111 // and all containers will be assigned larger project ids (e.g. >= 1000). 112 // This is a way to prevent xfs_quota management from conflicting with docker. 113 // 114 // Then try to create a test directory with the next project id and set a quota 115 // on it. If that works, continue to scan existing containers to map allocated 116 // project ids. 117 func NewControl(basePath string) (*Control, error) { 118 // 119 // If we are running in a user namespace quota won't be supported for 120 // now since makeBackingFsDev() will try to mknod(). 121 // 122 if sys.RunningInUserNS() { 123 return nil, ErrQuotaNotSupported 124 } 125 126 // 127 // create backing filesystem device node 128 // 129 backingFsBlockDev, err := makeBackingFsDev(basePath) 130 if err != nil { 131 return nil, err 132 } 133 134 // check if we can call quotactl with project quotas 135 // as a mechanism to determine (early) if we have support 136 hasQuotaSupport, err := hasQuotaSupport(backingFsBlockDev) 137 if err != nil { 138 return nil, err 139 } 140 if !hasQuotaSupport { 141 return nil, ErrQuotaNotSupported 142 } 143 144 // 145 // Get project id of parent dir as minimal id to be used by driver 146 // 147 baseProjectID, err := getProjectID(basePath) 148 if err != nil { 149 return nil, err 150 } 151 minProjectID := baseProjectID + 1 152 153 // 154 // Test if filesystem supports project quotas by trying to set 155 // a quota on the first available project id 156 // 157 quota := Quota{ 158 Size: 0, 159 } 160 if err := setProjectQuota(backingFsBlockDev, minProjectID, quota); err != nil { 161 return nil, err 162 } 163 164 q := Control{ 165 backingFsBlockDev: backingFsBlockDev, 166 quotas: make(map[string]uint32), 167 } 168 169 // 170 // update minimum project ID 171 // 172 state := getPquotaState() 173 state.updateMinProjID(minProjectID) 174 175 // 176 // get first project id to be used for next container 177 // 178 err = q.findNextProjectID(basePath, baseProjectID) 179 if err != nil { 180 return nil, err 181 } 182 183 logrus.Debugf("NewControl(%s): nextProjectID = %d", basePath, state.nextProjectID) 184 return &q, nil 185 } 186 187 // SetQuota - assign a unique project id to directory and set the quota limits 188 // for that project id 189 func (q *Control) SetQuota(targetPath string, quota Quota) error { 190 q.RLock() 191 projectID, ok := q.quotas[targetPath] 192 q.RUnlock() 193 if !ok { 194 state := getPquotaState() 195 state.Lock() 196 projectID = state.nextProjectID 197 198 // 199 // assign project id to new container directory 200 // 201 err := setProjectID(targetPath, projectID) 202 if err != nil { 203 state.Unlock() 204 return err 205 } 206 207 state.nextProjectID++ 208 state.Unlock() 209 210 q.Lock() 211 q.quotas[targetPath] = projectID 212 q.Unlock() 213 } 214 215 // 216 // set the quota limit for the container's project id 217 // 218 logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID) 219 return setProjectQuota(q.backingFsBlockDev, projectID, quota) 220 } 221 222 // setProjectQuota - set the quota for project id on xfs block device 223 func setProjectQuota(backingFsBlockDev string, projectID uint32, quota Quota) error { 224 var d C.fs_disk_quota_t 225 d.d_version = C.FS_DQUOT_VERSION 226 d.d_id = C.__u32(projectID) 227 d.d_flags = C.XFS_PROJ_QUOTA 228 229 d.d_fieldmask = C.FS_DQ_BHARD | C.FS_DQ_BSOFT 230 d.d_blk_hardlimit = C.__u64(quota.Size / 512) 231 d.d_blk_softlimit = d.d_blk_hardlimit 232 233 var cs = C.CString(backingFsBlockDev) 234 defer C.free(unsafe.Pointer(cs)) 235 236 _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XSETPQLIM, 237 uintptr(unsafe.Pointer(cs)), uintptr(d.d_id), 238 uintptr(unsafe.Pointer(&d)), 0, 0) 239 if errno != 0 { 240 return errors.Wrapf(errno, "failed to set quota limit for projid %d on %s", 241 projectID, backingFsBlockDev) 242 } 243 244 return nil 245 } 246 247 // GetQuota - get the quota limits of a directory that was configured with SetQuota 248 func (q *Control) GetQuota(targetPath string, quota *Quota) error { 249 q.RLock() 250 projectID, ok := q.quotas[targetPath] 251 q.RUnlock() 252 if !ok { 253 return errors.Errorf("quota not found for path: %s", targetPath) 254 } 255 256 // 257 // get the quota limit for the container's project id 258 // 259 var d C.fs_disk_quota_t 260 261 var cs = C.CString(q.backingFsBlockDev) 262 defer C.free(unsafe.Pointer(cs)) 263 264 _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, C.Q_XGETPQUOTA, 265 uintptr(unsafe.Pointer(cs)), uintptr(C.__u32(projectID)), 266 uintptr(unsafe.Pointer(&d)), 0, 0) 267 if errno != 0 { 268 return errors.Wrapf(errno, "Failed to get quota limit for projid %d on %s", 269 projectID, q.backingFsBlockDev) 270 } 271 quota.Size = uint64(d.d_blk_hardlimit) * 512 272 273 return nil 274 } 275 276 // getProjectID - get the project id of path on xfs 277 func getProjectID(targetPath string) (uint32, error) { 278 dir, err := openDir(targetPath) 279 if err != nil { 280 return 0, err 281 } 282 defer closeDir(dir) 283 284 var fsx C.struct_fsxattr 285 _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR, 286 uintptr(unsafe.Pointer(&fsx))) 287 if errno != 0 { 288 return 0, errors.Wrapf(errno, "failed to get projid for %s", targetPath) 289 } 290 291 return uint32(fsx.fsx_projid), nil 292 } 293 294 // setProjectID - set the project id of path on xfs 295 func setProjectID(targetPath string, projectID uint32) error { 296 dir, err := openDir(targetPath) 297 if err != nil { 298 return err 299 } 300 defer closeDir(dir) 301 302 var fsx C.struct_fsxattr 303 _, _, errno := unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSGETXATTR, 304 uintptr(unsafe.Pointer(&fsx))) 305 if errno != 0 { 306 return errors.Wrapf(errno, "failed to get projid for %s", targetPath) 307 } 308 fsx.fsx_projid = C.__u32(projectID) 309 fsx.fsx_xflags |= C.FS_XFLAG_PROJINHERIT 310 _, _, errno = unix.Syscall(unix.SYS_IOCTL, getDirFd(dir), C.FS_IOC_FSSETXATTR, 311 uintptr(unsafe.Pointer(&fsx))) 312 if errno != 0 { 313 return errors.Wrapf(errno, "failed to set projid for %s", targetPath) 314 } 315 316 return nil 317 } 318 319 // findNextProjectID - find the next project id to be used for containers 320 // by scanning driver home directory to find used project ids 321 func (q *Control) findNextProjectID(home string, baseID uint32) error { 322 state := getPquotaState() 323 state.Lock() 324 defer state.Unlock() 325 326 checkProjID := func(path string) (uint32, error) { 327 projid, err := getProjectID(path) 328 if err != nil { 329 return projid, err 330 } 331 if projid > 0 { 332 q.quotas[path] = projid 333 } 334 if state.nextProjectID <= projid { 335 state.nextProjectID = projid + 1 336 } 337 return projid, nil 338 } 339 340 files, err := os.ReadDir(home) 341 if err != nil { 342 return errors.Errorf("read directory failed: %s", home) 343 } 344 for _, file := range files { 345 if !file.IsDir() { 346 continue 347 } 348 path := filepath.Join(home, file.Name()) 349 projid, err := checkProjID(path) 350 if err != nil { 351 return err 352 } 353 if projid > 0 && projid != baseID { 354 continue 355 } 356 subfiles, err := os.ReadDir(path) 357 if err != nil { 358 return errors.Errorf("read directory failed: %s", path) 359 } 360 for _, subfile := range subfiles { 361 if !subfile.IsDir() { 362 continue 363 } 364 subpath := filepath.Join(path, subfile.Name()) 365 _, err := checkProjID(subpath) 366 if err != nil { 367 return err 368 } 369 } 370 } 371 372 return nil 373 } 374 375 func free(p *C.char) { 376 C.free(unsafe.Pointer(p)) 377 } 378 379 func openDir(path string) (*C.DIR, error) { 380 Cpath := C.CString(path) 381 defer free(Cpath) 382 383 dir := C.opendir(Cpath) 384 if dir == nil { 385 return nil, errors.Errorf("failed to open dir: %s", path) 386 } 387 return dir, nil 388 } 389 390 func closeDir(dir *C.DIR) { 391 if dir != nil { 392 C.closedir(dir) 393 } 394 } 395 396 func getDirFd(dir *C.DIR) uintptr { 397 return uintptr(C.dirfd(dir)) 398 } 399 400 // makeBackingFsDev gets the backing block device of the driver home directory 401 // and creates a block device node under the home directory to be used by 402 // quotactl commands. 403 func makeBackingFsDev(home string) (string, error) { 404 var stat unix.Stat_t 405 if err := unix.Stat(home, &stat); err != nil { 406 return "", err 407 } 408 409 backingFsBlockDev := path.Join(home, "backingFsBlockDev") 410 // Re-create just in case someone copied the home directory over to a new device 411 unix.Unlink(backingFsBlockDev) 412 err := unix.Mknod(backingFsBlockDev, unix.S_IFBLK|0600, int(stat.Dev)) 413 switch err { 414 case nil: 415 return backingFsBlockDev, nil 416 417 case unix.ENOSYS, unix.EPERM: 418 return "", ErrQuotaNotSupported 419 420 default: 421 return "", errors.Wrapf(err, "failed to mknod %s", backingFsBlockDev) 422 } 423 } 424 425 func hasQuotaSupport(backingFsBlockDev string) (bool, error) { 426 var cs = C.CString(backingFsBlockDev) 427 defer free(cs) 428 var qstat C.fs_quota_stat_t 429 430 _, _, errno := unix.Syscall6(unix.SYS_QUOTACTL, uintptr(C.Q_XGETQSTAT_PRJQUOTA), uintptr(unsafe.Pointer(cs)), 0, uintptr(unsafe.Pointer(&qstat)), 0, 0) 431 if errno == 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ENFD > 0 && qstat.qs_flags&C.FS_QUOTA_PDQ_ACCT > 0 { 432 return true, nil 433 } 434 435 switch errno { 436 // These are the known fatal errors, consider all other errors (ENOTTY, etc.. not supporting quota) 437 case unix.EFAULT, unix.ENOENT, unix.ENOTBLK, unix.EPERM: 438 default: 439 return false, nil 440 } 441 442 return false, errno 443 }