diff mbox series

[V7] squashfs: Add symlink size check in squash_read_inode

Message ID 20240803074349.3599957-1-lizhi.xu@windriver.com (mailing list archive)
State New
Headers show
Series [V7] squashfs: Add symlink size check in squash_read_inode | expand

Commit Message

Lizhi Xu Aug. 3, 2024, 7:43 a.m. UTC
syzbot report KMSAN: uninit-value in pick_link, the root cause is that
squashfs_symlink_read_folio did not check the length, resulting in folio
not being initialized and did not return the corresponding error code.

The length is calculated from i_size, this case is about symlink, so i_size
value is derived from symlink_size, so it is necessary to add a check to
confirm that symlink_size value is valid, otherwise an error -EINVAL will
be returned. 

If symlink_size is too large, it may result in a negative value when
calculating length in squashfs_symlink_read_folio due to int overflow,
and its value must be greater than PAGE_SIZE at this time.

Reported-and-tested-by: syzbot+24ac24ff58dc5b0d26b9@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=24ac24ff58dc5b0d26b9
Signed-off-by: Lizhi Xu <lizhi.xu@windriver.com>
---
 fs/squashfs/inode.c | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

Comments

Phillip Lougher Aug. 4, 2024, 9:16 p.m. UTC | #1
On 03/08/2024 08:43, Lizhi Xu wrote:
> syzbot report KMSAN: uninit-value in pick_link, the root cause is that
> squashfs_symlink_read_folio did not check the length, resulting in folio
> not being initialized and did not return the corresponding error code.
> 
> The length is calculated from i_size, this case is about symlink, so i_size
> value is derived from symlink_size, so it is necessary to add a check to
> confirm that symlink_size value is valid, otherwise an error -EINVAL will
> be returned.
> 
> If symlink_size is too large, it may result in a negative value when
> calculating length in squashfs_symlink_read_folio due to int overflow,
> and its value must be greater than PAGE_SIZE at this time.
> 
> Reported-and-tested-by: syzbot+24ac24ff58dc5b0d26b9@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=24ac24ff58dc5b0d26b9
> Signed-off-by: Lizhi Xu <lizhi.xu@windriver.com>
> ---
>   fs/squashfs/inode.c | 9 ++++++++-
>   1 file changed, 8 insertions(+), 1 deletion(-)
> 
> diff --git a/fs/squashfs/inode.c b/fs/squashfs/inode.c
> index 16bd693d0b3a..bed6764e4461 100644
> --- a/fs/squashfs/inode.c
> +++ b/fs/squashfs/inode.c
> @@ -273,14 +273,21 @@ int squashfs_read_inode(struct inode *inode, long long ino)
>   	case SQUASHFS_SYMLINK_TYPE:
>   	case SQUASHFS_LSYMLINK_TYPE: {
>   		struct squashfs_symlink_inode *sqsh_ino = &squashfs_ino.symlink;
> +		loff_t symlink_size;
>   
>   		err = squashfs_read_metadata(sb, sqsh_ino, &block, &offset,
>   				sizeof(*sqsh_ino));
>   		if (err < 0)
>   			goto failed_read;
>   
> +		symlink_size = le32_to_cpu(sqsh_ino->symlink_size);
> +		if (symlink_size > PAGE_SIZE) {
> +			ERROR("Corrupted symlink, size [%llu]\n", symlink_size);
> +			return -EINVAL;
> +		}
> +
>   		set_nlink(inode, le32_to_cpu(sqsh_ino->nlink));
> -		inode->i_size = le32_to_cpu(sqsh_ino->symlink_size);
> +		inode->i_size = symlink_size;

NACK. I see no reason to introduce an intermediate variable here.

Please do what Al Viro suggested.

Thanks

Phillip Lougher
--
Squashfs author and maintainer

BTW I have been on vacation since last week, and only saw
this today.

>   		inode->i_op = &squashfs_symlink_inode_ops;
>   		inode_nohighmem(inode);
>   		inode->i_data.a_ops = &squashfs_symlink_aops;
Al Viro Aug. 4, 2024, 9:20 p.m. UTC | #2
On Sun, Aug 04, 2024 at 10:16:05PM +0100, Phillip Lougher wrote:

> NACK. I see no reason to introduce an intermediate variable here.
> 
> Please do what Al Viro suggested.

Alternatively, just check ->i_size after assignment.  loff_t is
always a 64bit signed; le32_to_cpu() returns 32bit unsigned.
Conversion from u32 to s64 is always going to yield a non-negative
result; comparison with PAGE_SIZE is all you need there.
Phillip Lougher Aug. 4, 2024, 10:31 p.m. UTC | #3
On 04/08/2024 22:20, Al Viro wrote:
> On Sun, Aug 04, 2024 at 10:16:05PM +0100, Phillip Lougher wrote:
> 
>> NACK. I see no reason to introduce an intermediate variable here.
>>
>> Please do what Al Viro suggested.
> 
> Alternatively, just check ->i_size after assignment.  loff_t is
> always a 64bit signed; le32_to_cpu() returns 32bit unsigned.
> Conversion from u32 to s64 is always going to yield a non-negative
> result; comparison with PAGE_SIZE is all you need there.

I'm happy with that as well.

Thanks

Phillip
Lizhi Xu Aug. 5, 2024, 1:02 a.m. UTC | #4
On Sun, 4 Aug 2024 22:20:34 +0100, Al Viro wrote:
> Alternatively, just check ->i_size after assignment.  loff_t is
> always a 64bit signed; le32_to_cpu() returns 32bit unsigned.
> Conversion from u32 to s64 is always going to yield a non-negative
> result; comparison with PAGE_SIZE is all you need there.
It is int overflow, not others. 
Please see my V7 patch,
Link: https://lore.kernel.org/all/20240803074349.3599957-1-lizhi.xu@windriver.com/

--
Lizhi
Al Viro Aug. 5, 2024, 1:40 a.m. UTC | #5
On Mon, Aug 05, 2024 at 09:02:31AM +0800, Lizhi Xu wrote:
> On Sun, 4 Aug 2024 22:20:34 +0100, Al Viro wrote:
> > Alternatively, just check ->i_size after assignment.  loff_t is
> > always a 64bit signed; le32_to_cpu() returns 32bit unsigned.
> > Conversion from u32 to s64 is always going to yield a non-negative
> > result; comparison with PAGE_SIZE is all you need there.

> It is int overflow, not others. 

Excuse me, what?

> Please see my V7 patch,
> Link: https://lore.kernel.org/all/20240803074349.3599957-1-lizhi.xu@windriver.com/

I have seen your patch.  Integer overflow has nothing to do with
the problem here.

Please, show me an unsigned int value N such that

_Bool mismatch(unsigned int N)
{
	u32 v32 = N;
	loff_t v64 = N;

	return (v32 > PAGE_SIZE) != (v64 > PAGE_SIZE);
}

would yield true if passed that value as an argument.

Note that assignment of le32_to_cpu() result to inode->i_size
does conversion from unsigned 32bit integer type to a signed 64bit
integer type.  Since the range of the former fits into the range of the
latter, conversion preserves value.  In other words, possible values
of inode->i_size after such assignment are from 0 to (loff_t)0xfffffff
and (inode->i_size > PAGE_SIZE) is true in exactly the same cases when
(symlink_size > PAGE_SIZE) is true with your patch.

Again, on all architectures inode->i_size is capable of representing
all values in range 0..4G-1 (for rather obvious reasons - we want the
kernel to be able to work with files larger than 4Gb).  There is
no wraparound of any kind on that assignment.

See https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf,
in particular sections 6.5.16.1[2] and 6.3.1.3[1]
Christian Brauner Aug. 5, 2024, 7:03 a.m. UTC | #6
On Sun, Aug 04, 2024 at 11:31:51PM GMT, Phillip Lougher wrote:
> On 04/08/2024 22:20, Al Viro wrote:
> > On Sun, Aug 04, 2024 at 10:16:05PM +0100, Phillip Lougher wrote:
> > 
> > > NACK. I see no reason to introduce an intermediate variable here.
> > > 
> > > Please do what Al Viro suggested.
> > 
> > Alternatively, just check ->i_size after assignment.  loff_t is
> > always a 64bit signed; le32_to_cpu() returns 32bit unsigned.
> > Conversion from u32 to s64 is always going to yield a non-negative
> > result; comparison with PAGE_SIZE is all you need there.
> 
> I'm happy with that as well.

Fwiw, I think a good way to end this v7+ patch streak is to just tweak
the last version when applying.
Lizhi Xu Aug. 6, 2024, 2:56 a.m. UTC | #7
On Mon, 5 Aug 2024 02:40:37 +0100, Al Viro wrote:
> > It is int overflow, not others.
> 
> Excuse me, what?
> 
> > Please see my V7 patch,
> > Link: https://lore.kernel.org/all/20240803074349.3599957-1-lizhi.xu@windriver.com/
> 
> I have seen your patch.  Integer overflow has nothing to do with
> the problem here.
No, the fix to this issue is due to the length overflow of int type caused
by the i_size of loff_t type (in the function squashfs_symlink_read_folio).
> 
> Please, show me an unsigned int value N such that
> 
> _Bool mismatch(unsigned int N)
> {
> 	u32 v32 = N;
> 	loff_t v64 = N;
> 
> 	return (v32 > PAGE_SIZE) != (v64 > PAGE_SIZE);
> }
This always return 0, why are you asking this?
> 
> would yield true if passed that value as an argument.
> 
> Note that assignment of le32_to_cpu() result to inode->i_size
> does conversion from unsigned 32bit integer type to a signed 64bit
> integer type.  Since the range of the former fits into the range of the
> latter, conversion preserves value.  In other words, possible values
> of inode->i_size after such assignment are from 0 to (loff_t)0xfffffff
> and (inode->i_size > PAGE_SIZE) is true in exactly the same cases when
> (symlink_size > PAGE_SIZE) is true with your patch.
> 
> Again, on all architectures inode->i_size is capable of representing
> all values in range 0..4G-1 (for rather obvious reasons - we want the
> kernel to be able to work with files larger than 4Gb).  There is
> no wraparound of any kind on that assignment.
The type of loff_t is long long, so its values range is not 0..4G-1.

--
Lizhi
Al Viro Aug. 6, 2024, 4:59 a.m. UTC | #8
On Tue, Aug 06, 2024 at 10:56:09AM +0800, Lizhi Xu wrote:

> > Please, show me an unsigned int value N such that
> > 
> > _Bool mismatch(unsigned int N)
> > {
> > 	u32 v32 = N;
> > 	loff_t v64 = N;
> > 
> > 	return (v32 > PAGE_SIZE) != (v64 > PAGE_SIZE);
> > }
> This always return 0, why are you asking this?

Because that implies the equivalence between

	symlink_size = le32_to_cpu(something);
	if (symlink_size > PAGE_SIZE)
		return -EINVAL;
	inode->i_size = symlink_size;

and

	inode->i_size = le32_to_cpu(something);
	if (inode->i_size > PAGE_SIZE)
		return -EINVAL;

However, you seem to find some problem in the latter form, and
your explanations of the reasons have been hard to understand.

> > Again, on all architectures inode->i_size is capable of representing
> > all values in range 0..4G-1 (for rather obvious reasons - we want the
> > kernel to be able to work with files larger than 4Gb).  There is
> > no wraparound of any kind on that assignment.

> The type of loff_t is long long, so its values range is not 0..4G-1.

6.3.1.3[1] When a value with integer type is converted to another integer type
other than _Bool, if the value can be represented by the new type, it is unchanged.

Possible values of u32 are all in range 0..4G-1.  All numbers in that range
(and many others as well, but that is irrelevant here) can be represented by
loff_t.  In other words, nothing overflow-related is happening.
Lizhi Xu Aug. 6, 2024, 6:19 a.m. UTC | #9
On Tue, 6 Aug 2024 05:59:05 +0100, Al Viro wrote:
> > > Please, show me an unsigned int value N such that
> > >
> > > _Bool mismatch(unsigned int N)
> > > {
> > > 	u32 v32 = N;
> > > 	loff_t v64 = N;
> > >
> > > 	return (v32 > PAGE_SIZE) != (v64 > PAGE_SIZE);
> > > }
> > This always return 0, why are you asking this?
> 
> Because that implies the equivalence between
> 
> 	symlink_size = le32_to_cpu(something);
> 	if (symlink_size > PAGE_SIZE)
> 		return -EINVAL;
> 	inode->i_size = symlink_size;
> 
> and
> 
> 	inode->i_size = le32_to_cpu(something);
> 	if (inode->i_size > PAGE_SIZE)
> 		return -EINVAL;
> 
> However, you seem to find some problem in the latter form, and
> your explanations of the reasons have been hard to understand.
Here are the uninit-value related calltrace reports from syzbot:

page_get_link()->
  read_mapping_page()->
    read_cache_page()->
      do_read_cache_page()->
        do_read_cache_folio()->
          filemap_read_folio()->
            squashfs_symlink_read_folio()

fs/squashfs/symlink.c
 8 static int squashfs_symlink_read_folio(struct file *file, struct folio *folio)
 7 {
 6         struct inode *inode = folio->mapping->host;
 5         struct super_block *sb = inode->i_sb;
 4         struct squashfs_sb_info *msblk = sb->s_fs_info;
 3         int index = folio_pos(folio);
 2         u64 block = squashfs_i(inode)->start;
 1         int offset = squashfs_i(inode)->offset;
41         int length = min_t(int, i_size_read(inode) - index, PAGE_SIZE);

Please see line 41, because the value of i_size is too large, causing integer overflow
in the variable length. Which can result in folio not being initialized (as reported by 
Syzbot: "KMSAN: uninit-value in pick_link").

My solution is to check if the value of symlink_size is too large before
initializing i_size with symlink_size. If it is, return -EINVAL.
> 
> > > Again, on all architectures inode->i_size is capable of representing
> > > all values in range 0..4G-1 (for rather obvious reasons - we want the
> > > kernel to be able to work with files larger than 4Gb).  There is
> > > no wraparound of any kind on that assignment.
> 
> > The type of loff_t is long long, so its values range is not 0..4G-1.
> 
> 6.3.1.3[1] When a value with integer type is converted to another integer type
> other than _Bool, if the value can be represented by the new type, it is unchanged.
> 
> Possible values of u32 are all in range 0..4G-1.  All numbers in that range
> (and many others as well, but that is irrelevant here) can be represented by
> loff_t.  In other words, nothing overflow-related is happening.

--
Lizhi
Al Viro Aug. 6, 2024, 6:58 a.m. UTC | #10
On Tue, Aug 06, 2024 at 02:19:12PM +0800, Lizhi Xu wrote:

> > However, you seem to find some problem in the latter form, and
> > your explanations of the reasons have been hard to understand.
> Here are the uninit-value related calltrace reports from syzbot:
> 
> page_get_link()->
>   read_mapping_page()->
>     read_cache_page()->
>       do_read_cache_page()->
>         do_read_cache_folio()->
>           filemap_read_folio()->
>             squashfs_symlink_read_folio()
> 
> fs/squashfs/symlink.c
>  8 static int squashfs_symlink_read_folio(struct file *file, struct folio *folio)
>  7 {
>  6         struct inode *inode = folio->mapping->host;
>  5         struct super_block *sb = inode->i_sb;
>  4         struct squashfs_sb_info *msblk = sb->s_fs_info;
>  3         int index = folio_pos(folio);
>  2         u64 block = squashfs_i(inode)->start;
>  1         int offset = squashfs_i(inode)->offset;
> 41         int length = min_t(int, i_size_read(inode) - index, PAGE_SIZE);
> 
> Please see line 41, because the value of i_size is too large, causing integer overflow
> in the variable length. Which can result in folio not being initialized (as reported by 
> Syzbot: "KMSAN: uninit-value in pick_link").

What does that have to do with anything?  In the code you've quoted, ->i_size - index
is explicitly cast to signed 32bit.  _That_ will wrap around.  On typecast.  Nothing
of that sort would be present in
	if (inode->i_size > PAGE_SIZE)
as you could have verified easily.

At that point the only thing I can recommend is googling for "first law of holes".
diff mbox series

Patch

diff --git a/fs/squashfs/inode.c b/fs/squashfs/inode.c
index 16bd693d0b3a..bed6764e4461 100644
--- a/fs/squashfs/inode.c
+++ b/fs/squashfs/inode.c
@@ -273,14 +273,21 @@  int squashfs_read_inode(struct inode *inode, long long ino)
 	case SQUASHFS_SYMLINK_TYPE:
 	case SQUASHFS_LSYMLINK_TYPE: {
 		struct squashfs_symlink_inode *sqsh_ino = &squashfs_ino.symlink;
+		loff_t symlink_size;
 
 		err = squashfs_read_metadata(sb, sqsh_ino, &block, &offset,
 				sizeof(*sqsh_ino));
 		if (err < 0)
 			goto failed_read;
 
+		symlink_size = le32_to_cpu(sqsh_ino->symlink_size);
+		if (symlink_size > PAGE_SIZE) {
+			ERROR("Corrupted symlink, size [%llu]\n", symlink_size);
+			return -EINVAL;
+		}
+
 		set_nlink(inode, le32_to_cpu(sqsh_ino->nlink));
-		inode->i_size = le32_to_cpu(sqsh_ino->symlink_size);
+		inode->i_size = symlink_size;
 		inode->i_op = &squashfs_symlink_inode_ops;
 		inode_nohighmem(inode);
 		inode->i_data.a_ops = &squashfs_symlink_aops;