1#!/usr/bin/env bash 2# group: rw 3# 4# Test FUSE exports (in ways that are not captured by the generic 5# tests) 6# 7# Copyright (C) 2020 Red Hat, Inc. 8# 9# This program is free software; you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation; either version 2 of the License, or 12# (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program. If not, see <http://www.gnu.org/licenses/>. 21# 22 23seq=$(basename "$0") 24echo "QA output created by $seq" 25 26status=1 # failure is the default! 27 28_cleanup() 29{ 30 _cleanup_qemu 31 _cleanup_test_img 32 rmdir "$EXT_MP" 2>/dev/null 33 rm -f "$EXT_MP" 34 rm -f "$COPIED_IMG" 35} 36trap "_cleanup; exit \$status" 0 1 2 3 15 37 38# get standard environment, filters and checks 39. ./common.rc 40. ./common.filter 41. ./common.qemu 42 43# Generic format, but needs a plain filename 44_supported_fmt generic 45if [ "$IMGOPTSSYNTAX" = "true" ]; then 46 _unsupported_fmt $IMGFMT 47fi 48# We need the image to have exactly the specified size, and VPC does 49# not allow that by default 50_unsupported_fmt vpc 51 52_supported_proto file # We create the FUSE export manually 53_supported_os Linux # We need /dev/urandom 54_require_disk_usage 55 56# $1: Export ID 57# $2: Options (beyond the node-name and ID) 58# $3: Expected return value (defaults to 'return') 59# $4: Node to export (defaults to 'node-format') 60fuse_export_add() 61{ 62 # The grep -v is a filter for errors when /etc/fuse.conf does not contain 63 # user_allow_other. (The error is benign, but it is printed by fusermount 64 # on the first mount attempt, so our export code cannot hide it.) 65 _send_qemu_cmd $QEMU_HANDLE \ 66 "{'execute': 'block-export-add', 67 'arguments': { 68 'type': 'fuse', 69 'id': '$1', 70 'node-name': '${4:-node-format}', 71 $2 72 } }" \ 73 "${3:-return}" \ 74 | _filter_imgfmt \ 75 | grep -v 'option allow_other only allowed if' 76} 77 78# $1: Export ID 79fuse_export_del() 80{ 81 capture_events="BLOCK_EXPORT_DELETED" \ 82 _send_qemu_cmd $QEMU_HANDLE \ 83 "{'execute': 'block-export-del', 84 'arguments': { 85 'id': '$1' 86 } }" \ 87 'return' 88 89 _wait_event $QEMU_HANDLE \ 90 'BLOCK_EXPORT_DELETED' 91} 92 93# Return the length of the protocol file 94# $1: Protocol node export mount point 95# $2: Original file (to compare) 96get_proto_len() 97{ 98 len1=$(stat -c '%s' "$1") 99 len2=$(stat -c '%s' "$2") 100 101 if [ "$len1" != "$len2" ]; then 102 echo 'ERROR: Length of export and original differ:' >&2 103 echo "$len1 != $len2" >&2 104 else 105 echo '(OK: Lengths of export and original are the same)' >&2 106 fi 107 108 echo "$len1" 109} 110 111COPIED_IMG="$TEST_IMG.copy" 112EXT_MP="$TEST_IMG.fuse" 113 114echo '=== Set up ===' 115 116# Create image with random data 117_make_test_img 64M 118$QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io 119 120_launch_qemu 121_send_qemu_cmd $QEMU_HANDLE \ 122 "{'execute': 'qmp_capabilities'}" \ 123 'return' 124 125# Separate blockdev-add calls for format and protocol so we can remove 126# the format layer later on 127_send_qemu_cmd $QEMU_HANDLE \ 128 "{'execute': 'blockdev-add', 129 'arguments': { 130 'driver': 'file', 131 'node-name': 'node-protocol', 132 'filename': '$TEST_IMG' 133 } }" \ 134 'return' 135 136_send_qemu_cmd $QEMU_HANDLE \ 137 "{'execute': 'blockdev-add', 138 'arguments': { 139 'driver': '$IMGFMT', 140 'node-name': 'node-format', 141 'file': 'node-protocol' 142 } }" \ 143 'return' 144 145echo 146echo '=== Mountpoint not present ===' 147 148rmdir "$EXT_MP" 2>/dev/null 149rm -f "$EXT_MP" 150output=$(fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error) 151 152if echo "$output" | grep -q "Parameter 'type' does not accept value 'fuse'"; then 153 _notrun 'No FUSE support' 154fi 155 156echo "$output" 157 158echo 159echo '=== Mountpoint is a directory ===' 160 161mkdir "$EXT_MP" 162fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error 163rmdir "$EXT_MP" 164 165echo 166echo '=== Mountpoint is a regular file ===' 167 168touch "$EXT_MP" 169fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP'" 170 171# Check that the export presents the same data as the original image 172$QEMU_IMG compare -f raw -F $IMGFMT -U "$EXT_MP" "$TEST_IMG" 173 174# Some quick chmod tests 175stat -c 'Permissions pre-chmod: %a' "$EXT_MP" 176 177# Verify that we cannot set +w 178chmod u+w "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt 179stat -c 'Permissions post-+w: %a' "$EXT_MP" 180 181# But that we can set, say, +x (if we are so inclined) 182chmod u+x "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt 183stat -c 'Permissions post-+x: %a' "$EXT_MP" 184 185echo 186echo '=== Mount over existing file ===' 187 188# This is the coolest feature of FUSE exports: You can transparently 189# make images in any format appear as raw images 190fuse_export_add 'export-img' "'mountpoint': '$TEST_IMG'" 191 192# Accesses both exports at the same time, so we get a concurrency test 193$QEMU_IMG compare -f raw -F raw -U "$EXT_MP" "$TEST_IMG" 194 195# Just to be sure, we later want to compare the data offline. Also, 196# this allows us to see that cp works without complaining. 197# (This is not a given, because cp will expect a short read at EOF. 198# Internally, qemu does not allow short reads, so we have to check 199# whether the FUSE export driver lets them work.) 200cp "$TEST_IMG" "$COPIED_IMG" 201 202# $TEST_IMG will be in mode 0400 because it is read-only; we are going 203# to write to the copy, so make it writable 204chmod 0600 "$COPIED_IMG" 205 206echo 207echo '=== Double export ===' 208 209# We have already seen that exporting a node twice works fine, but you 210# cannot export anything twice on the same mount point. The reason is 211# that qemu has to stat the given mount point, and this would have to 212# be answered by the same qemu instance if it already has an export 213# there. However, it cannot answer the stat because it is itself 214# caught up in that same stat. 215fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error 216 217echo 218echo '=== Remove export ===' 219 220# Double-check that $EXT_MP appears as a non-empty file (the raw image) 221$QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1 222 223fuse_export_del 'export-mp' 224 225# See that the file appears empty again 226$QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1 227 228echo 229echo '=== Writable export ===' 230 231fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP', 'writable': true" 232 233# Check that writing to the read-only export fails 234output=$($QEMU_IO -f raw -c 'write -P 42 1M 64k' "$TEST_IMG" 2>&1 \ 235 | _filter_qemu_io | _filter_testdir | _filter_imgfmt) 236 237# Expected reference output: Opening the file fails because it has no 238# write permission 239reference="Could not open 'TEST_DIR/t.IMGFMT': Permission denied" 240 241if echo "$output" | grep -q "$reference"; then 242 echo "Writing to read-only export failed: OK" 243elif echo "$output" | grep -q "write failed: Permission denied"; then 244 # With CAP_DAC_OVERRIDE (e.g. when running this test as root), the export 245 # can be opened regardless of its file permissions, but writing will then 246 # fail. This is not the result for which we want to test, so count this as 247 # a SKIP. 248 _casenotrun "Opening RO export as R/W succeeded, perhaps because of" \ 249 "CAP_DAC_OVERRIDE" 250 251 # Still, write this to the reference output to make the test pass 252 echo "Writing to read-only export failed: OK" 253else 254 echo "Writing to read-only export failed: ERROR" 255 echo "$output" 256fi 257 258# But here it should work 259$QEMU_IO -f raw -c 'write -P 42 1M 64k' "$EXT_MP" | _filter_qemu_io 260 261# (Adjust the copy, too) 262$QEMU_IO -f raw -c 'write -P 42 1M 64k' "$COPIED_IMG" | _filter_qemu_io 263 264echo 265echo '=== Resizing exports ===' 266 267# Here, we need to export the protocol node -- the format layer may 268# not be growable, simply because the format does not support it. 269 270# Remove all exports and the format node first so permissions will not 271# get in the way 272fuse_export_del 'export-mp' 273fuse_export_del 'export-img' 274 275_send_qemu_cmd $QEMU_HANDLE \ 276 "{'execute': 'blockdev-del', 277 'arguments': { 278 'node-name': 'node-format' 279 } }" \ 280 'return' 281 282# Now export the protocol node 283fuse_export_add \ 284 'export-mp' \ 285 "'mountpoint': '$EXT_MP', 'writable': true" \ 286 'return' \ 287 'node-protocol' 288 289echo 290echo '--- Try growing non-growable export ---' 291 292# Get the current size so we can write beyond the EOF 293orig_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 294orig_disk_usage=$(disk_usage "$TEST_IMG") 295 296# Should fail (exports are non-growable by default) 297# (Note that qemu-io can never write beyond the EOF, so we have to use 298# dd here) 299dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$orig_len 2>&1 \ 300 | _filter_testdir | _filter_imgfmt 301 302echo 303echo '--- Resize export ---' 304 305# But we can truncate it explicitly; even with fallocate 306fallocate -o "$orig_len" -l 64k "$EXT_MP" 307 308new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 309if [ "$new_len" != "$((orig_len + 65536))" ]; then 310 echo 'ERROR: Unexpected post-truncate image size:' 311 echo "$new_len != $((orig_len + 65536))" 312else 313 echo 'OK: Post-truncate image size is as expected' 314fi 315 316new_disk_usage=$(disk_usage "$TEST_IMG") 317if [ "$new_disk_usage" -gt "$orig_disk_usage" ]; then 318 echo 'OK: Disk usage grew with fallocate' 319else 320 echo 'ERROR: Disk usage did not grow despite fallocate:' 321 echo "$orig_disk_usage => $new_disk_usage" 322fi 323 324echo 325echo '--- Try growing growable export ---' 326 327# Now export as growable 328fuse_export_del 'export-mp' 329fuse_export_add \ 330 'export-mp' \ 331 "'mountpoint': '$EXT_MP', 'writable': true, 'growable': true" \ 332 'return' \ 333 'node-protocol' 334 335# Now we should be able to write beyond the EOF 336dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$new_len 2>&1 \ 337 | _filter_testdir | _filter_imgfmt 338 339new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 340if [ "$new_len" != "$((orig_len + 131072))" ]; then 341 echo 'ERROR: Unexpected post-grow image size:' 342 echo "$new_len != $((orig_len + 131072))" 343else 344 echo 'OK: Post-grow image size is as expected' 345fi 346 347echo 348echo '--- Shrink export ---' 349 350# Now go back to the original size 351truncate -s "$orig_len" "$EXT_MP" 352 353new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 354if [ "$new_len" != "$orig_len" ]; then 355 echo 'ERROR: Unexpected post-truncate image size:' 356 echo "$new_len != $orig_len" 357else 358 echo 'OK: Post-truncate image size is as expected' 359fi 360 361echo 362echo '=== Tear down ===' 363 364_send_qemu_cmd $QEMU_HANDLE \ 365 "{'execute': 'quit'}" \ 366 'return' 367 368wait=yes _cleanup_qemu 369 370echo 371echo '=== Compare copy with original ===' 372 373$QEMU_IMG compare -f raw -F $IMGFMT "$COPIED_IMG" "$TEST_IMG" 374_cleanup_test_img 375 376echo 377echo '=== Writing zeroes while unmapping ===' 378# Regression test for https://gitlab.com/qemu-project/qemu/-/issues/1507 379_make_test_img 64M 380$QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io 381 382_launch_qemu 383_send_qemu_cmd $QEMU_HANDLE \ 384 "{'execute': 'qmp_capabilities'}" \ 385 'return' 386 387_send_qemu_cmd $QEMU_HANDLE \ 388 "{'execute': 'blockdev-add', 389 'arguments': { 390 'driver': '$IMGFMT', 391 'node-name': 'node-format', 392 'file': { 393 'driver': 'file', 394 'filename': '$TEST_IMG' 395 } 396 } }" \ 397 'return' 398 399fuse_export_add 'export' "'mountpoint': '$EXT_MP', 'writable': true" 400 401# Try writing zeroes by unmapping 402$QEMU_IO -f raw -c 'write -zu 0 64M' "$EXT_MP" | _filter_qemu_io 403 404# Check the result 405$QEMU_IO -f raw -c 'read -P 0 0 64M' "$EXT_MP" | _filter_qemu_io 406 407_send_qemu_cmd $QEMU_HANDLE \ 408 "{'execute': 'quit'}" \ 409 'return' 410 411wait=yes _cleanup_qemu 412 413# Check the original image 414$QEMU_IO -c 'read -P 0 0 64M' "$TEST_IMG" | _filter_qemu_io 415 416_cleanup_test_img 417 418# success, all done 419echo "*** done" 420rm -f $seq.full 421status=0 422