shmem: Add default quota limit mount options
authorLukas Czerner <lczerner@redhat.com>
Tue, 25 Jul 2023 14:45:09 +0000 (16:45 +0200)
committerChristian Brauner <brauner@kernel.org>
Wed, 9 Aug 2023 07:15:40 +0000 (09:15 +0200)
Allow system administrator to set default global quota limits at tmpfs
mount time.

Signed-off-by: Lukas Czerner <lczerner@redhat.com>
Signed-off-by: Carlos Maiolino <cmaiolino@redhat.com>
Reviewed-by: Jan Kara <jack@suse.cz>
Message-Id: <20230725144510.253763-7-cem@kernel.org>
Signed-off-by: Christian Brauner <brauner@kernel.org>
Documentation/filesystems/tmpfs.rst
include/linux/shmem_fs.h
mm/shmem.c
mm/shmem_quota.c

index 06d6195eaa2c8bb4d33ebcafa78b71080813ab4d..67422ee10e03dea1a7b8d69fba5e6d11e4f17a5d 100644 (file)
@@ -125,15 +125,31 @@ force huge pages on all tmpfs mounts for testing.
 
 tmpfs also supports quota with the following mount options
 
-========  =============================================================
-quota     User and group quota accounting and enforcement is enabled on
-          the mount. Tmpfs is using hidden system quota files that are
-          initialized on mount.
-usrquota  User quota accounting and enforcement is enabled on the
-          mount.
-grpquota  Group quota accounting and enforcement is enabled on the
-          mount.
-========  =============================================================
+======================== =================================================
+quota                    User and group quota accounting and enforcement
+                         is enabled on the mount. Tmpfs is using hidden
+                         system quota files that are initialized on mount.
+usrquota                 User quota accounting and enforcement is enabled
+                         on the mount.
+grpquota                 Group quota accounting and enforcement is enabled
+                         on the mount.
+usrquota_block_hardlimit Set global user quota block hard limit.
+usrquota_inode_hardlimit Set global user quota inode hard limit.
+grpquota_block_hardlimit Set global group quota block hard limit.
+grpquota_inode_hardlimit Set global group quota inode hard limit.
+======================== =================================================
+
+None of the quota related mount options can be set or changed on remount.
+
+Quota limit parameters accept a suffix k, m or g for kilo, mega and giga
+and can't be changed on remount. Default global quota limits are taking
+effect for any and all user/group/project except root the first time the
+quota entry for user/group/project id is being accessed - typically the
+first time an inode with a particular id ownership is being created after
+the mount. In other words, instead of the limits being initialized to zero,
+they are initialized with the particular value provided with these mount
+options. The limits can be changed for any user/group id at any time as they
+normally can be.
 
 Note that tmpfs quotas do not support user namespaces so no uid/gid
 translation is done if quotas are enabled inside user namespaces.
index 1a568a0f542f62bc6a2b5a85570e30e9464b4c27..c0058f3bba70a7c7ceaac7397561beda3780061f 100644 (file)
@@ -42,6 +42,13 @@ struct shmem_inode_info {
        (FS_IMMUTABLE_FL | FS_APPEND_FL | FS_NODUMP_FL | FS_NOATIME_FL)
 #define SHMEM_FL_INHERITED             (FS_NODUMP_FL | FS_NOATIME_FL)
 
+struct shmem_quota_limits {
+       qsize_t usrquota_bhardlimit; /* Default user quota block hard limit */
+       qsize_t usrquota_ihardlimit; /* Default user quota inode hard limit */
+       qsize_t grpquota_bhardlimit; /* Default group quota block hard limit */
+       qsize_t grpquota_ihardlimit; /* Default group quota inode hard limit */
+};
+
 struct shmem_sb_info {
        unsigned long max_blocks;   /* How many blocks are allowed */
        struct percpu_counter used_blocks;  /* How many are allocated */
@@ -60,6 +67,7 @@ struct shmem_sb_info {
        spinlock_t shrinklist_lock;   /* Protects shrinklist */
        struct list_head shrinklist;  /* List of shinkable inodes */
        unsigned long shrinklist_len; /* Length of shrinklist */
+       struct shmem_quota_limits qlimits; /* Default quota limits */
 };
 
 static inline struct shmem_inode_info *SHMEM_I(struct inode *inode)
index ec76ce9aea5fa6cd8c0063ab659e2730c2a53a32..5f2c9e2961c2f8bcfd0af33867bd7036fdd4a3bd 100644 (file)
@@ -118,6 +118,7 @@ struct shmem_options {
        int seen;
        bool noswap;
        unsigned short quota_types;
+       struct shmem_quota_limits qlimits;
 #define SHMEM_SEEN_BLOCKS 1
 #define SHMEM_SEEN_INODES 2
 #define SHMEM_SEEN_HUGE 4
@@ -3738,6 +3739,10 @@ enum shmem_param {
        Opt_quota,
        Opt_usrquota,
        Opt_grpquota,
+       Opt_usrquota_block_hardlimit,
+       Opt_usrquota_inode_hardlimit,
+       Opt_grpquota_block_hardlimit,
+       Opt_grpquota_inode_hardlimit,
 };
 
 static const struct constant_table shmem_param_enums_huge[] = {
@@ -3764,6 +3769,10 @@ const struct fs_parameter_spec shmem_fs_parameters[] = {
        fsparam_flag  ("quota",         Opt_quota),
        fsparam_flag  ("usrquota",      Opt_usrquota),
        fsparam_flag  ("grpquota",      Opt_grpquota),
+       fsparam_string("usrquota_block_hardlimit", Opt_usrquota_block_hardlimit),
+       fsparam_string("usrquota_inode_hardlimit", Opt_usrquota_inode_hardlimit),
+       fsparam_string("grpquota_block_hardlimit", Opt_grpquota_block_hardlimit),
+       fsparam_string("grpquota_inode_hardlimit", Opt_grpquota_inode_hardlimit),
 #endif
        {}
 };
@@ -3874,6 +3883,42 @@ static int shmem_parse_one(struct fs_context *fc, struct fs_parameter *param)
                ctx->seen |= SHMEM_SEEN_QUOTA;
                ctx->quota_types |= QTYPE_MASK_GRP;
                break;
+       case Opt_usrquota_block_hardlimit:
+               size = memparse(param->string, &rest);
+               if (*rest || !size)
+                       goto bad_value;
+               if (size > SHMEM_QUOTA_MAX_SPC_LIMIT)
+                       return invalfc(fc,
+                                      "User quota block hardlimit too large.");
+               ctx->qlimits.usrquota_bhardlimit = size;
+               break;
+       case Opt_grpquota_block_hardlimit:
+               size = memparse(param->string, &rest);
+               if (*rest || !size)
+                       goto bad_value;
+               if (size > SHMEM_QUOTA_MAX_SPC_LIMIT)
+                       return invalfc(fc,
+                                      "Group quota block hardlimit too large.");
+               ctx->qlimits.grpquota_bhardlimit = size;
+               break;
+       case Opt_usrquota_inode_hardlimit:
+               size = memparse(param->string, &rest);
+               if (*rest || !size)
+                       goto bad_value;
+               if (size > SHMEM_QUOTA_MAX_INO_LIMIT)
+                       return invalfc(fc,
+                                      "User quota inode hardlimit too large.");
+               ctx->qlimits.usrquota_ihardlimit = size;
+               break;
+       case Opt_grpquota_inode_hardlimit:
+               size = memparse(param->string, &rest);
+               if (*rest || !size)
+                       goto bad_value;
+               if (size > SHMEM_QUOTA_MAX_INO_LIMIT)
+                       return invalfc(fc,
+                                      "Group quota inode hardlimit too large.");
+               ctx->qlimits.grpquota_ihardlimit = size;
+               break;
        }
        return 0;
 
@@ -3987,6 +4032,18 @@ static int shmem_reconfigure(struct fs_context *fc)
                goto out;
        }
 
+#ifdef CONFIG_TMPFS_QUOTA
+#define CHANGED_LIMIT(name)                                            \
+       (ctx->qlimits.name## hardlimit &&                               \
+       (ctx->qlimits.name## hardlimit != sbinfo->qlimits.name## hardlimit))
+
+       if (CHANGED_LIMIT(usrquota_b) || CHANGED_LIMIT(usrquota_i) ||
+           CHANGED_LIMIT(grpquota_b) || CHANGED_LIMIT(grpquota_i)) {
+               err = "Cannot change global quota limit on remount";
+               goto out;
+       }
+#endif /* CONFIG_TMPFS_QUOTA */
+
        if (ctx->seen & SHMEM_SEEN_HUGE)
                sbinfo->huge = ctx->huge;
        if (ctx->seen & SHMEM_SEEN_INUMS)
@@ -4166,6 +4223,10 @@ static int shmem_fill_super(struct super_block *sb, struct fs_context *fc)
                sb->s_qcop = &dquot_quotactl_sysfile_ops;
                sb->s_quota_types = QTYPE_MASK_USR | QTYPE_MASK_GRP;
 
+               /* Copy the default limits from ctx into sbinfo */
+               memcpy(&sbinfo->qlimits, &ctx->qlimits,
+                      sizeof(struct shmem_quota_limits));
+
                if (shmem_enable_quotas(sb, ctx->quota_types))
                        goto failed;
        }
index e92b8ece98803d4588cbd4fa0705dd057b984fa7..062d1c1097ae35fdb9fc87faa722bb403c1804fd 100644 (file)
@@ -166,6 +166,7 @@ static int shmem_acquire_dquot(struct dquot *dquot)
 {
        struct mem_dqinfo *info = sb_dqinfo(dquot->dq_sb, dquot->dq_id.type);
        struct rb_node **n = &((struct rb_root *)info->dqi_priv)->rb_node;
+       struct shmem_sb_info *sbinfo = dquot->dq_sb->s_fs_info;
        struct rb_node *parent = NULL, *new_node = NULL;
        struct quota_id *new_entry, *entry;
        qid_t id = from_kqid(&init_user_ns, dquot->dq_id);
@@ -195,6 +196,14 @@ static int shmem_acquire_dquot(struct dquot *dquot)
        }
 
        new_entry->id = id;
+       if (dquot->dq_id.type == USRQUOTA) {
+               new_entry->bhardlimit = sbinfo->qlimits.usrquota_bhardlimit;
+               new_entry->ihardlimit = sbinfo->qlimits.usrquota_ihardlimit;
+       } else if (dquot->dq_id.type == GRPQUOTA) {
+               new_entry->bhardlimit = sbinfo->qlimits.grpquota_bhardlimit;
+               new_entry->ihardlimit = sbinfo->qlimits.grpquota_ihardlimit;
+       }
+
        new_node = &new_entry->node;
        rb_link_node(new_node, parent, n);
        rb_insert_color(new_node, (struct rb_root *)info->dqi_priv);
@@ -224,6 +233,29 @@ out_unlock:
        return ret;
 }
 
+static bool shmem_is_empty_dquot(struct dquot *dquot)
+{
+       struct shmem_sb_info *sbinfo = dquot->dq_sb->s_fs_info;
+       qsize_t bhardlimit;
+       qsize_t ihardlimit;
+
+       if (dquot->dq_id.type == USRQUOTA) {
+               bhardlimit = sbinfo->qlimits.usrquota_bhardlimit;
+               ihardlimit = sbinfo->qlimits.usrquota_ihardlimit;
+       } else if (dquot->dq_id.type == GRPQUOTA) {
+               bhardlimit = sbinfo->qlimits.grpquota_bhardlimit;
+               ihardlimit = sbinfo->qlimits.grpquota_ihardlimit;
+       }
+
+       if (test_bit(DQ_FAKE_B, &dquot->dq_flags) ||
+               (dquot->dq_dqb.dqb_curspace == 0 &&
+                dquot->dq_dqb.dqb_curinodes == 0 &&
+                dquot->dq_dqb.dqb_bhardlimit == bhardlimit &&
+                dquot->dq_dqb.dqb_ihardlimit == ihardlimit))
+               return true;
+
+       return false;
+}
 /*
  * Store limits from dquot in the tree unless it's fake. If it is fake
  * remove the id from the tree since there is no useful information in
@@ -261,7 +293,7 @@ static int shmem_release_dquot(struct dquot *dquot)
        return -ENOENT;
 
 found:
-       if (test_bit(DQ_FAKE_B, &dquot->dq_flags)) {
+       if (shmem_is_empty_dquot(dquot)) {
                /* Remove entry from the tree */
                rb_erase(&entry->node, info->dqi_priv);
                kfree(entry);