2022-09-22 12:54:47

by Zhihao Cheng

[permalink] [raw]
Subject: [PATCH v2 0/3] Check content after reading from quota file

1. Fix invalid memory access of dquot.
2. Cleanup, replace places of block number checking with helper
function.
3. Add more sanity checking for the content read from quota file.

v1->v2:
Add CC-stable tag in first patch.
Rename check_free_block() -> check_dquot_block_header().

Zhihao Cheng (3):
quota: Check next/prev free block number after reading from quota file
quota: Replace all block number checking with helper function
quota: Add more checking after reading from quota file

fs/quota/quota_tree.c | 81 ++++++++++++++++++++++++++++++++++++-------
1 file changed, 69 insertions(+), 12 deletions(-)

--
2.31.1


2022-09-22 12:55:38

by Zhihao Cheng

[permalink] [raw]
Subject: [PATCH v2 3/3] quota: Add more checking after reading from quota file

It would be better to do more sanity checking (eg. dqdh_entries,
block no.) for the content read from quota file, which can prevent
corrupting the quota file.

Signed-off-by: Zhihao Cheng <[email protected]>
---
fs/quota/quota_tree.c | 43 +++++++++++++++++++++++++++++++++----------
1 file changed, 33 insertions(+), 10 deletions(-)

diff --git a/fs/quota/quota_tree.c b/fs/quota/quota_tree.c
index 47711e739ddb..54fe4ad71de5 100644
--- a/fs/quota/quota_tree.c
+++ b/fs/quota/quota_tree.c
@@ -71,12 +71,12 @@ static ssize_t write_blk(struct qtree_mem_dqinfo *info, uint blk, char *buf)
return ret;
}

-static inline int do_check_range(struct super_block *sb, uint val,
- uint min_val, uint max_val)
+static inline int do_check_range(struct super_block *sb, const char *val_name,
+ uint val, uint min_val, uint max_val)
{
if (val < min_val || val >= max_val) {
- quota_error(sb, "Getting block %u out of range %u-%u",
- val, min_val, max_val);
+ quota_error(sb, "Getting %s %u out of range %u-%u",
+ val_name, val, min_val, max_val);
return -EUCLEAN;
}

@@ -90,11 +90,13 @@ static int check_dquot_block_header(struct qtree_mem_dqinfo *info,
uint nextblk, prevblk;

nextblk = le32_to_cpu(dh->dqdh_next_free);
- err = do_check_range(info->dqi_sb, nextblk, 0, info->dqi_blocks);
+ err = do_check_range(info->dqi_sb, "dqdh_next_free", nextblk, 0,
+ info->dqi_blocks);
if (err)
return err;
prevblk = le32_to_cpu(dh->dqdh_prev_free);
- err = do_check_range(info->dqi_sb, prevblk, 0, info->dqi_blocks);
+ err = do_check_range(info->dqi_sb, "dqdh_prev_free", prevblk, 0,
+ info->dqi_blocks);
if (err)
return err;

@@ -268,6 +270,11 @@ static uint find_free_dqentry(struct qtree_mem_dqinfo *info,
*err = check_dquot_block_header(info, dh);
if (*err)
goto out_buf;
+ *err = do_check_range(info->dqi_sb, "dqdh_entries",
+ le16_to_cpu(dh->dqdh_entries), 0,
+ qtree_dqstr_in_blk(info));
+ if (*err)
+ goto out_buf;
} else {
blk = get_free_dqblk(info);
if ((int)blk < 0) {
@@ -349,6 +356,10 @@ static int do_insert_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
}
ref = (__le32 *)buf;
newblk = le32_to_cpu(ref[get_index(info, dquot->dq_id, depth)]);
+ ret = do_check_range(dquot->dq_sb, "block", newblk, 0,
+ info->dqi_blocks);
+ if (ret)
+ goto out_buf;
if (!newblk)
newson = 1;
if (depth == info->dqi_qtree_depth - 1) {
@@ -461,6 +472,11 @@ static int free_dqentry(struct qtree_mem_dqinfo *info, struct dquot *dquot,
}
dh = (struct qt_disk_dqdbheader *)buf;
ret = check_dquot_block_header(info, dh);
+ if (ret)
+ goto out_buf;
+ ret = do_check_range(info->dqi_sb, "dqdh_entries",
+ le16_to_cpu(dh->dqdh_entries), 1,
+ qtree_dqstr_in_blk(info) + 1);
if (ret)
goto out_buf;
le16_add_cpu(&dh->dqdh_entries, -1);
@@ -519,7 +535,7 @@ static int remove_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
goto out_buf;
}
newblk = le32_to_cpu(ref[get_index(info, dquot->dq_id, depth)]);
- ret = do_check_range(dquot->dq_sb, newblk, QT_TREEOFF,
+ ret = do_check_range(dquot->dq_sb, "block", newblk, QT_TREEOFF,
info->dqi_blocks);
if (ret)
goto out_buf;
@@ -623,7 +639,8 @@ static loff_t find_tree_dqentry(struct qtree_mem_dqinfo *info,
blk = le32_to_cpu(ref[get_index(info, dquot->dq_id, depth)]);
if (!blk) /* No reference? */
goto out_buf;
- ret = do_check_range(dquot->dq_sb, blk, QT_TREEOFF, info->dqi_blocks);
+ ret = do_check_range(dquot->dq_sb, "block", blk, QT_TREEOFF,
+ info->dqi_blocks);
if (ret)
goto out_buf;

@@ -739,7 +756,13 @@ static int find_next_id(struct qtree_mem_dqinfo *info, qid_t *id,
goto out_buf;
}
for (i = __get_index(info, *id, depth); i < epb; i++) {
- if (ref[i] == cpu_to_le32(0)) {
+ uint blk_no = le32_to_cpu(ref[i]);
+
+ ret = do_check_range(info->dqi_sb, "block", blk_no, 0,
+ info->dqi_blocks);
+ if (ret)
+ goto out_buf;
+ if (blk_no == 0) {
*id += level_inc;
continue;
}
@@ -747,7 +770,7 @@ static int find_next_id(struct qtree_mem_dqinfo *info, qid_t *id,
ret = 0;
goto out_buf;
}
- ret = find_next_id(info, id, le32_to_cpu(ref[i]), depth + 1);
+ ret = find_next_id(info, id, blk_no, depth + 1);
if (ret != -ENOENT)
break;
}
--
2.31.1

2022-09-22 12:55:41

by Zhihao Cheng

[permalink] [raw]
Subject: [PATCH v2 2/3] quota: Replace all block number checking with helper function

Cleanup all block checking places, replace them with helper function
do_check_range().

Signed-off-by: Zhihao Cheng <[email protected]>
---
fs/quota/quota_tree.c | 28 ++++++++++++----------------
1 file changed, 12 insertions(+), 16 deletions(-)

diff --git a/fs/quota/quota_tree.c b/fs/quota/quota_tree.c
index f89186b6db1d..47711e739ddb 100644
--- a/fs/quota/quota_tree.c
+++ b/fs/quota/quota_tree.c
@@ -71,11 +71,12 @@ static ssize_t write_blk(struct qtree_mem_dqinfo *info, uint blk, char *buf)
return ret;
}

-static inline int do_check_range(struct super_block *sb, uint val, uint max_val)
+static inline int do_check_range(struct super_block *sb, uint val,
+ uint min_val, uint max_val)
{
- if (val >= max_val) {
- quota_error(sb, "Getting block too big (%u >= %u)",
- val, max_val);
+ if (val < min_val || val >= max_val) {
+ quota_error(sb, "Getting block %u out of range %u-%u",
+ val, min_val, max_val);
return -EUCLEAN;
}

@@ -89,11 +90,11 @@ static int check_dquot_block_header(struct qtree_mem_dqinfo *info,
uint nextblk, prevblk;

nextblk = le32_to_cpu(dh->dqdh_next_free);
- err = do_check_range(info->dqi_sb, nextblk, info->dqi_blocks);
+ err = do_check_range(info->dqi_sb, nextblk, 0, info->dqi_blocks);
if (err)
return err;
prevblk = le32_to_cpu(dh->dqdh_prev_free);
- err = do_check_range(info->dqi_sb, prevblk, info->dqi_blocks);
+ err = do_check_range(info->dqi_sb, prevblk, 0, info->dqi_blocks);
if (err)
return err;

@@ -518,12 +519,10 @@ static int remove_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
goto out_buf;
}
newblk = le32_to_cpu(ref[get_index(info, dquot->dq_id, depth)]);
- if (newblk < QT_TREEOFF || newblk >= info->dqi_blocks) {
- quota_error(dquot->dq_sb, "Getting block too big (%u >= %u)",
- newblk, info->dqi_blocks);
- ret = -EUCLEAN;
+ ret = do_check_range(dquot->dq_sb, newblk, QT_TREEOFF,
+ info->dqi_blocks);
+ if (ret)
goto out_buf;
- }

if (depth == info->dqi_qtree_depth - 1) {
ret = free_dqentry(info, dquot, newblk);
@@ -624,12 +623,9 @@ static loff_t find_tree_dqentry(struct qtree_mem_dqinfo *info,
blk = le32_to_cpu(ref[get_index(info, dquot->dq_id, depth)]);
if (!blk) /* No reference? */
goto out_buf;
- if (blk < QT_TREEOFF || blk >= info->dqi_blocks) {
- quota_error(dquot->dq_sb, "Getting block too big (%u >= %u)",
- blk, info->dqi_blocks);
- ret = -EUCLEAN;
+ ret = do_check_range(dquot->dq_sb, blk, QT_TREEOFF, info->dqi_blocks);
+ if (ret)
goto out_buf;
- }

if (depth < info->dqi_qtree_depth - 1)
ret = find_tree_dqentry(info, dquot, blk, depth+1);
--
2.31.1

2022-09-23 11:49:37

by Jan Kara

[permalink] [raw]
Subject: Re: [PATCH v2 3/3] quota: Add more checking after reading from quota file

On Thu 22-09-22 21:04:01, Zhihao Cheng wrote:
> It would be better to do more sanity checking (eg. dqdh_entries,
> block no.) for the content read from quota file, which can prevent
> corrupting the quota file.
>
> Signed-off-by: Zhihao Cheng <[email protected]>
> ---
> fs/quota/quota_tree.c | 43 +++++++++++++++++++++++++++++++++----------
> 1 file changed, 33 insertions(+), 10 deletions(-)
>
> diff --git a/fs/quota/quota_tree.c b/fs/quota/quota_tree.c
> index 47711e739ddb..54fe4ad71de5 100644
> --- a/fs/quota/quota_tree.c
> +++ b/fs/quota/quota_tree.c
> @@ -71,12 +71,12 @@ static ssize_t write_blk(struct qtree_mem_dqinfo *info, uint blk, char *buf)
> return ret;
> }
>
> -static inline int do_check_range(struct super_block *sb, uint val,
> - uint min_val, uint max_val)
> +static inline int do_check_range(struct super_block *sb, const char *val_name,
> + uint val, uint min_val, uint max_val)
> {
> if (val < min_val || val >= max_val) {
> - quota_error(sb, "Getting block %u out of range %u-%u",
> - val, min_val, max_val);
> + quota_error(sb, "Getting %s %u out of range %u-%u",
> + val_name, val, min_val, max_val);
> return -EUCLEAN;
> }

As I already wrote in my comments to v1, please create do_check_range()
already with this prototype in patch 1 so that you don't have to update it
(and all the call sites) in each of the patches. It makes review simpler.

> @@ -268,6 +270,11 @@ static uint find_free_dqentry(struct qtree_mem_dqinfo *info,
> *err = check_dquot_block_header(info, dh);
> if (*err)
> goto out_buf;
> + *err = do_check_range(info->dqi_sb, "dqdh_entries",
> + le16_to_cpu(dh->dqdh_entries), 0,
> + qtree_dqstr_in_blk(info));
> + if (*err)
> + goto out_buf;

The checking of dqdh_entries belongs into check_dquot_block_header(). That
was the reason why it was created. So that all the checks are together in
one function...

> } else {
> blk = get_free_dqblk(info);
> if ((int)blk < 0) {
> @@ -349,6 +356,10 @@ static int do_insert_tree(struct qtree_mem_dqinfo *info, struct dquot *dquot,
> }
> ref = (__le32 *)buf;
> newblk = le32_to_cpu(ref[get_index(info, dquot->dq_id, depth)]);
> + ret = do_check_range(dquot->dq_sb, "block", newblk, 0,
> + info->dqi_blocks);
> + if (ret)
> + goto out_buf;
> if (!newblk)
> newson = 1;
> if (depth == info->dqi_qtree_depth - 1) {
> @@ -461,6 +472,11 @@ static int free_dqentry(struct qtree_mem_dqinfo *info, struct dquot *dquot,
> }
> dh = (struct qt_disk_dqdbheader *)buf;
> ret = check_dquot_block_header(info, dh);
> + if (ret)
> + goto out_buf;
> + ret = do_check_range(info->dqi_sb, "dqdh_entries",
> + le16_to_cpu(dh->dqdh_entries), 1,
> + qtree_dqstr_in_blk(info) + 1);

Again, the check of dqdh_entries should be in check_dquot_block_header().

> @@ -739,7 +756,13 @@ static int find_next_id(struct qtree_mem_dqinfo *info, qid_t *id,
> goto out_buf;
> }
> for (i = __get_index(info, *id, depth); i < epb; i++) {
> - if (ref[i] == cpu_to_le32(0)) {
> + uint blk_no = le32_to_cpu(ref[i]);
> +
> + ret = do_check_range(info->dqi_sb, "block", blk_no, 0,
> + info->dqi_blocks);
> + if (ret)
> + goto out_buf;
> + if (blk_no == 0) {
> *id += level_inc;
> continue;
> }

I'd leave checking for 0 first here - i.e.:
if (ref[i] == cpu_to_le32(0)) {
*id += level_inc;
continue;
}

and only then do:
blk_no = le32_to_cpu(ref[i]);
ret = do_check_range(...);

There's no point in checking known-good value.

Honza

--
Jan Kara <[email protected]>
SUSE Labs, CR

2022-09-23 12:09:50

by Jan Kara

[permalink] [raw]
Subject: Re: [PATCH v2 2/3] quota: Replace all block number checking with helper function

On Thu 22-09-22 21:04:00, Zhihao Cheng wrote:
> Cleanup all block checking places, replace them with helper function
> do_check_range().
>
> Signed-off-by: Zhihao Cheng <[email protected]>
> ---
> fs/quota/quota_tree.c | 28 ++++++++++++----------------
> 1 file changed, 12 insertions(+), 16 deletions(-)

Thanks for the fix! One comment below:

> diff --git a/fs/quota/quota_tree.c b/fs/quota/quota_tree.c
> index f89186b6db1d..47711e739ddb 100644
> --- a/fs/quota/quota_tree.c
> +++ b/fs/quota/quota_tree.c
> @@ -71,11 +71,12 @@ static ssize_t write_blk(struct qtree_mem_dqinfo *info, uint blk, char *buf)
> return ret;
> }
>
> -static inline int do_check_range(struct super_block *sb, uint val, uint max_val)
> +static inline int do_check_range(struct super_block *sb, uint val,
> + uint min_val, uint max_val)
> {
> - if (val >= max_val) {
> - quota_error(sb, "Getting block too big (%u >= %u)",
> - val, max_val);
> + if (val < min_val || val >= max_val) {
> + quota_error(sb, "Getting block %u out of range %u-%u",
> + val, min_val, max_val);
> return -EUCLEAN;
> }

It is strange that do_check_range() checks min_val() with strict inequality
and max_val with non-strict one. That's off-by-one problem waiting to
happen when we forget about this detail. Probably make max_val
non-inclusive as well (the parameter max_val suggests the passed value is
the biggest valid one anyway).

Honza

--
Jan Kara <[email protected]>
SUSE Labs, CR

2022-09-27 01:16:34

by Zhihao Cheng

[permalink] [raw]
Subject: Re: [PATCH v2 2/3] quota: Replace all block number checking with helper function

?? 2022/9/23 19:48, Jan Kara ะด??:
> On Thu 22-09-22 21:04:00, Zhihao Cheng wrote:
>> Cleanup all block checking places, replace them with helper function
>> do_check_range().
>>
>> Signed-off-by: Zhihao Cheng <[email protected]>
>> ---
>> fs/quota/quota_tree.c | 28 ++++++++++++----------------
>> 1 file changed, 12 insertions(+), 16 deletions(-)
>
> Thanks for the fix! One comment below:
>
>> diff --git a/fs/quota/quota_tree.c b/fs/quota/quota_tree.c
>> index f89186b6db1d..47711e739ddb 100644
>> --- a/fs/quota/quota_tree.c
>> +++ b/fs/quota/quota_tree.c
>> @@ -71,11 +71,12 @@ static ssize_t write_blk(struct qtree_mem_dqinfo *info, uint blk, char *buf)
>> return ret;
>> }
>>
>> -static inline int do_check_range(struct super_block *sb, uint val, uint max_val)
>> +static inline int do_check_range(struct super_block *sb, uint val,
>> + uint min_val, uint max_val)
>> {
>> - if (val >= max_val) {
>> - quota_error(sb, "Getting block too big (%u >= %u)",
>> - val, max_val);
>> + if (val < min_val || val >= max_val) {
>> + quota_error(sb, "Getting block %u out of range %u-%u",
>> + val, min_val, max_val);
>> return -EUCLEAN;
>> }
>
> It is strange that do_check_range() checks min_val() with strict inequality
> and max_val with non-strict one. That's off-by-one problem waiting to
> happen when we forget about this detail. Probably make max_val
> non-inclusive as well (the parameter max_val suggests the passed value is
> the biggest valid one anyway).
>
> Honza
>

I have sent v3 series, see
https://lore.kernel.org/all/[email protected]/T/