|  | #! /bin/bash | 
|  | # SPDX-License-Identifier: GPL-2.0 | 
|  | # Copyright (c) Luis Chamberlain. All Rights Reserved. | 
|  | # | 
|  | # FS QA Test 749 | 
|  | # | 
|  | # As per POSIX NOTES mmap(2) maps multiples of the system page size, but if the | 
|  | # data mapped is not multiples of the page size the remaining bytes are zeroed | 
|  | # out when mapped and modifications to that region are not written to the file. | 
|  | # On Linux when you write data to such partial page after the end of the | 
|  | # object, the data stays in the page cache even after the file is closed and | 
|  | # unmapped and  even  though  the data  is never written to the file itself, | 
|  | # subsequent mappings may see the modified content. If you go *beyond* this | 
|  | # page, you should get a SIGBUS. This test verifies we zero-fill to page | 
|  | # boundary and ensures we get a SIGBUS if we write to data beyond the system | 
|  | # page size even if the block size is greater than the system page size. | 
|  | . ./common/preamble | 
|  | . ./common/rc | 
|  | _begin_fstest auto quick prealloc | 
|  |  | 
|  | # Import common functions. | 
|  | . ./common/filter | 
|  |  | 
|  | _require_scratch_nocheck | 
|  | _require_test | 
|  | _require_xfs_io_command "truncate" | 
|  | _require_xfs_io_command "falloc" | 
|  |  | 
|  | # _fixed_by_git_commit kernel <pending-upstream> \ | 
|  | #        "filemap: cap PTE range to be created to allowed zero fill in folio_map_range()" | 
|  |  | 
|  | filter_xfs_io_data_unique() | 
|  | { | 
|  | _filter_xfs_io_offset | sed -e 's| |\n|g' | grep -E -v "\.|XX|\*" | \ | 
|  | sort -u | tr -d '\n' | 
|  | } | 
|  |  | 
|  |  | 
|  | setup_zeroed_file() | 
|  | { | 
|  | local file_len=$1 | 
|  | local sparse=$2 | 
|  |  | 
|  | if $sparse; then | 
|  | $XFS_IO_PROG -f -c "truncate $file_len" $test_file | 
|  | else | 
|  | $XFS_IO_PROG -f -c "falloc 0 $file_len" $test_file | 
|  | fi | 
|  | } | 
|  |  | 
|  | mwrite() | 
|  | { | 
|  | local file=$1 | 
|  | local offset=$2 | 
|  | local length=$3 | 
|  | local map_len=${4:-$(_round_up_to_page_boundary $(_get_filesize $file)) } | 
|  |  | 
|  | # Some callers expect xfs_io to crash with SIGBUS due to the mread, | 
|  | # causing the shell to print "Bus error" to stderr.  To allow this | 
|  | # message to be redirected, execute xfs_io in a new shell instance. | 
|  | # However, for this to work reliably, we also need to prevent the new | 
|  | # shell instance from optimizing out the fork and directly exec'ing | 
|  | # xfs_io.  The easiest way to do that is to append 'true' to the | 
|  | # commands, so that xfs_io is no longer the last command the shell sees. | 
|  | bash -c "trap '' SIGBUS; ulimit -c 0; \ | 
|  | $XFS_IO_PROG $file \ | 
|  | -c 'mmap -w 0 $map_len' \ | 
|  | -c 'mwrite $offset $length'; \ | 
|  | true" | 
|  | } | 
|  |  | 
|  | do_mmap_tests() | 
|  | { | 
|  | local block_size=$1 | 
|  | local file_len=$2 | 
|  | local offset=$3 | 
|  | local len=$4 | 
|  | local use_sparse_file=${5:-false} | 
|  | local new_filelen=0 | 
|  | local map_len=0 | 
|  | local csum=0 | 
|  | local fs_block_size=$(_get_file_block_size $SCRATCH_MNT) | 
|  | local failed=0 | 
|  |  | 
|  | echo -en "\n\n==> Testing blocksize $block_size " >> $seqres.full | 
|  | echo -en "file_len: $file_len offset: $offset " >> $seqres.full | 
|  | echo -e "len: $len sparse: $use_sparse_file" >> $seqres.full | 
|  |  | 
|  | if ((fs_block_size != block_size)); then | 
|  | _fail "Block size created ($block_size) doesn't match _get_file_block_size on mount ($fs_block_size)" | 
|  | fi | 
|  |  | 
|  | rm -f $SCRATCH_MNT/file | 
|  |  | 
|  | # This let's us also test against sparse files | 
|  | setup_zeroed_file $file_len $use_sparse_file | 
|  |  | 
|  | # This will overwrite the old data, the file size is the | 
|  | # delta between offset and len now. | 
|  | $XFS_IO_PROG -f -c "pwrite -S 0xaa -b 512 $offset $len" \ | 
|  | $test_file >> $seqres.full | 
|  |  | 
|  | sync | 
|  | new_filelen=$(_get_filesize $test_file) | 
|  | map_len=$(_round_up_to_page_boundary $new_filelen) | 
|  | csum_orig="$(_md5_checksum $test_file)" | 
|  |  | 
|  | # A couple of mmap() tests: | 
|  | # | 
|  | # We are allowed to mmap() up to the boundary of the page size of a | 
|  | # data object, but there a few rules to follow we must check for: | 
|  | # | 
|  | # a) zero-fill test for the data: POSIX says we should zero fill any | 
|  | #    partial page after the end of the object. Verify zero-fill. | 
|  | # b) do not write this bogus data to disk: on Linux, if we write data | 
|  | #    to a partially filled page, it will stay in the page cache even | 
|  | #    after the file is closed and unmapped even if it never reaches the | 
|  | #    file. As per mmap(2) subsequent mappings *may* see the modified | 
|  | #    content. This means that it also can get other data and we have | 
|  | #    no rules about what this data should be. Since the data read after | 
|  | #    the actual object data can vary this test just verifies that the | 
|  | #    filesize does not change. | 
|  | if [[ $map_len -gt $new_filelen ]]; then | 
|  | zero_filled_data_len=$((map_len - new_filelen)) | 
|  | _scratch_cycle_mount | 
|  | expected_zero_data="00" | 
|  | zero_filled_data=$($XFS_IO_PROG -r $test_file \ | 
|  | -c "mmap -r 0 $map_len" \ | 
|  | -c "mread -v $new_filelen $zero_filled_data_len" \ | 
|  | -c "munmap" | \ | 
|  | filter_xfs_io_data_unique) | 
|  | if [[ "$zero_filled_data" != "$expected_zero_data" ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "Expected data: $expected_zero_data" | 
|  | echo "  Actual data: $zero_filled_data" | 
|  | echo "Zero-fill expectations with mmap() not respected" | 
|  | fi | 
|  |  | 
|  | _scratch_cycle_mount | 
|  | $XFS_IO_PROG $test_file \ | 
|  | -c "mmap -w 0 $map_len" \ | 
|  | -c "mwrite $new_filelen $zero_filled_data_len" \ | 
|  | -c "munmap" | 
|  | sync | 
|  | csum_post="$(_md5_checksum $test_file)" | 
|  | if [[ "$csum_orig" != "$csum_post" ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "Expected csum: $csum_orig" | 
|  | echo " Actual  csum: $csum_post" | 
|  | echo "mmap() write up to page boundary should not change actual file contents" | 
|  | fi | 
|  |  | 
|  | local filelen_test=$(_get_filesize $test_file) | 
|  | if [[ "$filelen_test" != "$new_filelen" ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "Expected file length: $new_filelen" | 
|  | echo " Actual  file length: $filelen_test" | 
|  | echo "mmap() write up to page boundary should not change actual file size" | 
|  | fi | 
|  | fi | 
|  |  | 
|  | # Now lets ensure we get SIGBUS when we go beyond the page boundary | 
|  | _scratch_cycle_mount | 
|  | new_filelen=$(_get_filesize $test_file) | 
|  | map_len=$(_round_up_to_page_boundary $new_filelen) | 
|  | csum_orig="$(_md5_checksum $test_file)" | 
|  | _mread $test_file 0 $map_len >> $seqres.full  2>$tmp.err | 
|  | if grep -q 'Bus error' $tmp.err; then | 
|  | failed=1 | 
|  | cat $tmp.err | 
|  | echo "Not expecting SIGBUS when reading up to page boundary" | 
|  | fi | 
|  |  | 
|  | # This should just work | 
|  | _mread $test_file 0 $map_len >> $seqres.full  2>$tmp.err | 
|  | if [[ $? -ne 0 ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "mmap() read up to page boundary should work" | 
|  | fi | 
|  |  | 
|  | # This should just work | 
|  | mwrite $map_len 0 $map_len >> $seqres.full  2>$tmp.err | 
|  | if [[ $? -ne 0 ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "mmap() write up to page boundary should work" | 
|  | fi | 
|  |  | 
|  | # If we mmap() on the boundary but try to read beyond it just | 
|  | # fails, we don't get a SIGBUS | 
|  | $XFS_IO_PROG -r $test_file \ | 
|  | -c "mmap -r 0 $map_len" \ | 
|  | -c "mread 0 $((map_len + 10))" >> $seqres.full  2>$tmp.err | 
|  | local mread_err=$? | 
|  | if [[ $mread_err -eq 0 ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "mmap() to page boundary works as expected but reading beyond should fail: $mread_err" | 
|  | fi | 
|  |  | 
|  | $XFS_IO_PROG -w $test_file \ | 
|  | -c "mmap -w 0 $map_len" \ | 
|  | -c "mwrite 0 $((map_len + 10))" >> $seqres.full  2>$tmp.err | 
|  | local mwrite_err=$? | 
|  | if [[ $mwrite_err -eq 0 ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "mmap() to page boundary works as expected but writing beyond should fail: $mwrite_err" | 
|  | fi | 
|  |  | 
|  | # Now let's go beyond the allowed mmap() page boundary | 
|  | _mread $test_file 0 $((map_len + 10)) $((map_len + 10)) >> $seqres.full  2>$tmp.err | 
|  | if ! grep -q 'Bus error' $tmp.err; then | 
|  | let failed=$failed+1 | 
|  | echo "Expected SIGBUS when mmap() reading beyond page boundary" | 
|  | fi | 
|  |  | 
|  | mwrite $test_file 0 $((map_len + 10)) $((map_len + 10))  >> $seqres.full  2>$tmp.err | 
|  | if ! grep -q 'Bus error' $tmp.err; then | 
|  | let failed=$failed+1 | 
|  | echo "Expected SIGBUS when mmap() writing beyond page boundary" | 
|  | fi | 
|  |  | 
|  | local filelen_test=$(_get_filesize $test_file) | 
|  | if [[ "$filelen_test" != "$new_filelen" ]]; then | 
|  | let failed=$failed+1 | 
|  | echo "Expected file length: $new_filelen" | 
|  | echo " Actual  file length: $filelen_test" | 
|  | echo "reading or writing beyond file size up to mmap() page boundary should not change file size" | 
|  | fi | 
|  |  | 
|  | if [[ $failed -eq 1 ]]; then | 
|  | _fail "Test had $failed failures..." | 
|  | fi | 
|  | } | 
|  |  | 
|  | test_block_size() | 
|  | { | 
|  | local block_size=$1 | 
|  |  | 
|  | do_mmap_tests $block_size 512 3 5 | 
|  | do_mmap_tests $block_size 11k 0 $((4096 * 3 + 3)) | 
|  | do_mmap_tests $block_size 16k 0 $((16384+3)) | 
|  | do_mmap_tests $block_size 16k $((16384-10)) $((16384+20)) | 
|  | do_mmap_tests $block_size 64k 0 $((65536+3)) | 
|  | do_mmap_tests $block_size 4k 4090 30 true | 
|  | } | 
|  |  | 
|  | _scratch_mkfs >> $seqres.full 2>&1 || _fail "mkfs failed" | 
|  | _scratch_mount | 
|  | test_file=$SCRATCH_MNT/file | 
|  | block_size=$(_get_file_block_size "$SCRATCH_MNT") | 
|  | test_block_size $block_size | 
|  |  | 
|  | echo "Silence is golden" | 
|  | status=0 | 
|  | exit |