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