1#!/usr/bin/env bash 2# 3# Test case for qcow2's handling of extra data in snapshot table entries 4# (and more generally, how certain cases of broken snapshot tables are 5# handled) 6# 7# Copyright (C) 2019 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 23# creator 24owner=mreitz@redhat.com 25 26seq=$(basename $0) 27echo "QA output created by $seq" 28 29status=1 # failure is the default! 30 31_cleanup() 32{ 33 _cleanup_test_img 34 rm -f "$TEST_IMG".v{2,3}.orig 35 rm -f "$TEST_DIR"/sn{0,1,2}{,-pre,-extra,-post} 36} 37trap "_cleanup; exit \$status" 0 1 2 3 15 38 39# get standard environment, filters and checks 40. ./common.rc 41. ./common.filter 42 43# This tests qcow2-specific low-level functionality 44_supported_fmt qcow2 45_supported_proto file 46_supported_os Linux 47# (1) We create a v2 image that supports nothing but refcount_bits=16 48# (2) We do some refcount management on our own which expects 49# refcount_bits=16 50# As for data files, they do not support snapshots at all. 51_unsupported_imgopts 'refcount_bits=\([^1]\|.\([^6]\|$\)\)' data_file 52 53# Parameters: 54# $1: image filename 55# $2: snapshot table entry offset in the image 56snapshot_table_entry_size() 57{ 58 id_len=$(peek_file_be "$1" $(($2 + 12)) 2) 59 name_len=$(peek_file_be "$1" $(($2 + 14)) 2) 60 extra_len=$(peek_file_be "$1" $(($2 + 36)) 4) 61 62 full_len=$((40 + extra_len + id_len + name_len)) 63 echo $(((full_len + 7) / 8 * 8)) 64} 65 66# Parameter: 67# $1: image filename 68print_snapshot_table() 69{ 70 nb_entries=$(peek_file_be "$1" 60 4) 71 offset=$(peek_file_be "$1" 64 8) 72 73 echo "Snapshots in $1:" | _filter_testdir | _filter_imgfmt 74 75 for ((i = 0; i < nb_entries; i++)); do 76 id_len=$(peek_file_be "$1" $((offset + 12)) 2) 77 name_len=$(peek_file_be "$1" $((offset + 14)) 2) 78 extra_len=$(peek_file_be "$1" $((offset + 36)) 4) 79 80 extra_ofs=$((offset + 40)) 81 id_ofs=$((extra_ofs + extra_len)) 82 name_ofs=$((id_ofs + id_len)) 83 84 echo " [$i]" 85 echo " ID: $(peek_file_raw "$1" $id_ofs $id_len)" 86 echo " Name: $(peek_file_raw "$1" $name_ofs $name_len)" 87 echo " Extra data size: $extra_len" 88 if [ $extra_len -ge 8 ]; then 89 echo " VM state size: $(peek_file_be "$1" $extra_ofs 8)" 90 fi 91 if [ $extra_len -ge 16 ]; then 92 echo " Disk size: $(peek_file_be "$1" $((extra_ofs + 8)) 8)" 93 fi 94 if [ $extra_len -ge 24 ]; then 95 echo " Icount: $(peek_file_be "$1" $((extra_ofs + 16)) 8)" 96 fi 97 if [ $extra_len -gt 24 ]; then 98 echo ' Unknown extra data:' \ 99 "$(peek_file_raw "$1" $((extra_ofs + 16)) $((extra_len - 16)) \ 100 | tr -d '\0')" 101 fi 102 103 offset=$((offset + $(snapshot_table_entry_size "$1" $offset))) 104 done 105} 106 107# Mark clusters as allocated; works only in refblock 0 (i.e. before 108# cluster #32768). 109# Parameters: 110# $1: Start offset of what to allocate 111# $2: End offset (exclusive) 112refblock0_allocate() 113{ 114 reftable_ofs=$(peek_file_be "$TEST_IMG" 48 8) 115 refblock_ofs=$(peek_file_be "$TEST_IMG" $reftable_ofs 8) 116 117 cluster=$(($1 / 65536)) 118 ecluster=$((($2 + 65535) / 65536)) 119 120 while [ $cluster -lt $ecluster ]; do 121 if [ $cluster -ge 32768 ]; then 122 echo "*** Abort: Cluster $cluster exceeds refblock 0 ***" 123 exit 1 124 fi 125 poke_file "$TEST_IMG" $((refblock_ofs + cluster * 2)) '\x00\x01' 126 cluster=$((cluster + 1)) 127 done 128} 129 130 131echo 132echo '=== Create v2 template ===' 133echo 134 135# Create v2 image with a snapshot table with three entries: 136# [0]: No extra data (valid with v2, not valid with v3) 137# [1]: Has extra data unknown to qemu 138# [2]: Has the 64-bit VM state size, but not the disk size (again, 139# valid with v2, not valid with v3) 140 141TEST_IMG="$TEST_IMG.v2.orig" IMGOPTS='compat=0.10' _make_test_img 64M 142$QEMU_IMG snapshot -c sn0 "$TEST_IMG.v2.orig" 143$QEMU_IMG snapshot -c sn1 "$TEST_IMG.v2.orig" 144$QEMU_IMG snapshot -c sn2 "$TEST_IMG.v2.orig" 145 146# Copy out all existing snapshot table entries 147sn_table_ofs=$(peek_file_be "$TEST_IMG.v2.orig" 64 8) 148 149# ofs: Snapshot table entry offset 150# eds: Extra data size 151# ids: Name + ID size 152# len: Total entry length 153sn0_ofs=$sn_table_ofs 154sn0_eds=$(peek_file_be "$TEST_IMG.v2.orig" $((sn0_ofs + 36)) 4) 155sn0_ids=$(($(peek_file_be "$TEST_IMG.v2.orig" $((sn0_ofs + 12)) 2) + 156 $(peek_file_be "$TEST_IMG.v2.orig" $((sn0_ofs + 14)) 2))) 157sn0_len=$(snapshot_table_entry_size "$TEST_IMG.v2.orig" $sn0_ofs) 158sn1_ofs=$((sn0_ofs + sn0_len)) 159sn1_eds=$(peek_file_be "$TEST_IMG.v2.orig" $((sn1_ofs + 36)) 4) 160sn1_ids=$(($(peek_file_be "$TEST_IMG.v2.orig" $((sn1_ofs + 12)) 2) + 161 $(peek_file_be "$TEST_IMG.v2.orig" $((sn1_ofs + 14)) 2))) 162sn1_len=$(snapshot_table_entry_size "$TEST_IMG.v2.orig" $sn1_ofs) 163sn2_ofs=$((sn1_ofs + sn1_len)) 164sn2_eds=$(peek_file_be "$TEST_IMG.v2.orig" $((sn2_ofs + 36)) 4) 165sn2_ids=$(($(peek_file_be "$TEST_IMG.v2.orig" $((sn2_ofs + 12)) 2) + 166 $(peek_file_be "$TEST_IMG.v2.orig" $((sn2_ofs + 14)) 2))) 167sn2_len=$(snapshot_table_entry_size "$TEST_IMG.v2.orig" $sn2_ofs) 168 169# Data before extra data 170dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn0-pre" bs=1 skip=$sn0_ofs count=40 \ 171 &> /dev/null 172dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn1-pre" bs=1 skip=$sn1_ofs count=40 \ 173 &> /dev/null 174dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn2-pre" bs=1 skip=$sn2_ofs count=40 \ 175 &> /dev/null 176 177# Extra data 178dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn0-extra" bs=1 \ 179 skip=$((sn0_ofs + 40)) count=$sn0_eds &> /dev/null 180dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn1-extra" bs=1 \ 181 skip=$((sn1_ofs + 40)) count=$sn1_eds &> /dev/null 182dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn2-extra" bs=1 \ 183 skip=$((sn2_ofs + 40)) count=$sn2_eds &> /dev/null 184 185# Data after extra data 186dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn0-post" bs=1 \ 187 skip=$((sn0_ofs + 40 + sn0_eds)) count=$sn0_ids \ 188 &> /dev/null 189dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn1-post" bs=1 \ 190 skip=$((sn1_ofs + 40 + sn1_eds)) count=$sn1_ids \ 191 &> /dev/null 192dd if="$TEST_IMG.v2.orig" of="$TEST_DIR/sn2-post" bs=1 \ 193 skip=$((sn2_ofs + 40 + sn2_eds)) count=$sn2_ids \ 194 &> /dev/null 195 196# Amend them, one by one 197# Set sn0's extra data size to 0 198poke_file "$TEST_DIR/sn0-pre" 36 '\x00\x00\x00\x00' 199truncate -s 0 "$TEST_DIR/sn0-extra" 200# Grow sn0-post to pad 201truncate -s $(($(snapshot_table_entry_size "$TEST_DIR/sn0-pre") - 40)) \ 202 "$TEST_DIR/sn0-post" 203 204# Set sn1's extra data size to 50 205poke_file "$TEST_DIR/sn1-pre" 36 '\x00\x00\x00\x32' 206truncate -s 50 "$TEST_DIR/sn1-extra" 207poke_file "$TEST_DIR/sn1-extra" 24 'very important data' 208# Grow sn1-post to pad 209truncate -s $(($(snapshot_table_entry_size "$TEST_DIR/sn1-pre") - 90)) \ 210 "$TEST_DIR/sn1-post" 211 212# Set sn2's extra data size to 8 213poke_file "$TEST_DIR/sn2-pre" 36 '\x00\x00\x00\x08' 214truncate -s 8 "$TEST_DIR/sn2-extra" 215# Grow sn2-post to pad 216truncate -s $(($(snapshot_table_entry_size "$TEST_DIR/sn2-pre") - 48)) \ 217 "$TEST_DIR/sn2-post" 218 219# Construct snapshot table 220cat "$TEST_DIR"/sn0-{pre,extra,post} \ 221 "$TEST_DIR"/sn1-{pre,extra,post} \ 222 "$TEST_DIR"/sn2-{pre,extra,post} \ 223 | dd of="$TEST_IMG.v2.orig" bs=1 seek=$sn_table_ofs conv=notrunc \ 224 &> /dev/null 225 226# Done! 227TEST_IMG="$TEST_IMG.v2.orig" _check_test_img 228print_snapshot_table "$TEST_IMG.v2.orig" 229 230echo 231echo '=== Upgrade to v3 ===' 232echo 233 234cp "$TEST_IMG.v2.orig" "$TEST_IMG.v3.orig" 235$QEMU_IMG amend -o compat=1.1 "$TEST_IMG.v3.orig" 236TEST_IMG="$TEST_IMG.v3.orig" _check_test_img 237print_snapshot_table "$TEST_IMG.v3.orig" 238 239echo 240echo '=== Repair botched v3 ===' 241echo 242 243# Force the v2 file to be v3. v3 requires each snapshot table entry 244# to have at least 16 bytes of extra data, so it will not comply to 245# the qcow2 v3 specification; but we can fix that. 246cp "$TEST_IMG.v2.orig" "$TEST_IMG" 247 248# Set version 249poke_file "$TEST_IMG" 4 '\x00\x00\x00\x03' 250# Increase header length (necessary for v3) 251poke_file "$TEST_IMG" 100 '\x00\x00\x00\x68' 252# Set refcount order (necessary for v3) 253poke_file "$TEST_IMG" 96 '\x00\x00\x00\x04' 254 255_check_test_img -r all 256print_snapshot_table "$TEST_IMG" 257 258 259# From now on, just test the qcow2 version we are supposed to test. 260# (v3 by default, v2 by choice through $IMGOPTS.) 261# That works because we always write all known extra data when 262# updating the snapshot table, independent of the version. 263 264if echo "$IMGOPTS" | grep -q 'compat=\(0\.10\|v2\)' 2> /dev/null; then 265 subver=v2 266else 267 subver=v3 268fi 269 270echo 271echo '=== Add new snapshot ===' 272echo 273 274cp "$TEST_IMG.$subver.orig" "$TEST_IMG" 275$QEMU_IMG snapshot -c sn3 "$TEST_IMG" 276_check_test_img 277print_snapshot_table "$TEST_IMG" 278 279echo 280echo '=== Remove different snapshots ===' 281 282for sn in sn0 sn1 sn2; do 283 echo 284 echo "--- $sn ---" 285 286 cp "$TEST_IMG.$subver.orig" "$TEST_IMG" 287 $QEMU_IMG snapshot -d $sn "$TEST_IMG" 288 _check_test_img 289 print_snapshot_table "$TEST_IMG" 290done 291 292echo 293echo '=== Reject too much unknown extra data ===' 294echo 295 296cp "$TEST_IMG.$subver.orig" "$TEST_IMG" 297$QEMU_IMG snapshot -c sn3 "$TEST_IMG" 298 299sn_table_ofs=$(peek_file_be "$TEST_IMG" 64 8) 300sn0_ofs=$sn_table_ofs 301sn1_ofs=$((sn0_ofs + $(snapshot_table_entry_size "$TEST_IMG" $sn0_ofs))) 302sn2_ofs=$((sn1_ofs + $(snapshot_table_entry_size "$TEST_IMG" $sn1_ofs))) 303sn3_ofs=$((sn2_ofs + $(snapshot_table_entry_size "$TEST_IMG" $sn2_ofs))) 304 305# 64 kB of extra data should be rejected 306# (Note that this also induces a refcount error, because it spills 307# over to the next cluster. That's a good way to test that we can 308# handle simultaneous snapshot table and refcount errors.) 309poke_file "$TEST_IMG" $((sn3_ofs + 36)) '\x00\x01\x00\x00' 310 311# Print error 312_img_info 313echo 314_check_test_img 315echo 316 317# Should be repairable 318_check_test_img -r all 319 320echo 321echo '=== Snapshot table too big ===' 322echo 323 324sn_table_ofs=$(peek_file_be "$TEST_IMG.v3.orig" 64 8) 325 326# Fill a snapshot with 1 kB of extra data, a 65535-char ID, and a 327# 65535-char name, and repeat it as many times as necessary to fill 328# 64 MB (the maximum supported by qemu) 329 330touch "$TEST_DIR/sn0" 331 332# Full size (fixed + extra + ID + name + padding) 333sn_size=$((40 + 1024 + 65535 + 65535 + 2)) 334 335# We only need the fixed part, though. 336truncate -s 40 "$TEST_DIR/sn0" 337 338# 65535-char ID string 339poke_file "$TEST_DIR/sn0" 12 '\xff\xff' 340# 65535-char name 341poke_file "$TEST_DIR/sn0" 14 '\xff\xff' 342# 1 kB of extra data 343poke_file "$TEST_DIR/sn0" 36 '\x00\x00\x04\x00' 344 345# Create test image 346_make_test_img 64M 347 348# Hook up snapshot table somewhere safe (at 1 MB) 349poke_file "$TEST_IMG" 64 '\x00\x00\x00\x00\x00\x10\x00\x00' 350 351offset=1048576 352size_written=0 353sn_count=0 354while [ $size_written -le $((64 * 1048576)) ]; do 355 dd if="$TEST_DIR/sn0" of="$TEST_IMG" bs=1 seek=$offset conv=notrunc \ 356 &> /dev/null 357 offset=$((offset + sn_size)) 358 size_written=$((size_written + sn_size)) 359 sn_count=$((sn_count + 1)) 360done 361truncate -s "$offset" "$TEST_IMG" 362 363# Give the last snapshot (the one to be removed) an L1 table so we can 364# see how that is handled when repairing the image 365# (Put it two clusters before 1 MB, and one L2 table one cluster 366# before 1 MB) 367poke_file "$TEST_IMG" $((offset - sn_size + 0)) \ 368 '\x00\x00\x00\x00\x00\x0e\x00\x00' 369poke_file "$TEST_IMG" $((offset - sn_size + 8)) \ 370 '\x00\x00\x00\x01' 371 372# Hook up the L2 table 373poke_file "$TEST_IMG" $((1048576 - 2 * 65536)) \ 374 '\x80\x00\x00\x00\x00\x0f\x00\x00' 375 376# Make sure all of the clusters we just hooked up are allocated: 377# - The snapshot table 378# - The last snapshot's L1 and L2 table 379refblock0_allocate $((1048576 - 2 * 65536)) $offset 380 381poke_file "$TEST_IMG" 60 \ 382 "$(printf '%08x' $sn_count | sed -e 's/\(..\)/\\x\1/g')" 383 384# Print error 385_img_info 386echo 387_check_test_img 388echo 389 390# Should be repairable 391_check_test_img -r all 392 393echo 394echo "$((sn_count - 1)) snapshots should remain:" 395echo " qemu-img info reports $(_img_info | grep -c '^ \{32\}') snapshots" 396echo " Image header reports $(peek_file_be "$TEST_IMG" 60 4) snapshots" 397 398echo 399echo '=== Snapshot table too big with one entry with too much extra data ===' 400echo 401 402# For this test, we reuse the image from the previous case, which has 403# a snapshot table that is right at the limit. 404# Our layout looks like this: 405# - (a number of snapshot table entries) 406# - One snapshot with $extra_data_size extra data 407# - One normal snapshot that breaks the 64 MB boundary 408# - One normal snapshot beyond the 64 MB boundary 409# 410# $extra_data_size is calculated so that simply by virtue of it 411# decreasing to 1 kB, the penultimate snapshot will fit into 64 MB 412# limit again. The final snapshot will always be beyond the limit, so 413# that we can see that the repair algorithm does still determine the 414# limit to be somewhere, even when truncating one snapshot's extra 415# data. 416 417# The last case has removed the last snapshot, so calculate 418# $old_offset to get the current image's real length 419old_offset=$((offset - sn_size)) 420 421# The layout from the previous test had one snapshot beyond the 64 MB 422# limit; we want the same (after the oversized extra data has been 423# truncated to 1 kB), so we drop the last three snapshots and 424# construct them from scratch. 425offset=$((offset - 3 * sn_size)) 426sn_count=$((sn_count - 3)) 427 428# Assuming we had already written one of the three snapshots 429# (necessary so we can calculate $extra_data_size next). 430size_written=$((size_written - 2 * sn_size)) 431 432# Increase the extra data size so we go past the limit 433# (The -1024 comes from the 1 kB of extra data we already have) 434extra_data_size=$((64 * 1048576 + 8 - sn_size - (size_written - 1024))) 435 436poke_file "$TEST_IMG" $((offset + 36)) \ 437 "$(printf '%08x' $extra_data_size | sed -e 's/\(..\)/\\x\1/g')" 438 439offset=$((offset + sn_size - 1024 + extra_data_size)) 440size_written=$((size_written - 1024 + extra_data_size)) 441sn_count=$((sn_count + 1)) 442 443# Write the two normal snapshots 444for ((i = 0; i < 2; i++)); do 445 dd if="$TEST_DIR/sn0" of="$TEST_IMG" bs=1 seek=$offset conv=notrunc \ 446 &> /dev/null 447 offset=$((offset + sn_size)) 448 size_written=$((size_written + sn_size)) 449 sn_count=$((sn_count + 1)) 450 451 if [ $i = 0 ]; then 452 # Check that the penultimate snapshot is beyond the 64 MB limit 453 echo "Snapshot table size should equal $((64 * 1048576 + 8)):" \ 454 $size_written 455 echo 456 fi 457done 458 459truncate -s $offset "$TEST_IMG" 460refblock0_allocate $old_offset $offset 461 462poke_file "$TEST_IMG" 60 \ 463 "$(printf '%08x' $sn_count | sed -e 's/\(..\)/\\x\1/g')" 464 465# Print error 466_img_info 467echo 468_check_test_img 469echo 470 471# Just truncating the extra data should be sufficient to shorten the 472# snapshot table so only one snapshot exceeds the extra size 473_check_test_img -r all 474 475echo 476echo '=== Too many snapshots ===' 477echo 478 479# Create a v2 image, for speeds' sake: All-zero snapshot table entries 480# are only valid in v2. 481IMGOPTS='compat=0.10' _make_test_img 64M 482 483# Hook up snapshot table somewhere safe (at 1 MB) 484poke_file "$TEST_IMG" 64 '\x00\x00\x00\x00\x00\x10\x00\x00' 485# "Create" more than 65536 snapshots (twice that many here) 486poke_file "$TEST_IMG" 60 '\x00\x02\x00\x00' 487 488# 40-byte all-zero snapshot table entries are valid snapshots, but 489# only in v2 (v3 needs 16 bytes of extra data, so we would have to 490# write 131072x '\x10'). 491truncate -s $((1048576 + 40 * 131072)) "$TEST_IMG" 492 493# But let us give one of the snapshots to be removed an L1 table so 494# we can see how that is handled when repairing the image. 495# (Put it two clusters before 1 MB, and one L2 table one cluster 496# before 1 MB) 497poke_file "$TEST_IMG" $((1048576 + 40 * 65536 + 0)) \ 498 '\x00\x00\x00\x00\x00\x0e\x00\x00' 499poke_file "$TEST_IMG" $((1048576 + 40 * 65536 + 8)) \ 500 '\x00\x00\x00\x01' 501 502# Hook up the L2 table 503poke_file "$TEST_IMG" $((1048576 - 2 * 65536)) \ 504 '\x80\x00\x00\x00\x00\x0f\x00\x00' 505 506# Make sure all of the clusters we just hooked up are allocated: 507# - The snapshot table 508# - The last snapshot's L1 and L2 table 509refblock0_allocate $((1048576 - 2 * 65536)) $((1048576 + 40 * 131072)) 510 511# Print error 512_img_info 513echo 514_check_test_img 515echo 516 517# Should be repairable 518_check_test_img -r all 519 520echo 521echo '65536 snapshots should remain:' 522echo " qemu-img info reports $(_img_info | grep -c '^ \{32\}') snapshots" 523echo " Image header reports $(peek_file_be "$TEST_IMG" 60 4) snapshots" 524 525# success, all done 526echo "*** done" 527status=0 528