projectquota.go 11 KB

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