| #! /bin/bash |
| # SPDX-License-Identifier: GPL-2.0 |
| # Copyright 2018 Google LLC |
| # |
| # FS QA Test generic/574 |
| # |
| # Test corrupting verity files. This test corrupts various parts of the |
| # contents of a verity file, or parts of its Merkle tree, by writing directly to |
| # the block device. It verifies that this causes I/O errors when the relevant |
| # part of the contents is later read by any means. |
| # |
| . ./common/preamble |
| _begin_fstest auto quick verity mmap |
| |
| # Override the default cleanup function. |
| _cleanup() |
| { |
| cd / |
| _restore_fsverity_signatures |
| rm -f $tmp.* |
| } |
| |
| # Import common functions. |
| . ./common/filter |
| . ./common/verity |
| |
| _require_scratch_verity |
| _disable_fsverity_signatures |
| _require_fsverity_corruption |
| |
| _scratch_mkfs_verity &>> $seqres.full |
| _scratch_mount |
| fsv_orig_file=$SCRATCH_MNT/file |
| fsv_file=$SCRATCH_MNT/file.fsv |
| |
| setup_zeroed_file() |
| { |
| local block_size=$1 |
| local file_len=$2 |
| local sparse=$3 |
| |
| if $sparse; then |
| dd if=/dev/zero of=$fsv_orig_file bs=1 count=0 seek=$file_len \ |
| status=none |
| else |
| head -c $file_len /dev/zero > $fsv_orig_file |
| fi |
| cp $fsv_orig_file $fsv_file |
| _fsv_enable $fsv_file --block-size=$block_size |
| cmp $fsv_orig_file $fsv_file |
| } |
| |
| corruption_test() |
| { |
| local block_size=$1 |
| local file_len=$2 |
| local zap_offset=$3 |
| local zap_len=$4 |
| local is_merkle_tree=${5:-false} # if true, zap tree instead of data |
| local use_sparse_file=${6:-false} |
| |
| local paramstr="block_size=$block_size" |
| paramstr+=", file_len=$file_len" |
| paramstr+=", zap_offset=$zap_offset" |
| paramstr+=", zap_len=$zap_len" |
| paramstr+=", is_merkle_tree=$is_merkle_tree" |
| paramstr+=", use_sparse_file=$use_sparse_file" |
| |
| if $is_merkle_tree; then |
| local corrupt_func=_fsv_scratch_corrupt_merkle_tree |
| else |
| local corrupt_func=_fsv_scratch_corrupt_bytes |
| fi |
| local fs_block_size=$(_get_block_size $SCRATCH_MNT) |
| |
| rm -rf "${SCRATCH_MNT:?}"/* |
| setup_zeroed_file $block_size $file_len $use_sparse_file |
| |
| # Corrupt part of the file (data or Merkle tree). |
| head -c $zap_len /dev/zero | tr '\0' X \ |
| | $corrupt_func $fsv_file $zap_offset |
| |
| # Reading the full file with buffered I/O should fail. |
| _scratch_cycle_mount |
| if cat $fsv_file >/dev/null 2>$tmp.err; then |
| echo "Unexpectedly was able to read full file ($paramstr)" |
| elif ! grep -q 'Input/output error' $tmp.err; then |
| echo "Wrong error reading full file ($paramstr):" |
| cat $tmp.err |
| fi |
| |
| # Reading the full file with direct I/O should fail. |
| _scratch_cycle_mount |
| if dd if=$fsv_file bs=$fs_block_size iflag=direct status=none \ |
| of=/dev/null 2>$tmp.err |
| then |
| echo "Unexpectedly was able to read full file with DIO ($paramstr)" |
| elif ! grep -q 'Input/output error' $tmp.err; then |
| echo "Wrong error reading full file with DIO ($paramstr):" |
| cat $tmp.err |
| fi |
| |
| # Reading just the corrupted part of the file should fail. |
| if ! $is_merkle_tree; then |
| if dd if=$fsv_file bs=1 skip=$zap_offset count=$zap_len \ |
| of=/dev/null status=none 2>$tmp.err; then |
| echo "Unexpectedly was able to read corrupted part ($paramstr)" |
| elif ! grep -q 'Input/output error' $tmp.err; then |
| echo "Wrong error reading corrupted part ($paramstr):" |
| cat $tmp.err |
| fi |
| fi |
| |
| # Reading the full file via mmap should fail. |
| _mread $fsv_file 0 $file_len >/dev/null 2>$tmp.err |
| if ! grep -q 'Bus error' $tmp.err; then |
| echo "Didn't see SIGBUS when reading file via mmap" |
| cat $tmp.err |
| fi |
| |
| # Reading just the corrupted part via mmap should fail. |
| if ! $is_merkle_tree; then |
| _mread $fsv_file $zap_offset $zap_len >/dev/null 2>$tmp.err |
| if ! grep -q 'Bus error' $tmp.err; then |
| echo "Didn't see SIGBUS when reading corrupted part via mmap" |
| cat $tmp.err |
| fi |
| fi |
| } |
| |
| # Reading the last block of the file with mmap is tricky, so we need to be |
| # a bit careful. Some filesystems read the last block in full, while others |
| # return zeros in the last block past EOF, regardless of the contents on |
| # disk. In the former, corruption should be detected and result in SIGBUS, |
| # while in the latter we would expect zeros past EOF, but no error. |
| corrupt_eof_block_test() |
| { |
| local block_size=$1 |
| local file_len=$2 |
| local zap_len=$3 |
| |
| rm -rf "${SCRATCH_MNT:?}"/* |
| setup_zeroed_file $block_size $file_len false |
| head -c $zap_len /dev/zero | tr '\0' X \ |
| | _fsv_scratch_corrupt_bytes $fsv_file $file_len |
| |
| _mread $fsv_file $file_len $zap_len >$tmp.out 2>$tmp.err |
| |
| head -c $file_len /dev/zero >$tmp.zeroes |
| _mread $tmp.zeroes $file_len $zap_len >$tmp.zeroes_out |
| |
| grep -q 'Bus error' $tmp.err || diff $tmp.out $tmp.zeroes_out |
| } |
| |
| test_block_size() |
| { |
| local block_size=$1 |
| |
| # Note: these tests just overwrite some bytes without checking their |
| # original values. Therefore, make sure to overwrite at least 5 or so |
| # bytes, to make it nearly guaranteed that there will be a change -- |
| # even when the test file is encrypted due to the test_dummy_encryption |
| # mount option being specified. |
| corruption_test $block_size 131072 0 5 |
| corruption_test $block_size 131072 4091 5 |
| corruption_test $block_size 131072 65536 65536 |
| corruption_test $block_size 131072 131067 5 |
| |
| # Test corrupting a block in files of length 1..4 blocks, and test |
| # corrupting each block of a 4-block file. This ensures that all code |
| # paths that might exist due to multi-block hashing optimizations the |
| # fsverity implementation may use get covered, assuming no more than 4 |
| # blocks are hashed at once. E.g., consider an fsverity implementation |
| # that verifies sets of blocks but has a bug when given a single block, |
| # or that has a bug that makes it not verify all the blocks of each set. |
| local i |
| for i in $(seq 1 4); do |
| corruption_test $block_size $((i*block_size)) $((block_size/2)) 5 |
| done |
| for i in $(seq 0 3); do |
| corruption_test $block_size $((4*block_size)) $((i*block_size)) 5 |
| done |
| |
| corrupt_eof_block_test $block_size 130999 72 |
| |
| # Merkle tree corruption. |
| corruption_test $block_size 200000 100 10 true |
| |
| # Sparse file. Corrupting the Merkle tree should still cause reads to |
| # fail, i.e. the filesystem must verify holes. |
| corruption_test $block_size 200000 100 10 true true |
| } |
| |
| # Always test FSV_BLOCK_SIZE. Also test some other block sizes if they happen |
| # to be supported. |
| _fsv_scratch_begin_subtest "Testing block_size=FSV_BLOCK_SIZE" |
| test_block_size $FSV_BLOCK_SIZE |
| for block_size in 1024 4096 16384 65536; do |
| _fsv_scratch_begin_subtest "Testing block_size=$block_size if supported" |
| if (( block_size == FSV_BLOCK_SIZE )); then |
| continue # Skip redundant test case. |
| fi |
| if ! _fsv_can_enable $fsv_file --block-size=$block_size; then |
| echo "block_size=$block_size is unsupported" >> $seqres.full |
| continue |
| fi |
| test_block_size $block_size |
| done |
| |
| # success, all done |
| status=0 |
| exit |