quota: Detect loops in quota tree
authorJan Kara <jack@suse.cz>
Wed, 7 Feb 2024 18:12:15 +0000 (19:12 +0100)
committerJan Kara <jack@suse.cz>
Mon, 12 Feb 2024 13:14:49 +0000 (14:14 +0100)
Syzbot has found that when it creates corrupted quota files where the
quota tree contains a loop, we will deadlock when tryling to insert a
dquot. Add loop detection into functions traversing the quota tree.

Signed-off-by: Jan Kara <jack@suse.cz>
fs/quota/quota_tree.c
fs/quota/quota_v2.c

index ef0461542d3a1d62ba2a26853b6b664fbab35a79..afceef3ddfaa4763a7901ce4ab7cdf802caaaf1c 100644 (file)
@@ -21,6 +21,12 @@ MODULE_AUTHOR("Jan Kara");
 MODULE_DESCRIPTION("Quota trie support");
 MODULE_LICENSE("GPL");
 
+/*
+ * Maximum quota tree depth we support. Only to limit recursion when working
+ * with the tree.
+ */
+#define MAX_QTREE_DEPTH 6
+
 #define __QUOTA_QT_PARANOIA
 
 static int __get_index(struct qtree_mem_dqinfo *info, qid_t id, int depth)
@@ -327,27 +333,36 @@ out_buf:
 
 /* Insert reference to structure into the trie */
 static int do_insert_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
-                         uint *treeblk, int depth)
+                         uint *blks, int depth)
 {
        char *buf = kmalloc(info->dqi_usable_bs, GFP_KERNEL);
        int ret = 0, newson = 0, newact = 0;
        __le32 *ref;
        uint newblk;
+       int i;
 
        if (!buf)
                return -ENOMEM;
-       if (!*treeblk) {
+       if (!blks[depth]) {
                ret = get_free_dqblk(info);
                if (ret < 0)
                        goto out_buf;
-               *treeblk = ret;
+               for (i = 0; i < depth; i++)
+                       if (ret == blks[i]) {
+                               quota_error(dquot->dq_sb,
+                                       "Free block already used in tree: block %u",
+                                       ret);
+                               ret = -EIO;
+                               goto out_buf;
+                       }
+               blks[depth] = ret;
                memset(buf, 0, info->dqi_usable_bs);
                newact = 1;
        } else {
-               ret = read_blk(info, *treeblk, buf);
+               ret = read_blk(info, blks[depth], buf);
                if (ret < 0) {
                        quota_error(dquot->dq_sb, "Can't read tree quota "
-                                   "block %u", *treeblk);
+                                   "block %u", blks[depth]);
                        goto out_buf;
                }
        }
@@ -357,8 +372,20 @@ static int do_insert_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
                             info->dqi_blocks - 1);
        if (ret)
                goto out_buf;
-       if (!newblk)
+       if (!newblk) {
                newson = 1;
+       } else {
+               for (i = 0; i <= depth; i++)
+                       if (newblk == blks[i]) {
+                               quota_error(dquot->dq_sb,
+                                       "Cycle in quota tree detected: block %u index %u",
+                                       blks[depth],
+                                       get_index(info, dquot->dq_id, depth));
+                               ret = -EIO;
+                               goto out_buf;
+                       }
+       }
+       blks[depth + 1] = newblk;
        if (depth == info->dqi_qtree_depth - 1) {
 #ifdef __QUOTA_QT_PARANOIA
                if (newblk) {
@@ -370,16 +397,16 @@ static int do_insert_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
                        goto out_buf;
                }
 #endif
-               newblk = find_free_dqentry(info, dquot, &ret);
+               blks[depth + 1] = find_free_dqentry(info, dquot, &ret);
        } else {
-               ret = do_insert_tree(info, dquot, &newblk, depth+1);
+               ret = do_insert_tree(info, dquot, blks, depth + 1);
        }
        if (newson && ret >= 0) {
                ref[get_index(info, dquot->dq_id, depth)] =
-                                                       cpu_to_le32(newblk);
-               ret = write_blk(info, *treeblk, buf);
+                                               cpu_to_le32(blks[depth + 1]);
+               ret = write_blk(info, blks[depth], buf);
        } else if (newact && ret < 0) {
-               put_free_dqblk(info, buf, *treeblk);
+               put_free_dqblk(info, buf, blks[depth]);
        }
 out_buf:
        kfree(buf);
@@ -390,7 +417,7 @@ out_buf:
 static inline int dq_insert_tree(struct qtree_mem_dqinfo *info,
                                 struct dquot *dquot)
 {
-       int tmp = QT_TREEOFF;
+       uint blks[MAX_QTREE_DEPTH] = { QT_TREEOFF };
 
 #ifdef __QUOTA_QT_PARANOIA
        if (info->dqi_blocks <= QT_TREEOFF) {
@@ -398,7 +425,11 @@ static inline int dq_insert_tree(struct qtree_mem_dqinfo *info,
                return -EIO;
        }
 #endif
-       return do_insert_tree(info, dquot, &tmp, 0);
+       if (info->dqi_qtree_depth >= MAX_QTREE_DEPTH) {
+               quota_error(dquot->dq_sb, "Quota tree depth too big!");
+               return -EIO;
+       }
+       return do_insert_tree(info, dquot, blks, 0);
 }
 
 /*
@@ -511,19 +542,20 @@ out_buf:
 
 /* Remove reference to dquot from tree */
 static int remove_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
-                      uint *blk, int depth)
+                      uint *blks, int depth)
 {
        char *buf = kmalloc(info->dqi_usable_bs, GFP_KERNEL);
        int ret = 0;
        uint newblk;
        __le32 *ref = (__le32 *)buf;
+       int i;
 
        if (!buf)
                return -ENOMEM;
-       ret = read_blk(info, *blk, buf);
+       ret = read_blk(info, blks[depth], buf);
        if (ret < 0) {
                quota_error(dquot->dq_sb, "Can't read quota data block %u",
-                           *blk);
+                           blks[depth]);
                goto out_buf;
        }
        newblk = le32_to_cpu(ref[get_index(info, dquot->dq_id, depth)]);
@@ -532,29 +564,38 @@ static int remove_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
        if (ret)
                goto out_buf;
 
+       for (i = 0; i <= depth; i++)
+               if (newblk == blks[i]) {
+                       quota_error(dquot->dq_sb,
+                               "Cycle in quota tree detected: block %u index %u",
+                               blks[depth],
+                               get_index(info, dquot->dq_id, depth));
+                       ret = -EIO;
+                       goto out_buf;
+               }
        if (depth == info->dqi_qtree_depth - 1) {
                ret = free_dqentry(info, dquot, newblk);
-               newblk = 0;
+               blks[depth + 1] = 0;
        } else {
-               ret = remove_tree(info, dquot, &newblk, depth+1);
+               blks[depth + 1] = newblk;
+               ret = remove_tree(info, dquot, blks, depth + 1);
        }
-       if (ret >= 0 && !newblk) {
-               int i;
+       if (ret >= 0 && !blks[depth + 1]) {
                ref[get_index(info, dquot->dq_id, depth)] = cpu_to_le32(0);
                /* Block got empty? */
                for (i = 0; i < (info->dqi_usable_bs >> 2) && !ref[i]; i++)
                        ;
                /* Don't put the root block into the free block list */
                if (i == (info->dqi_usable_bs >> 2)
-                   && *blk != QT_TREEOFF) {
-                       put_free_dqblk(info, buf, *blk);
-                       *blk = 0;
+                   && blks[depth] != QT_TREEOFF) {
+                       put_free_dqblk(info, buf, blks[depth]);
+                       blks[depth] = 0;
                } else {
-                       ret = write_blk(info, *blk, buf);
+                       ret = write_blk(info, blks[depth], buf);
                        if (ret < 0)
                                quota_error(dquot->dq_sb,
                                            "Can't write quota tree block %u",
-                                           *blk);
+                                           blks[depth]);
                }
        }
 out_buf:
@@ -565,11 +606,15 @@ out_buf:
 /* Delete dquot from tree */
 int qtree_delete_dquot(struct qtree_mem_dqinfo *info, struct dquot *dquot)
 {
-       uint tmp = QT_TREEOFF;
+       uint blks[MAX_QTREE_DEPTH] = { QT_TREEOFF };
 
        if (!dquot->dq_off)     /* Even not allocated? */
                return 0;
-       return remove_tree(info, dquot, &tmp, 0);
+       if (info->dqi_qtree_depth >= MAX_QTREE_DEPTH) {
+               quota_error(dquot->dq_sb, "Quota tree depth too big!");
+               return -EIO;
+       }
+       return remove_tree(info, dquot, blks, 0);
 }
 EXPORT_SYMBOL(qtree_delete_dquot);
 
@@ -613,18 +658,20 @@ out_buf:
 
 /* Find entry for given id in the tree */
 static loff_t find_tree_dqentry(struct qtree_mem_dqinfo *info,
-                               struct dquot *dquot, uint blk, int depth)
+                               struct dquot *dquot, uint *blks, int depth)
 {
        char *buf = kmalloc(info->dqi_usable_bs, GFP_KERNEL);
        loff_t ret = 0;
        __le32 *ref = (__le32 *)buf;
+       uint blk;
+       int i;
 
        if (!buf)
                return -ENOMEM;
-       ret = read_blk(info, blk, buf);
+       ret = read_blk(info, blks[depth], buf);
        if (ret < 0) {
                quota_error(dquot->dq_sb, "Can't read quota tree block %u",
-                           blk);
+                           blks[depth]);
                goto out_buf;
        }
        ret = 0;
@@ -636,8 +683,19 @@ static loff_t find_tree_dqentry(struct qtree_mem_dqinfo *info,
        if (ret)
                goto out_buf;
 
+       /* Check for cycles in the tree */
+       for (i = 0; i <= depth; i++)
+               if (blk == blks[i]) {
+                       quota_error(dquot->dq_sb,
+                               "Cycle in quota tree detected: block %u index %u",
+                               blks[depth],
+                               get_index(info, dquot->dq_id, depth));
+                       ret = -EIO;
+                       goto out_buf;
+               }
+       blks[depth + 1] = blk;
        if (depth < info->dqi_qtree_depth - 1)
-               ret = find_tree_dqentry(info, dquot, blk, depth+1);
+               ret = find_tree_dqentry(info, dquot, blks, depth + 1);
        else
                ret = find_block_dqentry(info, dquot, blk);
 out_buf:
@@ -649,7 +707,13 @@ out_buf:
 static inline loff_t find_dqentry(struct qtree_mem_dqinfo *info,
                                  struct dquot *dquot)
 {
-       return find_tree_dqentry(info, dquot, QT_TREEOFF, 0);
+       uint blks[MAX_QTREE_DEPTH] = { QT_TREEOFF };
+
+       if (info->dqi_qtree_depth >= MAX_QTREE_DEPTH) {
+               quota_error(dquot->dq_sb, "Quota tree depth too big!");
+               return -EIO;
+       }
+       return find_tree_dqentry(info, dquot, blks, 0);
 }
 
 int qtree_read_dquot(struct qtree_mem_dqinfo *info, struct dquot *dquot)
index 5eb0de8e7e40f58fc7cb940e26a88b43fedc5f3e..c48c233f3bef04b62f90f63b3e2546f0eb343b63 100644 (file)
@@ -168,14 +168,17 @@ static int v2_read_file_info(struct super_block *sb, int type)
                    i_size_read(sb_dqopt(sb)->files[type]));
                goto out_free;
        }
-       if (qinfo->dqi_free_blk >= qinfo->dqi_blocks) {
-               quota_error(sb, "Free block number too big (%u >= %u).",
-                           qinfo->dqi_free_blk, qinfo->dqi_blocks);
+       if (qinfo->dqi_free_blk && (qinfo->dqi_free_blk <= QT_TREEOFF ||
+           qinfo->dqi_free_blk >= qinfo->dqi_blocks)) {
+               quota_error(sb, "Free block number %u out of range (%u, %u).",
+                           qinfo->dqi_free_blk, QT_TREEOFF, qinfo->dqi_blocks);
                goto out_free;
        }
-       if (qinfo->dqi_free_entry >= qinfo->dqi_blocks) {
-               quota_error(sb, "Block with free entry too big (%u >= %u).",
-                           qinfo->dqi_free_entry, qinfo->dqi_blocks);
+       if (qinfo->dqi_free_entry && (qinfo->dqi_free_entry <= QT_TREEOFF ||
+           qinfo->dqi_free_entry >= qinfo->dqi_blocks)) {
+               quota_error(sb, "Block with free entry %u out of range (%u, %u).",
+                           qinfo->dqi_free_entry, QT_TREEOFF,
+                           qinfo->dqi_blocks);
                goto out_free;
        }
        ret = 0;