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