github.com/rumpl/bof@v23.0.0-rc.2+incompatible/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/pkg/userns"
    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 userns.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  }