diff mbox series

[v4,4/4] generic: test fs-verity EFBIG scenarios

Message ID 508058f805a45808764a477e9ad04353a841cf53.1620248200.git.boris@bur.io (mailing list archive)
State New, archived
Headers show
Series tests for btrfs fsverity | expand

Commit Message

Boris Burkov May 5, 2021, 9:04 p.m. UTC
btrfs, ext4, and f2fs cache the Merkle tree past EOF, which restricts
the maximum file size beneath the normal maximum. Test the logic in
those filesystems against files with sizes near the maximum.

To work properly, this does require some understanding of the practical
but not standardized layout of the Merkle tree. This is a bit unpleasant
and could make the test incorrect in the future, if the implementation
changes. On the other hand, it feels quite useful to test this tricky
edge case. It could perhaps be made more generic by adding some ioctls
to let the file system communicate the maximum file size for a verity
file or some information about the storage of the Merkle tree.

Signed-off-by: Boris Burkov <boris@bur.io>
---
 tests/generic/632     | 86 +++++++++++++++++++++++++++++++++++++++++++
 tests/generic/632.out |  7 ++++
 tests/generic/group   |  1 +
 3 files changed, 94 insertions(+)
 create mode 100755 tests/generic/632
 create mode 100644 tests/generic/632.out

Comments

Eryu Guan May 16, 2021, 4:47 p.m. UTC | #1
On Wed, May 05, 2021 at 02:04:46PM -0700, Boris Burkov wrote:
> btrfs, ext4, and f2fs cache the Merkle tree past EOF, which restricts
> the maximum file size beneath the normal maximum. Test the logic in
> those filesystems against files with sizes near the maximum.
> 
> To work properly, this does require some understanding of the practical
> but not standardized layout of the Merkle tree. This is a bit unpleasant
> and could make the test incorrect in the future, if the implementation
> changes. On the other hand, it feels quite useful to test this tricky
> edge case. It could perhaps be made more generic by adding some ioctls
> to let the file system communicate the maximum file size for a verity
> file or some information about the storage of the Merkle tree.
> 
> Signed-off-by: Boris Burkov <boris@bur.io>
> ---
>  tests/generic/632     | 86 +++++++++++++++++++++++++++++++++++++++++++
>  tests/generic/632.out |  7 ++++
>  tests/generic/group   |  1 +
>  3 files changed, 94 insertions(+)
>  create mode 100755 tests/generic/632
>  create mode 100644 tests/generic/632.out
> 
> diff --git a/tests/generic/632 b/tests/generic/632
> new file mode 100755
> index 00000000..5a5ed576
> --- /dev/null
> +++ b/tests/generic/632
> @@ -0,0 +1,86 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2021 Facebook, Inc.  All Rights Reserved.
> +#
> +# FS QA Test 632
> +#
> +# Test some EFBIG scenarios with very large files.
> +# To create the files, use pwrite with an offset close to the
> +# file system's max file size.
> +#
> +seq=`basename $0`
> +seqres=$RESULT_DIR/$seq
> +echo "QA output created by $seq"
> +
> +here=`pwd`
> +tmp=/tmp/$$
> +status=1	# failure is the default!
> +trap "_cleanup; exit \$status" 0 1 2 3 15
> +
> +_cleanup()
> +{
> +	cd /
> +	rm -f $tmp.*
> +}
> +
> +# get standard environment, filters and checks
> +. ./common/rc
> +. ./common/filter
> +. ./common/verity
> +
> +# remove previous $seqres.full before test
> +rm -f $seqres.full
> +
> +# real QA test starts here
> +
> +# Modify as appropriate.
> +_supported_fs btrfs ext4 f2fs

A generic test should '_supported_fs generic', and use proper _require
rules to filter unsupported filesystems.

> +_require_test
> +_require_math
> +_require_scratch_verity
> +
> +_scratch_mkfs_verity &>> $seqres.full
> +_scratch_mount
> +
> +fsv_file=$SCRATCH_MNT/file.fsv
> +
> +max_sz=$(_get_max_file_size)
> +_fsv_scratch_begin_subtest "way too big: fail on first merkle block"
> +# have to go back by 4096 from max to not hit the fsverity MAX_DEPTH check.
> +$XFS_IO_PROG -fc "pwrite -q $(($max_sz - 4096)) 1" $fsv_file
> +_fsv_enable $fsv_file |& _filter_scratch
> +
> +# The goal of this second test is to make a big enough file that we trip the
> +# EFBIG codepath, but not so big that we hit it immediately as soon as we try
> +# to write a Merkle leaf. Because of the layout of the Merkle tree that
> +# fs-verity uses, this is a bit complicated to compute dynamically.
> +
> +# The layout of the Merkle tree has the leaf nodes last, but writes them first.
> +# To get an interesting overflow, we need the start of L0 to be < MAX but the
> +# end of the merkle tree (EOM) to be past MAX. Ideally, the start of L0 is only
> +# just smaller than MAX, so that we don't have to write many blocks to blow up.
> +
> +# 0                        EOF round-to-64k L7L6L5 L4   L3    L2    L1  L0 MAX  EOM
> +# |-------------------------|               ||-|--|---|----|-----|------|--|!!!!!|
> +
> +# Given this structure, we can compute the size of the file that yields the
> +# desired properties:
> +# sz + 64k + sz/128^8 + sz/128^7 + ... + sz/128^2 < MAX
> +# (128^8)sz + (128^8)64k + sz + (128)sz + (128^2)sz + ... + (128^6)sz < (128^8)MAX
> +# sz(128^8 + 128^6 + 128^5 + 128^4 + 128^3 + 128^2 + 128 + 1) < (128^8)(MAX - 64k)
> +# sz < (128^8/(128^8 + (128^6 + ... 1))(MAX - 64k)
> +#
> +# Do the actual caclulation with 'bc' and 20 digits of precision.
> +set -f
> +calc="scale=20; ($max_sz - 65536) * ((128^8) / (1 + 128 + 128^2 + 128^3 + 128^4 + 128^5 + 128^6 + 128^8))"
> +sz=$(echo $calc | $BC -q | cut -d. -f1)
> +set +f

Now sure why set -f is needed here, add some comments about it as well?

Thanks,
Eryu

> +
> +
> +_fsv_scratch_begin_subtest "still too big: fail on first invalid merkle block"
> +$XFS_IO_PROG -fc "pwrite -q $sz 1" $fsv_file
> +_fsv_enable $fsv_file |& _filter_scratch
> +
> +# success, all done
> +status=0
> +exit
> diff --git a/tests/generic/632.out b/tests/generic/632.out
> new file mode 100644
> index 00000000..59602c24
> --- /dev/null
> +++ b/tests/generic/632.out
> @@ -0,0 +1,7 @@
> +QA output created by 632
> +
> +# way too big: fail on first merkle block
> +ERROR: FS_IOC_ENABLE_VERITY failed on 'SCRATCH_MNT/file.fsv': File too large
> +
> +# still too big: fail on first invalid merkle block
> +ERROR: FS_IOC_ENABLE_VERITY failed on 'SCRATCH_MNT/file.fsv': File too large
> diff --git a/tests/generic/group b/tests/generic/group
> index ab00cc04..76d46e86 100644
> --- a/tests/generic/group
> +++ b/tests/generic/group
> @@ -634,3 +634,4 @@
>  629 auto quick rw copy_range
>  630 auto quick rw dedupe clone
>  631 auto rw overlay rename
> +632 auto quick verity
> -- 
> 2.30.2
Eric Biggers May 25, 2021, 8:24 p.m. UTC | #2
On Wed, May 05, 2021 at 02:04:46PM -0700, Boris Burkov wrote:
> diff --git a/tests/generic/632 b/tests/generic/632
> new file mode 100755
> index 00000000..5a5ed576
> --- /dev/null
> +++ b/tests/generic/632
> @@ -0,0 +1,86 @@
> +#! /bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +# Copyright (c) 2021 Facebook, Inc.  All Rights Reserved.
> +#
> +# FS QA Test 632
> +#
> +# Test some EFBIG scenarios with very large files.
> +# To create the files, use pwrite with an offset close to the
> +# file system's max file size.

Can you please make this comment properly describe the purpose of this test?
As-is it doesn't mention that it is related to fs-verity at all, let alone to
specific filesystems' implementations of fs-verity.

> +max_sz=$(_get_max_file_size)
> +_fsv_scratch_begin_subtest "way too big: fail on first merkle block"
> +# have to go back by 4096 from max to not hit the fsverity MAX_DEPTH check.

What is meant by the "fsverity MAX_DEPTH" check?

> +$XFS_IO_PROG -fc "pwrite -q $(($max_sz - 4096)) 1" $fsv_file
> +_fsv_enable $fsv_file |& _filter_scratch

Using the "truncate" xfs_io command instead of "pwrite" would probably make more
sense here, as the goal is to just create a file of a specific size.

> +
> +# The goal of this second test is to make a big enough file that we trip the
> +# EFBIG codepath, but not so big that we hit it immediately as soon as we try
> +# to write a Merkle leaf. Because of the layout of the Merkle tree that
> +# fs-verity uses, this is a bit complicated to compute dynamically.
> +
> +# The layout of the Merkle tree has the leaf nodes last, but writes them first.
> +# To get an interesting overflow, we need the start of L0 to be < MAX but the
> +# end of the merkle tree (EOM) to be past MAX. Ideally, the start of L0 is only
> +# just smaller than MAX, so that we don't have to write many blocks to blow up.
> +
> +# 0                        EOF round-to-64k L7L6L5 L4   L3    L2    L1  L0 MAX  EOM
> +# |-------------------------|               ||-|--|---|----|-----|------|--|!!!!!|
> +
> +# Given this structure, we can compute the size of the file that yields the
> +# desired properties:
> +# sz + 64k + sz/128^8 + sz/128^7 + ... + sz/128^2 < MAX
> +# (128^8)sz + (128^8)64k + sz + (128)sz + (128^2)sz + ... + (128^6)sz < (128^8)MAX
> +# sz(128^8 + 128^6 + 128^5 + 128^4 + 128^3 + 128^2 + 128 + 1) < (128^8)(MAX - 64k)
> +# sz < (128^8/(128^8 + (128^6 + ... 1))(MAX - 64k)
> +#
> +# Do the actual caclulation with 'bc' and 20 digits of precision.

This calculation isn't completely accurate because it doesn't round the levels
to a block boundary.  Nor does it consider that the 64K is an alignment rather
than a fixed amount added.

But for the test you don't need the absolute largest file whose level 1 doesn't
exceed the limit, but rather just one almost that large.

So it would be okay to add 64K as a fixed amount, along with 4K for every level
on top of the 'sz/128^(level+1)' you already have, to get an over-estimate of
the amount of extra space needed to cache the Merkle tree.

But please make it clear that it's an over-estimate, and hence an under-estimate
of the file size desired for the test.

Also please document that this is all assuming SHA-256 with 4K blocks, and also
that the maximum file size is assumed to fit in 64 bits; hence the consideration
of 8 levels is sufficient.

- Eric
Boris Burkov Sept. 10, 2021, 11:26 p.m. UTC | #3
On Tue, May 25, 2021 at 01:24:11PM -0700, Eric Biggers wrote:
> On Wed, May 05, 2021 at 02:04:46PM -0700, Boris Burkov wrote:
> > diff --git a/tests/generic/632 b/tests/generic/632
> > new file mode 100755
> > index 00000000..5a5ed576
> > --- /dev/null
> > +++ b/tests/generic/632
> > @@ -0,0 +1,86 @@
> > +#! /bin/bash
> > +# SPDX-License-Identifier: GPL-2.0
> > +# Copyright (c) 2021 Facebook, Inc.  All Rights Reserved.
> > +#
> > +# FS QA Test 632
> > +#
> > +# Test some EFBIG scenarios with very large files.
> > +# To create the files, use pwrite with an offset close to the
> > +# file system's max file size.
> 
> Can you please make this comment properly describe the purpose of this test?
> As-is it doesn't mention that it is related to fs-verity at all, let alone to
> specific filesystems' implementations of fs-verity.

Sorry for disappearing on this one for a while.

Oops, good point. In addressing your and Eryu's points, I realized that
this isn't really a generic test, since as you say, it assumes the
filesystem's implementation. Further, I think it is plausible for an fs
to cache the Merkle tree pages some other way which wouldn't need to
EFBIG for large files. With that said, I do think it's a useful test of
an edge case I got wrong several times in the btrfs implementation.

I am leaning towards making this a btrfs specific test. Just wanted to
double check with you if you think ext4 and f2fs would benefit from
running this test too..

> 
> > +max_sz=$(_get_max_file_size)
> > +_fsv_scratch_begin_subtest "way too big: fail on first merkle block"
> > +# have to go back by 4096 from max to not hit the fsverity MAX_DEPTH check.
> 
> What is meant by the "fsverity MAX_DEPTH" check?

If you use $max_sz or $max_sz-1 (or anything bigger than $max_sz-4096)
the vfs fsverity code will conclude the tree will exceed MAX_LEVELS. I
got LEVELS and DEPTH mixed up.

> 
> > +$XFS_IO_PROG -fc "pwrite -q $(($max_sz - 4096)) 1" $fsv_file
> > +_fsv_enable $fsv_file |& _filter_scratch
> 
> Using the "truncate" xfs_io command instead of "pwrite" would probably make more
> sense here, as the goal is to just create a file of a specific size.

In my memory, truncate didn't work for btrfs, but it took me a while to
get this to work, so I might have made some silly mistake early on with
truncate. I'll try again to be sure.

> 
> > +
> > +# The goal of this second test is to make a big enough file that we trip the
> > +# EFBIG codepath, but not so big that we hit it immediately as soon as we try
> > +# to write a Merkle leaf. Because of the layout of the Merkle tree that
> > +# fs-verity uses, this is a bit complicated to compute dynamically.
> > +
> > +# The layout of the Merkle tree has the leaf nodes last, but writes them first.
> > +# To get an interesting overflow, we need the start of L0 to be < MAX but the
> > +# end of the merkle tree (EOM) to be past MAX. Ideally, the start of L0 is only
> > +# just smaller than MAX, so that we don't have to write many blocks to blow up.
> > +
> > +# 0                        EOF round-to-64k L7L6L5 L4   L3    L2    L1  L0 MAX  EOM
> > +# |-------------------------|               ||-|--|---|----|-----|------|--|!!!!!|
> > +
> > +# Given this structure, we can compute the size of the file that yields the
> > +# desired properties:
> > +# sz + 64k + sz/128^8 + sz/128^7 + ... + sz/128^2 < MAX
> > +# (128^8)sz + (128^8)64k + sz + (128)sz + (128^2)sz + ... + (128^6)sz < (128^8)MAX
> > +# sz(128^8 + 128^6 + 128^5 + 128^4 + 128^3 + 128^2 + 128 + 1) < (128^8)(MAX - 64k)
> > +# sz < (128^8/(128^8 + (128^6 + ... 1))(MAX - 64k)
> > +#
> > +# Do the actual caclulation with 'bc' and 20 digits of precision.
> 
> This calculation isn't completely accurate because it doesn't round the levels
> to a block boundary.  Nor does it consider that the 64K is an alignment rather
> than a fixed amount added.
> 
> But for the test you don't need the absolute largest file whose level 1 doesn't
> exceed the limit, but rather just one almost that large.
> 
> So it would be okay to add 64K as a fixed amount, along with 4K for every level
> on top of the 'sz/128^(level+1)' you already have, to get an over-estimate of
> the amount of extra space needed to cache the Merkle tree.
> 
> But please make it clear that it's an over-estimate, and hence an under-estimate
> of the file size desired for the test.
> 
> Also please document that this is all assuming SHA-256 with 4K blocks, and also
> that the maximum file size is assumed to fit in 64 bits; hence the consideration
> of 8 levels is sufficient.

Agreed with all of this, will do.

> 
> - Eric
Eric Biggers Sept. 10, 2021, 11:32 p.m. UTC | #4
On Fri, Sep 10, 2021 at 04:26:42PM -0700, Boris Burkov wrote:
> 
> I am leaning towards making this a btrfs specific test. Just wanted to
> double check with you if you think ext4 and f2fs would benefit from
> running this test too..

It's applicable to ext4 and f2fs too, so it probably should be kept as a generic
test.  Just make sure that filesystems have to "opt in" to it (with a new
function _require_fsverity_max_file_size_limit() in common/verity or something),
since it's probably not going to be applicable to every filesystem that
implements fs-verity.

- Eric
diff mbox series

Patch

diff --git a/tests/generic/632 b/tests/generic/632
new file mode 100755
index 00000000..5a5ed576
--- /dev/null
+++ b/tests/generic/632
@@ -0,0 +1,86 @@ 
+#! /bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2021 Facebook, Inc.  All Rights Reserved.
+#
+# FS QA Test 632
+#
+# Test some EFBIG scenarios with very large files.
+# To create the files, use pwrite with an offset close to the
+# file system's max file size.
+#
+seq=`basename $0`
+seqres=$RESULT_DIR/$seq
+echo "QA output created by $seq"
+
+here=`pwd`
+tmp=/tmp/$$
+status=1	# failure is the default!
+trap "_cleanup; exit \$status" 0 1 2 3 15
+
+_cleanup()
+{
+	cd /
+	rm -f $tmp.*
+}
+
+# get standard environment, filters and checks
+. ./common/rc
+. ./common/filter
+. ./common/verity
+
+# remove previous $seqres.full before test
+rm -f $seqres.full
+
+# real QA test starts here
+
+# Modify as appropriate.
+_supported_fs btrfs ext4 f2fs
+_require_test
+_require_math
+_require_scratch_verity
+
+_scratch_mkfs_verity &>> $seqres.full
+_scratch_mount
+
+fsv_file=$SCRATCH_MNT/file.fsv
+
+max_sz=$(_get_max_file_size)
+_fsv_scratch_begin_subtest "way too big: fail on first merkle block"
+# have to go back by 4096 from max to not hit the fsverity MAX_DEPTH check.
+$XFS_IO_PROG -fc "pwrite -q $(($max_sz - 4096)) 1" $fsv_file
+_fsv_enable $fsv_file |& _filter_scratch
+
+# The goal of this second test is to make a big enough file that we trip the
+# EFBIG codepath, but not so big that we hit it immediately as soon as we try
+# to write a Merkle leaf. Because of the layout of the Merkle tree that
+# fs-verity uses, this is a bit complicated to compute dynamically.
+
+# The layout of the Merkle tree has the leaf nodes last, but writes them first.
+# To get an interesting overflow, we need the start of L0 to be < MAX but the
+# end of the merkle tree (EOM) to be past MAX. Ideally, the start of L0 is only
+# just smaller than MAX, so that we don't have to write many blocks to blow up.
+
+# 0                        EOF round-to-64k L7L6L5 L4   L3    L2    L1  L0 MAX  EOM
+# |-------------------------|               ||-|--|---|----|-----|------|--|!!!!!|
+
+# Given this structure, we can compute the size of the file that yields the
+# desired properties:
+# sz + 64k + sz/128^8 + sz/128^7 + ... + sz/128^2 < MAX
+# (128^8)sz + (128^8)64k + sz + (128)sz + (128^2)sz + ... + (128^6)sz < (128^8)MAX
+# sz(128^8 + 128^6 + 128^5 + 128^4 + 128^3 + 128^2 + 128 + 1) < (128^8)(MAX - 64k)
+# sz < (128^8/(128^8 + (128^6 + ... 1))(MAX - 64k)
+#
+# Do the actual caclulation with 'bc' and 20 digits of precision.
+set -f
+calc="scale=20; ($max_sz - 65536) * ((128^8) / (1 + 128 + 128^2 + 128^3 + 128^4 + 128^5 + 128^6 + 128^8))"
+sz=$(echo $calc | $BC -q | cut -d. -f1)
+set +f
+
+
+_fsv_scratch_begin_subtest "still too big: fail on first invalid merkle block"
+$XFS_IO_PROG -fc "pwrite -q $sz 1" $fsv_file
+_fsv_enable $fsv_file |& _filter_scratch
+
+# success, all done
+status=0
+exit
diff --git a/tests/generic/632.out b/tests/generic/632.out
new file mode 100644
index 00000000..59602c24
--- /dev/null
+++ b/tests/generic/632.out
@@ -0,0 +1,7 @@ 
+QA output created by 632
+
+# way too big: fail on first merkle block
+ERROR: FS_IOC_ENABLE_VERITY failed on 'SCRATCH_MNT/file.fsv': File too large
+
+# still too big: fail on first invalid merkle block
+ERROR: FS_IOC_ENABLE_VERITY failed on 'SCRATCH_MNT/file.fsv': File too large
diff --git a/tests/generic/group b/tests/generic/group
index ab00cc04..76d46e86 100644
--- a/tests/generic/group
+++ b/tests/generic/group
@@ -634,3 +634,4 @@ 
 629 auto quick rw copy_range
 630 auto quick rw dedupe clone
 631 auto rw overlay rename
+632 auto quick verity