 27e0d8b508
			
		
	
	
		27e0d8b508
		
	
	
	
	
		
			
			Try writing zeroes to a FUSE export while allowing the area to be
unmapped; block/file-posix.c generally implements writing zeroes with
BDRV_REQ_MAY_UNMAP ('write -zu') by calling fallocate(PUNCH_HOLE).  This
used to lead to a blk_pdiscard() in the FUSE export, which may or may
not lead to the area being zeroed.  HEAD^ fixed this to use
blk_pwrite_zeroes() instead (again with BDRV_REQ_MAY_UNMAP), so verify
that running `qemu-io 'write -zu'` on a FUSE exports always results in
zeroes being written.
Signed-off-by: Hanna Czenczek <hreitz@redhat.com>
Message-Id: <20230227104725.33511-3-hreitz@redhat.com>
Reviewed-by: Kevin Wolf <kwolf@redhat.com>
Signed-off-by: Kevin Wolf <kwolf@redhat.com>
		
	
			
		
			
				
	
	
		
			421 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			421 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Bash
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env bash
 | |
| # group: rw
 | |
| #
 | |
| # Test FUSE exports (in ways that are not captured by the generic
 | |
| # tests)
 | |
| #
 | |
| # Copyright (C) 2020 Red Hat, Inc.
 | |
| #
 | |
| # This program is free software; you can redistribute it and/or modify
 | |
| # it under the terms of the GNU General Public License as published by
 | |
| # the Free Software Foundation; either version 2 of the License, or
 | |
| # (at your option) any later version.
 | |
| #
 | |
| # This program is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU General Public License for more details.
 | |
| #
 | |
| # You should have received a copy of the GNU General Public License
 | |
| # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
| #
 | |
| 
 | |
| seq=$(basename "$0")
 | |
| echo "QA output created by $seq"
 | |
| 
 | |
| status=1	# failure is the default!
 | |
| 
 | |
| _cleanup()
 | |
| {
 | |
|     _cleanup_qemu
 | |
|     _cleanup_test_img
 | |
|     rmdir "$EXT_MP" 2>/dev/null
 | |
|     rm -f "$EXT_MP"
 | |
|     rm -f "$COPIED_IMG"
 | |
| }
 | |
| trap "_cleanup; exit \$status" 0 1 2 3 15
 | |
| 
 | |
| # get standard environment, filters and checks
 | |
| . ./common.rc
 | |
| . ./common.filter
 | |
| . ./common.qemu
 | |
| 
 | |
| # Generic format, but needs a plain filename
 | |
| _supported_fmt generic
 | |
| if [ "$IMGOPTSSYNTAX" = "true" ]; then
 | |
|     _unsupported_fmt $IMGFMT
 | |
| fi
 | |
| # We need the image to have exactly the specified size, and VPC does
 | |
| # not allow that by default
 | |
| _unsupported_fmt vpc
 | |
| 
 | |
| _supported_proto file # We create the FUSE export manually
 | |
| _supported_os Linux # We need /dev/urandom
 | |
| 
 | |
| # $1: Export ID
 | |
| # $2: Options (beyond the node-name and ID)
 | |
| # $3: Expected return value (defaults to 'return')
 | |
| # $4: Node to export (defaults to 'node-format')
 | |
| fuse_export_add()
 | |
| {
 | |
|     # The grep -v is a filter for errors when /etc/fuse.conf does not contain
 | |
|     # user_allow_other.  (The error is benign, but it is printed by fusermount
 | |
|     # on the first mount attempt, so our export code cannot hide it.)
 | |
|     _send_qemu_cmd $QEMU_HANDLE \
 | |
|         "{'execute': 'block-export-add',
 | |
|           'arguments': {
 | |
|               'type': 'fuse',
 | |
|               'id': '$1',
 | |
|               'node-name': '${4:-node-format}',
 | |
|               $2
 | |
|           } }" \
 | |
|         "${3:-return}" \
 | |
|         | _filter_imgfmt \
 | |
|         | grep -v 'option allow_other only allowed if'
 | |
| }
 | |
| 
 | |
| # $1: Export ID
 | |
| fuse_export_del()
 | |
| {
 | |
|     _send_qemu_cmd $QEMU_HANDLE \
 | |
|         "{'execute': 'block-export-del',
 | |
|           'arguments': {
 | |
|               'id': '$1'
 | |
|           } }" \
 | |
|         'return'
 | |
| 
 | |
|     _send_qemu_cmd $QEMU_HANDLE \
 | |
|         '' \
 | |
|         'BLOCK_EXPORT_DELETED'
 | |
| }
 | |
| 
 | |
| # Return the length of the protocol file
 | |
| # $1: Protocol node export mount point
 | |
| # $2: Original file (to compare)
 | |
| get_proto_len()
 | |
| {
 | |
|     len1=$(stat -c '%s' "$1")
 | |
|     len2=$(stat -c '%s' "$2")
 | |
| 
 | |
|     if [ "$len1" != "$len2" ]; then
 | |
|         echo 'ERROR: Length of export and original differ:' >&2
 | |
|         echo "$len1 != $len2" >&2
 | |
|     else
 | |
|         echo '(OK: Lengths of export and original are the same)' >&2
 | |
|     fi
 | |
| 
 | |
|     echo "$len1"
 | |
| }
 | |
| 
 | |
| COPIED_IMG="$TEST_IMG.copy"
 | |
| EXT_MP="$TEST_IMG.fuse"
 | |
| 
 | |
| echo '=== Set up ==='
 | |
| 
 | |
| # Create image with random data
 | |
| _make_test_img 64M
 | |
| $QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io
 | |
| 
 | |
| _launch_qemu
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'qmp_capabilities'}" \
 | |
|     'return'
 | |
| 
 | |
| # Separate blockdev-add calls for format and protocol so we can remove
 | |
| # the format layer later on
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'blockdev-add',
 | |
|       'arguments': {
 | |
|           'driver': 'file',
 | |
|           'node-name': 'node-protocol',
 | |
|           'filename': '$TEST_IMG'
 | |
|       } }" \
 | |
|     'return'
 | |
| 
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'blockdev-add',
 | |
|       'arguments': {
 | |
|           'driver': '$IMGFMT',
 | |
|           'node-name': 'node-format',
 | |
|           'file': 'node-protocol'
 | |
|       } }" \
 | |
|     'return'
 | |
| 
 | |
| echo
 | |
| echo '=== Mountpoint not present ==='
 | |
| 
 | |
| rmdir "$EXT_MP" 2>/dev/null
 | |
| rm -f "$EXT_MP"
 | |
| output=$(fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error)
 | |
| 
 | |
| if echo "$output" | grep -q "Parameter 'type' does not accept value 'fuse'"; then
 | |
|     _notrun 'No FUSE support'
 | |
| fi
 | |
| 
 | |
| echo "$output"
 | |
| 
 | |
| echo
 | |
| echo '=== Mountpoint is a directory ==='
 | |
| 
 | |
| mkdir "$EXT_MP"
 | |
| fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error
 | |
| rmdir "$EXT_MP"
 | |
| 
 | |
| echo
 | |
| echo '=== Mountpoint is a regular file ==='
 | |
| 
 | |
| touch "$EXT_MP"
 | |
| fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP'"
 | |
| 
 | |
| # Check that the export presents the same data as the original image
 | |
| $QEMU_IMG compare -f raw -F $IMGFMT -U "$EXT_MP" "$TEST_IMG"
 | |
| 
 | |
| # Some quick chmod tests
 | |
| stat -c 'Permissions pre-chmod: %a' "$EXT_MP"
 | |
| 
 | |
| # Verify that we cannot set +w
 | |
| chmod u+w "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt
 | |
| stat -c 'Permissions post-+w: %a' "$EXT_MP"
 | |
| 
 | |
| # But that we can set, say, +x (if we are so inclined)
 | |
| chmod u+x "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt
 | |
| stat -c 'Permissions post-+x: %a' "$EXT_MP"
 | |
| 
 | |
| echo
 | |
| echo '=== Mount over existing file ==='
 | |
| 
 | |
| # This is the coolest feature of FUSE exports: You can transparently
 | |
| # make images in any format appear as raw images
 | |
| fuse_export_add 'export-img' "'mountpoint': '$TEST_IMG'"
 | |
| 
 | |
| # Accesses both exports at the same time, so we get a concurrency test
 | |
| $QEMU_IMG compare -f raw -F raw -U "$EXT_MP" "$TEST_IMG"
 | |
| 
 | |
| # Just to be sure, we later want to compare the data offline.  Also,
 | |
| # this allows us to see that cp works without complaining.
 | |
| # (This is not a given, because cp will expect a short read at EOF.
 | |
| # Internally, qemu does not allow short reads, so we have to check
 | |
| # whether the FUSE export driver lets them work.)
 | |
| cp "$TEST_IMG" "$COPIED_IMG"
 | |
| 
 | |
| # $TEST_IMG will be in mode 0400 because it is read-only; we are going
 | |
| # to write to the copy, so make it writable
 | |
| chmod 0600 "$COPIED_IMG"
 | |
| 
 | |
| echo
 | |
| echo '=== Double export ==='
 | |
| 
 | |
| # We have already seen that exporting a node twice works fine, but you
 | |
| # cannot export anything twice on the same mount point.  The reason is
 | |
| # that qemu has to stat the given mount point, and this would have to
 | |
| # be answered by the same qemu instance if it already has an export
 | |
| # there.  However, it cannot answer the stat because it is itself
 | |
| # caught up in that same stat.
 | |
| fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error
 | |
| 
 | |
| echo
 | |
| echo '=== Remove export ==='
 | |
| 
 | |
| # Double-check that $EXT_MP appears as a non-empty file (the raw image)
 | |
| $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1
 | |
| 
 | |
| fuse_export_del 'export-mp'
 | |
| 
 | |
| # See that the file appears empty again
 | |
| $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1
 | |
| 
 | |
| echo
 | |
| echo '=== Writable export ==='
 | |
| 
 | |
| fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP', 'writable': true"
 | |
| 
 | |
| # Check that writing to the read-only export fails
 | |
| output=$($QEMU_IO -f raw -c 'write -P 42 1M 64k' "$TEST_IMG" 2>&1 \
 | |
|              | _filter_qemu_io | _filter_testdir | _filter_imgfmt)
 | |
| 
 | |
| # Expected reference output: Opening the file fails because it has no
 | |
| # write permission
 | |
| reference="Could not open 'TEST_DIR/t.IMGFMT': Permission denied"
 | |
| 
 | |
| if echo "$output" | grep -q "$reference"; then
 | |
|     echo "Writing to read-only export failed: OK"
 | |
| elif echo "$output" | grep -q "write failed: Permission denied"; then
 | |
|     # With CAP_DAC_OVERRIDE (e.g. when running this test as root), the export
 | |
|     # can be opened regardless of its file permissions, but writing will then
 | |
|     # fail.  This is not the result for which we want to test, so count this as
 | |
|     # a SKIP.
 | |
|     _casenotrun "Opening RO export as R/W succeeded, perhaps because of" \
 | |
|         "CAP_DAC_OVERRIDE"
 | |
| 
 | |
|     # Still, write this to the reference output to make the test pass
 | |
|     echo "Writing to read-only export failed: OK"
 | |
| else
 | |
|     echo "Writing to read-only export failed: ERROR"
 | |
|     echo "$output"
 | |
| fi
 | |
| 
 | |
| # But here it should work
 | |
| $QEMU_IO -f raw -c 'write -P 42 1M 64k' "$EXT_MP" | _filter_qemu_io
 | |
| 
 | |
| # (Adjust the copy, too)
 | |
| $QEMU_IO -f raw -c 'write -P 42 1M 64k' "$COPIED_IMG" | _filter_qemu_io
 | |
| 
 | |
| echo
 | |
| echo '=== Resizing exports ==='
 | |
| 
 | |
| # Here, we need to export the protocol node -- the format layer may
 | |
| # not be growable, simply because the format does not support it.
 | |
| 
 | |
| # Remove all exports and the format node first so permissions will not
 | |
| # get in the way
 | |
| fuse_export_del 'export-mp'
 | |
| fuse_export_del 'export-img'
 | |
| 
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'blockdev-del',
 | |
|       'arguments': {
 | |
|           'node-name': 'node-format'
 | |
|       } }" \
 | |
|     'return'
 | |
| 
 | |
| # Now export the protocol node
 | |
| fuse_export_add \
 | |
|     'export-mp' \
 | |
|     "'mountpoint': '$EXT_MP', 'writable': true" \
 | |
|     'return' \
 | |
|     'node-protocol'
 | |
| 
 | |
| echo
 | |
| echo '--- Try growing non-growable export ---'
 | |
| 
 | |
| # Get the current size so we can write beyond the EOF
 | |
| orig_len=$(get_proto_len "$EXT_MP" "$TEST_IMG")
 | |
| orig_disk_usage=$(stat -c '%b' "$TEST_IMG")
 | |
| 
 | |
| # Should fail (exports are non-growable by default)
 | |
| # (Note that qemu-io can never write beyond the EOF, so we have to use
 | |
| # dd here)
 | |
| dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$orig_len 2>&1 \
 | |
|     | _filter_testdir | _filter_imgfmt
 | |
| 
 | |
| echo
 | |
| echo '--- Resize export ---'
 | |
| 
 | |
| # But we can truncate it explicitly; even with fallocate
 | |
| fallocate -o "$orig_len" -l 64k "$EXT_MP"
 | |
| 
 | |
| new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG")
 | |
| if [ "$new_len" != "$((orig_len + 65536))" ]; then
 | |
|     echo 'ERROR: Unexpected post-truncate image size:'
 | |
|     echo "$new_len != $((orig_len + 65536))"
 | |
| else
 | |
|     echo 'OK: Post-truncate image size is as expected'
 | |
| fi
 | |
| 
 | |
| new_disk_usage=$(stat -c '%b' "$TEST_IMG")
 | |
| if [ "$new_disk_usage" -gt "$orig_disk_usage" ]; then
 | |
|     echo 'OK: Disk usage grew with fallocate'
 | |
| else
 | |
|     echo 'ERROR: Disk usage did not grow despite fallocate:'
 | |
|     echo "$orig_disk_usage => $new_disk_usage"
 | |
| fi
 | |
| 
 | |
| echo
 | |
| echo '--- Try growing growable export ---'
 | |
| 
 | |
| # Now export as growable
 | |
| fuse_export_del 'export-mp'
 | |
| fuse_export_add \
 | |
|     'export-mp' \
 | |
|     "'mountpoint': '$EXT_MP', 'writable': true, 'growable': true" \
 | |
|     'return' \
 | |
|     'node-protocol'
 | |
| 
 | |
| # Now we should be able to write beyond the EOF
 | |
| dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$new_len 2>&1 \
 | |
|     | _filter_testdir | _filter_imgfmt
 | |
| 
 | |
| new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG")
 | |
| if [ "$new_len" != "$((orig_len + 131072))" ]; then
 | |
|     echo 'ERROR: Unexpected post-grow image size:'
 | |
|     echo "$new_len != $((orig_len + 131072))"
 | |
| else
 | |
|     echo 'OK: Post-grow image size is as expected'
 | |
| fi
 | |
| 
 | |
| echo
 | |
| echo '--- Shrink export ---'
 | |
| 
 | |
| # Now go back to the original size
 | |
| truncate -s "$orig_len" "$EXT_MP"
 | |
| 
 | |
| new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG")
 | |
| if [ "$new_len" != "$orig_len" ]; then
 | |
|     echo 'ERROR: Unexpected post-truncate image size:'
 | |
|     echo "$new_len != $orig_len"
 | |
| else
 | |
|     echo 'OK: Post-truncate image size is as expected'
 | |
| fi
 | |
| 
 | |
| echo
 | |
| echo '=== Tear down ==='
 | |
| 
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'quit'}" \
 | |
|     'return'
 | |
| 
 | |
| wait=yes _cleanup_qemu
 | |
| 
 | |
| echo
 | |
| echo '=== Compare copy with original ==='
 | |
| 
 | |
| $QEMU_IMG compare -f raw -F $IMGFMT "$COPIED_IMG" "$TEST_IMG"
 | |
| _cleanup_test_img
 | |
| 
 | |
| echo
 | |
| echo '=== Writing zeroes while unmapping ==='
 | |
| # Regression test for https://gitlab.com/qemu-project/qemu/-/issues/1507
 | |
| _make_test_img 64M
 | |
| $QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io
 | |
| 
 | |
| _launch_qemu
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'qmp_capabilities'}" \
 | |
|     'return'
 | |
| 
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'blockdev-add',
 | |
|       'arguments': {
 | |
|           'driver': '$IMGFMT',
 | |
|           'node-name': 'node-format',
 | |
|           'file': {
 | |
|               'driver': 'file',
 | |
|               'filename': '$TEST_IMG'
 | |
|           }
 | |
|       } }" \
 | |
|     'return'
 | |
| 
 | |
| fuse_export_add 'export' "'mountpoint': '$EXT_MP', 'writable': true"
 | |
| 
 | |
| # Try writing zeroes by unmapping
 | |
| $QEMU_IO -f raw -c 'write -zu 0 64M' "$EXT_MP" | _filter_qemu_io
 | |
| 
 | |
| # Check the result
 | |
| $QEMU_IO -f raw -c 'read -P 0 0 64M' "$EXT_MP" | _filter_qemu_io
 | |
| 
 | |
| _send_qemu_cmd $QEMU_HANDLE \
 | |
|     "{'execute': 'quit'}" \
 | |
|     'return'
 | |
| 
 | |
| wait=yes _cleanup_qemu
 | |
| 
 | |
| # Check the original image
 | |
| $QEMU_IO -c 'read -P 0 0 64M' "$TEST_IMG" | _filter_qemu_io
 | |
| 
 | |
| _cleanup_test_img
 | |
| 
 | |
| # success, all done
 | |
| echo "*** done"
 | |
| rm -f $seq.full
 | |
| status=0
 |