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