1e1232323SMaria Kustova# Generator of fuzzed qcow2 images
2e1232323SMaria Kustova#
3e1232323SMaria Kustova# Copyright (C) 2014 Maria Kustova <maria.k@catit.be>
4e1232323SMaria Kustova#
5e1232323SMaria Kustova# This program is free software: you can redistribute it and/or modify
6e1232323SMaria Kustova# it under the terms of the GNU General Public License as published by
7e1232323SMaria Kustova# the Free Software Foundation, either version 2 of the License, or
8e1232323SMaria Kustova# (at your option) any later version.
9e1232323SMaria Kustova#
10e1232323SMaria Kustova# This program is distributed in the hope that it will be useful,
11e1232323SMaria Kustova# but WITHOUT ANY WARRANTY; without even the implied warranty of
12e1232323SMaria Kustova# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13e1232323SMaria Kustova# GNU General Public License for more details.
14e1232323SMaria Kustova#
15e1232323SMaria Kustova# You should have received a copy of the GNU General Public License
16e1232323SMaria Kustova# along with this program.  If not, see <http://www.gnu.org/licenses/>.
17e1232323SMaria Kustova#
18e1232323SMaria Kustova
19e1232323SMaria Kustovaimport random
20e1232323SMaria Kustovaimport struct
21068cf7a4SEduardo Habkostfrom . import fuzz
2238eb193bSMaria Kustovafrom math import ceil
2338eb193bSMaria Kustovafrom os import urandom
2494c83a24SMaria Kustovafrom itertools import chain
25e1232323SMaria Kustova
26e1232323SMaria KustovaMAX_IMAGE_SIZE = 10 * (1 << 20)
27e1232323SMaria Kustova# Standard sizes
28e1232323SMaria KustovaUINT32_S = 4
29e1232323SMaria KustovaUINT64_S = 8
30e1232323SMaria Kustova
31e1232323SMaria Kustova
32e1232323SMaria Kustovaclass Field(object):
33e1232323SMaria Kustova
34e1232323SMaria Kustova    """Atomic image element (field).
35e1232323SMaria Kustova
36e1232323SMaria Kustova    The class represents an image field as quadruple of a data format
37e1232323SMaria Kustova    of value necessary for its packing to binary form, an offset from
38e1232323SMaria Kustova    the beginning of the image, a value and a name.
39e1232323SMaria Kustova
4094c83a24SMaria Kustova    The field can be iterated as a list [format, offset, value, name].
41e1232323SMaria Kustova    """
42e1232323SMaria Kustova
43e1232323SMaria Kustova    __slots__ = ('fmt', 'offset', 'value', 'name')
44e1232323SMaria Kustova
45e1232323SMaria Kustova    def __init__(self, fmt, offset, val, name):
46e1232323SMaria Kustova        self.fmt = fmt
47e1232323SMaria Kustova        self.offset = offset
48e1232323SMaria Kustova        self.value = val
49e1232323SMaria Kustova        self.name = name
50e1232323SMaria Kustova
51e1232323SMaria Kustova    def __iter__(self):
5294c83a24SMaria Kustova        return iter([self.fmt, self.offset, self.value, self.name])
53e1232323SMaria Kustova
54e1232323SMaria Kustova    def __repr__(self):
55c439143bSEduardo Habkost        return "Field(fmt=%r, offset=%r, value=%r, name=%r)" % \
56c439143bSEduardo Habkost            (self.fmt, self.offset, self.value, self.name)
57e1232323SMaria Kustova
58e1232323SMaria Kustova
59e1232323SMaria Kustovaclass FieldsList(object):
60e1232323SMaria Kustova
61e1232323SMaria Kustova    """List of fields.
62e1232323SMaria Kustova
6394c83a24SMaria Kustova    The class allows access to a field in the list by its name.
64e1232323SMaria Kustova    """
65e1232323SMaria Kustova
66e1232323SMaria Kustova    def __init__(self, meta_data=None):
67e1232323SMaria Kustova        if meta_data is None:
68e1232323SMaria Kustova            self.data = []
69e1232323SMaria Kustova        else:
7094c83a24SMaria Kustova            self.data = [Field(*f)
71e1232323SMaria Kustova                         for f in meta_data]
72e1232323SMaria Kustova
73e1232323SMaria Kustova    def __getitem__(self, name):
74e1232323SMaria Kustova        return [x for x in self.data if x.name == name]
75e1232323SMaria Kustova
76e1232323SMaria Kustova    def __iter__(self):
77e1232323SMaria Kustova        return iter(self.data)
78e1232323SMaria Kustova
79e1232323SMaria Kustova    def __len__(self):
80e1232323SMaria Kustova        return len(self.data)
81e1232323SMaria Kustova
82e1232323SMaria Kustova
83e1232323SMaria Kustovaclass Image(object):
84e1232323SMaria Kustova
85e1232323SMaria Kustova    """ Qcow2 image object.
86e1232323SMaria Kustova
87e1232323SMaria Kustova    This class allows to create qcow2 images with random valid structures and
88e1232323SMaria Kustova    values, fuzz them via external qcow2.fuzz module and write the result to
89e1232323SMaria Kustova    a file.
90e1232323SMaria Kustova    """
91e1232323SMaria Kustova
9294c83a24SMaria Kustova    def __init__(self, backing_file_name=None):
9394c83a24SMaria Kustova        """Create a random valid qcow2 image with the correct header and stored
9494c83a24SMaria Kustova        backing file name.
9594c83a24SMaria Kustova        """
9694c83a24SMaria Kustova        cluster_bits, self.image_size = self._size_params()
9794c83a24SMaria Kustova        self.cluster_size = 1 << cluster_bits
9894c83a24SMaria Kustova        self.header = FieldsList()
9994c83a24SMaria Kustova        self.backing_file_name = FieldsList()
10094c83a24SMaria Kustova        self.backing_file_format = FieldsList()
10194c83a24SMaria Kustova        self.feature_name_table = FieldsList()
10294c83a24SMaria Kustova        self.end_of_extension_area = FieldsList()
10394c83a24SMaria Kustova        self.l2_tables = FieldsList()
10494c83a24SMaria Kustova        self.l1_table = FieldsList()
1051e8fd8d4SMaria Kustova        self.refcount_table = FieldsList()
1061e8fd8d4SMaria Kustova        self.refcount_blocks = FieldsList()
10794c83a24SMaria Kustova        self.ext_offset = 0
10894c83a24SMaria Kustova        self.create_header(cluster_bits, backing_file_name)
10994c83a24SMaria Kustova        self.set_backing_file_name(backing_file_name)
11094c83a24SMaria Kustova        self.data_clusters = self._alloc_data(self.image_size,
11194c83a24SMaria Kustova                                              self.cluster_size)
11294c83a24SMaria Kustova        # Percentage of fields will be fuzzed
11394c83a24SMaria Kustova        self.bias = random.uniform(0.2, 0.5)
11494c83a24SMaria Kustova
11594c83a24SMaria Kustova    def __iter__(self):
11694c83a24SMaria Kustova        return chain(self.header, self.backing_file_format,
11794c83a24SMaria Kustova                     self.feature_name_table, self.end_of_extension_area,
1181e8fd8d4SMaria Kustova                     self.backing_file_name, self.l1_table, self.l2_tables,
1191e8fd8d4SMaria Kustova                     self.refcount_table, self.refcount_blocks)
12094c83a24SMaria Kustova
12194c83a24SMaria Kustova    def create_header(self, cluster_bits, backing_file_name=None):
12294c83a24SMaria Kustova        """Generate a random valid header."""
12394c83a24SMaria Kustova        meta_header = [
124ee1fde71SEduardo Habkost            ['>4s', 0, b"QFI\xfb", 'magic'],
12594c83a24SMaria Kustova            ['>I', 4, random.randint(2, 3), 'version'],
12694c83a24SMaria Kustova            ['>Q', 8, 0, 'backing_file_offset'],
12794c83a24SMaria Kustova            ['>I', 16, 0, 'backing_file_size'],
12894c83a24SMaria Kustova            ['>I', 20, cluster_bits, 'cluster_bits'],
12994c83a24SMaria Kustova            ['>Q', 24, self.image_size, 'size'],
13094c83a24SMaria Kustova            ['>I', 32, 0, 'crypt_method'],
13194c83a24SMaria Kustova            ['>I', 36, 0, 'l1_size'],
13294c83a24SMaria Kustova            ['>Q', 40, 0, 'l1_table_offset'],
13394c83a24SMaria Kustova            ['>Q', 48, 0, 'refcount_table_offset'],
13494c83a24SMaria Kustova            ['>I', 56, 0, 'refcount_table_clusters'],
13594c83a24SMaria Kustova            ['>I', 60, 0, 'nb_snapshots'],
13694c83a24SMaria Kustova            ['>Q', 64, 0, 'snapshots_offset'],
13794c83a24SMaria Kustova            ['>Q', 72, 0, 'incompatible_features'],
13894c83a24SMaria Kustova            ['>Q', 80, 0, 'compatible_features'],
13994c83a24SMaria Kustova            ['>Q', 88, 0, 'autoclear_features'],
14094c83a24SMaria Kustova            # Only refcount_order = 4 is supported by current (07.2014)
14194c83a24SMaria Kustova            # implementation of QEMU
14294c83a24SMaria Kustova            ['>I', 96, 4, 'refcount_order'],
14394c83a24SMaria Kustova            ['>I', 100, 0, 'header_length']
14494c83a24SMaria Kustova        ]
14594c83a24SMaria Kustova        self.header = FieldsList(meta_header)
14694c83a24SMaria Kustova
14794c83a24SMaria Kustova        if self.header['version'][0].value == 2:
14894c83a24SMaria Kustova            self.header['header_length'][0].value = 72
14994c83a24SMaria Kustova        else:
15094c83a24SMaria Kustova            self.header['incompatible_features'][0].value = \
15194c83a24SMaria Kustova                                                        random.getrandbits(2)
15294c83a24SMaria Kustova            self.header['compatible_features'][0].value = random.getrandbits(1)
15394c83a24SMaria Kustova            self.header['header_length'][0].value = 104
15494c83a24SMaria Kustova        # Extensions start at the header last field offset and the field size
15594c83a24SMaria Kustova        self.ext_offset = struct.calcsize(
15694c83a24SMaria Kustova            self.header['header_length'][0].fmt) + \
15794c83a24SMaria Kustova            self.header['header_length'][0].offset
15894c83a24SMaria Kustova        end_of_extension_area_len = 2 * UINT32_S
15994c83a24SMaria Kustova        free_space = self.cluster_size - self.ext_offset - \
16094c83a24SMaria Kustova                     end_of_extension_area_len
16194c83a24SMaria Kustova        # If the backing file name specified and there is enough space for it
16294c83a24SMaria Kustova        # in the first cluster, then it's placed in the very end of the first
16394c83a24SMaria Kustova        # cluster.
16494c83a24SMaria Kustova        if (backing_file_name is not None) and \
16594c83a24SMaria Kustova           (free_space >= len(backing_file_name)):
16694c83a24SMaria Kustova            self.header['backing_file_size'][0].value = len(backing_file_name)
16794c83a24SMaria Kustova            self.header['backing_file_offset'][0].value = \
16894c83a24SMaria Kustova                                    self.cluster_size - len(backing_file_name)
16994c83a24SMaria Kustova
17094c83a24SMaria Kustova    def set_backing_file_name(self, backing_file_name=None):
17194c83a24SMaria Kustova        """Add the name of the backing file at the offset specified
17294c83a24SMaria Kustova        in the header.
17394c83a24SMaria Kustova        """
17494c83a24SMaria Kustova        if (backing_file_name is not None) and \
17594c83a24SMaria Kustova           (not self.header['backing_file_offset'][0].value == 0):
17694c83a24SMaria Kustova            data_len = len(backing_file_name)
17794c83a24SMaria Kustova            data_fmt = '>' + str(data_len) + 's'
17894c83a24SMaria Kustova            self.backing_file_name = FieldsList([
17994c83a24SMaria Kustova                [data_fmt, self.header['backing_file_offset'][0].value,
18094c83a24SMaria Kustova                 backing_file_name, 'bf_name']
18194c83a24SMaria Kustova            ])
18294c83a24SMaria Kustova
18394c83a24SMaria Kustova    def set_backing_file_format(self, backing_file_fmt=None):
18494c83a24SMaria Kustova        """Generate the header extension for the backing file format."""
18594c83a24SMaria Kustova        if backing_file_fmt is not None:
18694c83a24SMaria Kustova            # Calculation of the free space available in the first cluster
18794c83a24SMaria Kustova            end_of_extension_area_len = 2 * UINT32_S
18894c83a24SMaria Kustova            high_border = (self.header['backing_file_offset'][0].value or
18994c83a24SMaria Kustova                           (self.cluster_size - 1)) - \
19094c83a24SMaria Kustova                end_of_extension_area_len
19194c83a24SMaria Kustova            free_space = high_border - self.ext_offset
19294c83a24SMaria Kustova            ext_size = 2 * UINT32_S + ((len(backing_file_fmt) + 7) & ~7)
19394c83a24SMaria Kustova
19494c83a24SMaria Kustova            if free_space >= ext_size:
19594c83a24SMaria Kustova                ext_data_len = len(backing_file_fmt)
19694c83a24SMaria Kustova                ext_data_fmt = '>' + str(ext_data_len) + 's'
19794c83a24SMaria Kustova                ext_padding_len = 7 - (ext_data_len - 1) % 8
19894c83a24SMaria Kustova                self.backing_file_format = FieldsList([
19994c83a24SMaria Kustova                    ['>I', self.ext_offset, 0xE2792ACA, 'ext_magic'],
20094c83a24SMaria Kustova                    ['>I', self.ext_offset + UINT32_S, ext_data_len,
20194c83a24SMaria Kustova                     'ext_length'],
20294c83a24SMaria Kustova                    [ext_data_fmt, self.ext_offset + UINT32_S * 2,
20394c83a24SMaria Kustova                     backing_file_fmt, 'bf_format']
20494c83a24SMaria Kustova                ])
20594c83a24SMaria Kustova                self.ext_offset = \
20694c83a24SMaria Kustova                        struct.calcsize(
20794c83a24SMaria Kustova                            self.backing_file_format['bf_format'][0].fmt) + \
20894c83a24SMaria Kustova                        ext_padding_len + \
20994c83a24SMaria Kustova                        self.backing_file_format['bf_format'][0].offset
21094c83a24SMaria Kustova
21194c83a24SMaria Kustova    def create_feature_name_table(self):
21294c83a24SMaria Kustova        """Generate a random header extension for names of features used in
21394c83a24SMaria Kustova        the image.
21494c83a24SMaria Kustova        """
21594c83a24SMaria Kustova        def gen_feat_ids():
21694c83a24SMaria Kustova            """Return random feature type and feature bit."""
21794c83a24SMaria Kustova            return (random.randint(0, 2), random.randint(0, 63))
21894c83a24SMaria Kustova
21994c83a24SMaria Kustova        end_of_extension_area_len = 2 * UINT32_S
22094c83a24SMaria Kustova        high_border = (self.header['backing_file_offset'][0].value or
22194c83a24SMaria Kustova                       (self.cluster_size - 1)) - \
22294c83a24SMaria Kustova            end_of_extension_area_len
22394c83a24SMaria Kustova        free_space = high_border - self.ext_offset
22494c83a24SMaria Kustova        # Sum of sizes of 'magic' and 'length' header extension fields
22594c83a24SMaria Kustova        ext_header_len = 2 * UINT32_S
22694c83a24SMaria Kustova        fnt_entry_size = 6 * UINT64_S
22794c83a24SMaria Kustova        num_fnt_entries = min(10, (free_space - ext_header_len) /
22894c83a24SMaria Kustova                              fnt_entry_size)
22994c83a24SMaria Kustova        if not num_fnt_entries == 0:
23094c83a24SMaria Kustova            feature_tables = []
23194c83a24SMaria Kustova            feature_ids = []
23294c83a24SMaria Kustova            inner_offset = self.ext_offset + ext_header_len
233ee1fde71SEduardo Habkost            feat_name = b'some cool feature'
23494c83a24SMaria Kustova            while len(feature_tables) < num_fnt_entries * 3:
23594c83a24SMaria Kustova                feat_type, feat_bit = gen_feat_ids()
23694c83a24SMaria Kustova                # Remove duplicates
23794c83a24SMaria Kustova                while (feat_type, feat_bit) in feature_ids:
23894c83a24SMaria Kustova                    feat_type, feat_bit = gen_feat_ids()
23994c83a24SMaria Kustova                feature_ids.append((feat_type, feat_bit))
24094c83a24SMaria Kustova                feat_fmt = '>' + str(len(feat_name)) + 's'
24194c83a24SMaria Kustova                feature_tables += [['B', inner_offset,
24294c83a24SMaria Kustova                                    feat_type, 'feature_type'],
24394c83a24SMaria Kustova                                   ['B', inner_offset + 1, feat_bit,
24494c83a24SMaria Kustova                                    'feature_bit_number'],
24594c83a24SMaria Kustova                                   [feat_fmt, inner_offset + 2,
24694c83a24SMaria Kustova                                    feat_name, 'feature_name']
24794c83a24SMaria Kustova                ]
24894c83a24SMaria Kustova                inner_offset += fnt_entry_size
24994c83a24SMaria Kustova            # No padding for the extension is necessary, because
25094c83a24SMaria Kustova            # the extension length is multiple of 8
25194c83a24SMaria Kustova            self.feature_name_table = FieldsList([
25294c83a24SMaria Kustova                ['>I', self.ext_offset, 0x6803f857, 'ext_magic'],
25394c83a24SMaria Kustova                # One feature table contains 3 fields and takes 48 bytes
25494c83a24SMaria Kustova                ['>I', self.ext_offset + UINT32_S,
255d974451cSEduardo Habkost                 len(feature_tables) // 3 * 48, 'ext_length']
25694c83a24SMaria Kustova            ] + feature_tables)
25794c83a24SMaria Kustova            self.ext_offset = inner_offset
25894c83a24SMaria Kustova
25994c83a24SMaria Kustova    def set_end_of_extension_area(self):
26094c83a24SMaria Kustova        """Generate a mandatory header extension marking end of header
26194c83a24SMaria Kustova        extensions.
26294c83a24SMaria Kustova        """
26394c83a24SMaria Kustova        self.end_of_extension_area = FieldsList([
26494c83a24SMaria Kustova            ['>I', self.ext_offset, 0, 'ext_magic'],
26594c83a24SMaria Kustova            ['>I', self.ext_offset + UINT32_S, 0, 'ext_length']
26694c83a24SMaria Kustova        ])
26794c83a24SMaria Kustova
26894c83a24SMaria Kustova    def create_l_structures(self):
26994c83a24SMaria Kustova        """Generate random valid L1 and L2 tables."""
27094c83a24SMaria Kustova        def create_l2_entry(host, guest, l2_cluster):
27194c83a24SMaria Kustova            """Generate one L2 entry."""
27294c83a24SMaria Kustova            offset = l2_cluster * self.cluster_size
273d974451cSEduardo Habkost            l2_size = self.cluster_size // UINT64_S
27494c83a24SMaria Kustova            entry_offset = offset + UINT64_S * (guest % l2_size)
27594c83a24SMaria Kustova            cluster_descriptor = host * self.cluster_size
27694c83a24SMaria Kustova            if not self.header['version'][0].value == 2:
27794c83a24SMaria Kustova                cluster_descriptor += random.randint(0, 1)
27894c83a24SMaria Kustova            # While snapshots are not supported, bit #63 = 1
27994c83a24SMaria Kustova            # Compressed clusters are not supported => bit #62 = 0
28094c83a24SMaria Kustova            entry_val = (1 << 63) + cluster_descriptor
28194c83a24SMaria Kustova            return ['>Q', entry_offset, entry_val, 'l2_entry']
28294c83a24SMaria Kustova
28394c83a24SMaria Kustova        def create_l1_entry(l2_cluster, l1_offset, guest):
28494c83a24SMaria Kustova            """Generate one L1 entry."""
285d974451cSEduardo Habkost            l2_size = self.cluster_size // UINT64_S
286d974451cSEduardo Habkost            entry_offset = l1_offset + UINT64_S * (guest // l2_size)
28794c83a24SMaria Kustova            # While snapshots are not supported bit #63 = 1
28894c83a24SMaria Kustova            entry_val = (1 << 63) + l2_cluster * self.cluster_size
28994c83a24SMaria Kustova            return ['>Q', entry_offset, entry_val, 'l1_entry']
29094c83a24SMaria Kustova
29194c83a24SMaria Kustova        if len(self.data_clusters) == 0:
29294c83a24SMaria Kustova            # All metadata for an empty guest image needs 4 clusters:
29394c83a24SMaria Kustova            # header, rfc table, rfc block, L1 table.
29494c83a24SMaria Kustova            # Header takes cluster #0, other clusters ##1-3 can be used
29594c83a24SMaria Kustova            l1_offset = random.randint(1, 3) * self.cluster_size
29694c83a24SMaria Kustova            l1 = [['>Q', l1_offset, 0, 'l1_entry']]
29794c83a24SMaria Kustova            l2 = []
29894c83a24SMaria Kustova        else:
29994c83a24SMaria Kustova            meta_data = self._get_metadata()
300d974451cSEduardo Habkost            guest_clusters = random.sample(range(self.image_size //
30194c83a24SMaria Kustova                                                 self.cluster_size),
30294c83a24SMaria Kustova                                           len(self.data_clusters))
30394c83a24SMaria Kustova            # Number of entries in a L1/L2 table
304d974451cSEduardo Habkost            l_size = self.cluster_size // UINT64_S
30594c83a24SMaria Kustova            # Number of clusters necessary for L1 table
30694c83a24SMaria Kustova            l1_size = int(ceil((max(guest_clusters) + 1) / float(l_size**2)))
30794c83a24SMaria Kustova            l1_start = self._get_adjacent_clusters(self.data_clusters |
30894c83a24SMaria Kustova                                                   meta_data, l1_size)
30994c83a24SMaria Kustova            meta_data |= set(range(l1_start, l1_start + l1_size))
31094c83a24SMaria Kustova            l1_offset = l1_start * self.cluster_size
31194c83a24SMaria Kustova            # Indices of L2 tables
31294c83a24SMaria Kustova            l2_ids = []
31394c83a24SMaria Kustova            # Host clusters allocated for L2 tables
31494c83a24SMaria Kustova            l2_clusters = []
31594c83a24SMaria Kustova            # L1 entries
31694c83a24SMaria Kustova            l1 = []
31794c83a24SMaria Kustova            # L2 entries
31894c83a24SMaria Kustova            l2 = []
31994c83a24SMaria Kustova            for host, guest in zip(self.data_clusters, guest_clusters):
320d974451cSEduardo Habkost                l2_id = guest // l_size
32194c83a24SMaria Kustova                if l2_id not in l2_ids:
32294c83a24SMaria Kustova                    l2_ids.append(l2_id)
32394c83a24SMaria Kustova                    l2_clusters.append(self._get_adjacent_clusters(
32494c83a24SMaria Kustova                        self.data_clusters | meta_data | set(l2_clusters),
32594c83a24SMaria Kustova                        1))
32694c83a24SMaria Kustova                    l1.append(create_l1_entry(l2_clusters[-1], l1_offset,
32794c83a24SMaria Kustova                                              guest))
32894c83a24SMaria Kustova                l2.append(create_l2_entry(host, guest,
32994c83a24SMaria Kustova                                          l2_clusters[l2_ids.index(l2_id)]))
33094c83a24SMaria Kustova        self.l2_tables = FieldsList(l2)
33194c83a24SMaria Kustova        self.l1_table = FieldsList(l1)
33294c83a24SMaria Kustova        self.header['l1_size'][0].value = int(ceil(UINT64_S * self.image_size /
33394c83a24SMaria Kustova                                                float(self.cluster_size**2)))
33494c83a24SMaria Kustova        self.header['l1_table_offset'][0].value = l1_offset
33594c83a24SMaria Kustova
3361e8fd8d4SMaria Kustova    def create_refcount_structures(self):
3371e8fd8d4SMaria Kustova        """Generate random refcount blocks and refcount table."""
3381e8fd8d4SMaria Kustova        def allocate_rfc_blocks(data, size):
3391e8fd8d4SMaria Kustova            """Return indices of clusters allocated for refcount blocks."""
3401e8fd8d4SMaria Kustova            cluster_ids = set()
341d974451cSEduardo Habkost            diff = block_ids = set([x // size for x in data])
3421e8fd8d4SMaria Kustova            while len(diff) != 0:
3431e8fd8d4SMaria Kustova                # Allocate all yet not allocated clusters
3441e8fd8d4SMaria Kustova                new = self._get_available_clusters(data | cluster_ids,
3451e8fd8d4SMaria Kustova                                                   len(diff))
3461e8fd8d4SMaria Kustova                # Indices of new refcount blocks necessary to cover clusters
3471e8fd8d4SMaria Kustova                # in 'new'
348d974451cSEduardo Habkost                diff = set([x // size for x in new]) - block_ids
3491e8fd8d4SMaria Kustova                cluster_ids |= new
3501e8fd8d4SMaria Kustova                block_ids |= diff
3511e8fd8d4SMaria Kustova            return cluster_ids, block_ids
3521e8fd8d4SMaria Kustova
3531e8fd8d4SMaria Kustova        def allocate_rfc_table(data, init_blocks, block_size):
3541e8fd8d4SMaria Kustova            """Return indices of clusters allocated for the refcount table
3551e8fd8d4SMaria Kustova            and updated indices of clusters allocated for blocks and indices
3561e8fd8d4SMaria Kustova            of blocks.
3571e8fd8d4SMaria Kustova            """
3581e8fd8d4SMaria Kustova            blocks = set(init_blocks)
3591e8fd8d4SMaria Kustova            clusters = set()
3601e8fd8d4SMaria Kustova            # Number of entries in one cluster of the refcount table
361d974451cSEduardo Habkost            size = self.cluster_size // UINT64_S
3621e8fd8d4SMaria Kustova            # Number of clusters necessary for the refcount table based on
3631e8fd8d4SMaria Kustova            # the current number of refcount blocks
3641e8fd8d4SMaria Kustova            table_size = int(ceil((max(blocks) + 1) / float(size)))
3651e8fd8d4SMaria Kustova            # Index of the first cluster of the refcount table
3661e8fd8d4SMaria Kustova            table_start = self._get_adjacent_clusters(data, table_size + 1)
3671e8fd8d4SMaria Kustova            # Clusters allocated for the current length of the refcount table
3681e8fd8d4SMaria Kustova            table_clusters = set(range(table_start, table_start + table_size))
3691e8fd8d4SMaria Kustova            # Clusters allocated for the refcount table including
3701e8fd8d4SMaria Kustova            # last optional one for potential l1 growth
3711e8fd8d4SMaria Kustova            table_clusters_allocated = set(range(table_start, table_start +
3721e8fd8d4SMaria Kustova                                                 table_size + 1))
3731e8fd8d4SMaria Kustova            # New refcount blocks necessary for clusters occupied by the
3741e8fd8d4SMaria Kustova            # refcount table
375d974451cSEduardo Habkost            diff = set([c // block_size for c in table_clusters]) - blocks
3761e8fd8d4SMaria Kustova            blocks |= diff
3771e8fd8d4SMaria Kustova            while len(diff) != 0:
3781e8fd8d4SMaria Kustova                # Allocate clusters for new refcount blocks
3791e8fd8d4SMaria Kustova                new = self._get_available_clusters((data | clusters) |
3801e8fd8d4SMaria Kustova                                                   table_clusters_allocated,
3811e8fd8d4SMaria Kustova                                                   len(diff))
3821e8fd8d4SMaria Kustova                # Indices of new refcount blocks necessary to cover
3831e8fd8d4SMaria Kustova                # clusters in 'new'
384d974451cSEduardo Habkost                diff = set([x // block_size for x in new]) - blocks
3851e8fd8d4SMaria Kustova                clusters |= new
3861e8fd8d4SMaria Kustova                blocks |= diff
3871e8fd8d4SMaria Kustova                # Check if the refcount table needs one more cluster
3881e8fd8d4SMaria Kustova                if int(ceil((max(blocks) + 1) / float(size))) > table_size:
389d974451cSEduardo Habkost                    new_block_id = (table_start + table_size) // block_size
3901e8fd8d4SMaria Kustova                    # Check if the additional table cluster needs
3911e8fd8d4SMaria Kustova                    # one more refcount block
3921e8fd8d4SMaria Kustova                    if new_block_id not in blocks:
3931e8fd8d4SMaria Kustova                        diff.add(new_block_id)
3941e8fd8d4SMaria Kustova                    table_clusters.add(table_start + table_size)
3951e8fd8d4SMaria Kustova                    table_size += 1
3961e8fd8d4SMaria Kustova            return table_clusters, blocks, clusters
3971e8fd8d4SMaria Kustova
3981e8fd8d4SMaria Kustova        def create_table_entry(table_offset, block_cluster, block_size,
3991e8fd8d4SMaria Kustova                               cluster):
4001e8fd8d4SMaria Kustova            """Generate a refcount table entry."""
401d974451cSEduardo Habkost            offset = table_offset + UINT64_S * (cluster // block_size)
4021e8fd8d4SMaria Kustova            return ['>Q', offset, block_cluster * self.cluster_size,
4031e8fd8d4SMaria Kustova                    'refcount_table_entry']
4041e8fd8d4SMaria Kustova
4051e8fd8d4SMaria Kustova        def create_block_entry(block_cluster, block_size, cluster):
4061e8fd8d4SMaria Kustova            """Generate a list of entries for the current block."""
407d974451cSEduardo Habkost            entry_size = self.cluster_size // block_size
4081e8fd8d4SMaria Kustova            offset = block_cluster * self.cluster_size
4091e8fd8d4SMaria Kustova            entry_offset = offset + entry_size * (cluster % block_size)
4101e8fd8d4SMaria Kustova            # While snapshots are not supported all refcounts are set to 1
4111e8fd8d4SMaria Kustova            return ['>H', entry_offset, 1, 'refcount_block_entry']
4121e8fd8d4SMaria Kustova        # Size of a block entry in bits
4131e8fd8d4SMaria Kustova        refcount_bits = 1 << self.header['refcount_order'][0].value
4141e8fd8d4SMaria Kustova        # Number of refcount entries per refcount block
4151e8fd8d4SMaria Kustova        # Convert self.cluster_size from bytes to bits to have the same
4161e8fd8d4SMaria Kustova        # base for the numerator and denominator
417d974451cSEduardo Habkost        block_size = self.cluster_size * 8 // refcount_bits
4181e8fd8d4SMaria Kustova        meta_data = self._get_metadata()
4191e8fd8d4SMaria Kustova        if len(self.data_clusters) == 0:
4201e8fd8d4SMaria Kustova            # All metadata for an empty guest image needs 4 clusters:
4211e8fd8d4SMaria Kustova            # header, rfc table, rfc block, L1 table.
4221e8fd8d4SMaria Kustova            # Header takes cluster #0, other clusters ##1-3 can be used
4231e8fd8d4SMaria Kustova            block_clusters = set([random.choice(list(set(range(1, 4)) -
4241e8fd8d4SMaria Kustova                                                     meta_data))])
4251e8fd8d4SMaria Kustova            block_ids = set([0])
4261e8fd8d4SMaria Kustova            table_clusters = set([random.choice(list(set(range(1, 4)) -
4271e8fd8d4SMaria Kustova                                                     meta_data -
4281e8fd8d4SMaria Kustova                                                     block_clusters))])
4291e8fd8d4SMaria Kustova        else:
4301e8fd8d4SMaria Kustova            block_clusters, block_ids = \
4311e8fd8d4SMaria Kustova                                allocate_rfc_blocks(self.data_clusters |
4321e8fd8d4SMaria Kustova                                                    meta_data, block_size)
4331e8fd8d4SMaria Kustova            table_clusters, block_ids, new_clusters = \
4341e8fd8d4SMaria Kustova                                    allocate_rfc_table(self.data_clusters |
4351e8fd8d4SMaria Kustova                                                       meta_data |
4361e8fd8d4SMaria Kustova                                                       block_clusters,
4371e8fd8d4SMaria Kustova                                                       block_ids,
4381e8fd8d4SMaria Kustova                                                       block_size)
4391e8fd8d4SMaria Kustova            block_clusters |= new_clusters
4401e8fd8d4SMaria Kustova
4411e8fd8d4SMaria Kustova        meta_data |= block_clusters | table_clusters
4421e8fd8d4SMaria Kustova        table_offset = min(table_clusters) * self.cluster_size
4431e8fd8d4SMaria Kustova        block_id = None
4441e8fd8d4SMaria Kustova        # Clusters allocated for refcount blocks
4451e8fd8d4SMaria Kustova        block_clusters = list(block_clusters)
4461e8fd8d4SMaria Kustova        # Indices of refcount blocks
4471e8fd8d4SMaria Kustova        block_ids = list(block_ids)
4481e8fd8d4SMaria Kustova        # Refcount table entries
4491e8fd8d4SMaria Kustova        rfc_table = []
4501e8fd8d4SMaria Kustova        # Refcount entries
4511e8fd8d4SMaria Kustova        rfc_blocks = []
4521e8fd8d4SMaria Kustova
4531e8fd8d4SMaria Kustova        for cluster in sorted(self.data_clusters | meta_data):
454d974451cSEduardo Habkost            if cluster // block_size != block_id:
455d974451cSEduardo Habkost                block_id = cluster // block_size
4561e8fd8d4SMaria Kustova                block_cluster = block_clusters[block_ids.index(block_id)]
4571e8fd8d4SMaria Kustova                rfc_table.append(create_table_entry(table_offset,
4581e8fd8d4SMaria Kustova                                                    block_cluster,
4591e8fd8d4SMaria Kustova                                                    block_size, cluster))
4601e8fd8d4SMaria Kustova            rfc_blocks.append(create_block_entry(block_cluster, block_size,
4611e8fd8d4SMaria Kustova                                                 cluster))
4621e8fd8d4SMaria Kustova        self.refcount_table = FieldsList(rfc_table)
4631e8fd8d4SMaria Kustova        self.refcount_blocks = FieldsList(rfc_blocks)
4641e8fd8d4SMaria Kustova
4651e8fd8d4SMaria Kustova        self.header['refcount_table_offset'][0].value = table_offset
4661e8fd8d4SMaria Kustova        self.header['refcount_table_clusters'][0].value = len(table_clusters)
4671e8fd8d4SMaria Kustova
46894c83a24SMaria Kustova    def fuzz(self, fields_to_fuzz=None):
46994c83a24SMaria Kustova        """Fuzz an image by corrupting values of a random subset of its fields.
47094c83a24SMaria Kustova
47194c83a24SMaria Kustova        Without parameters the method fuzzes an entire image.
47294c83a24SMaria Kustova
47394c83a24SMaria Kustova        If 'fields_to_fuzz' is specified then only fields in this list will be
47494c83a24SMaria Kustova        fuzzed. 'fields_to_fuzz' can contain both individual fields and more
47594c83a24SMaria Kustova        general image elements as a header or tables.
47694c83a24SMaria Kustova
47794c83a24SMaria Kustova        In the first case the field will be fuzzed always.
47894c83a24SMaria Kustova        In the second a random subset of fields will be selected and fuzzed.
47994c83a24SMaria Kustova        """
48094c83a24SMaria Kustova        def coin():
48194c83a24SMaria Kustova            """Return boolean value proportional to a portion of fields to be
48294c83a24SMaria Kustova            fuzzed.
48394c83a24SMaria Kustova            """
48494c83a24SMaria Kustova            return random.random() < self.bias
48594c83a24SMaria Kustova
48694c83a24SMaria Kustova        if fields_to_fuzz is None:
48794c83a24SMaria Kustova            for field in self:
48894c83a24SMaria Kustova                if coin():
48994c83a24SMaria Kustova                    field.value = getattr(fuzz, field.name)(field.value)
49094c83a24SMaria Kustova        else:
49194c83a24SMaria Kustova            for item in fields_to_fuzz:
49294c83a24SMaria Kustova                if len(item) == 1:
49394c83a24SMaria Kustova                    for field in getattr(self, item[0]):
49494c83a24SMaria Kustova                        if coin():
49594c83a24SMaria Kustova                            field.value = getattr(fuzz,
49694c83a24SMaria Kustova                                                  field.name)(field.value)
49794c83a24SMaria Kustova                else:
49894c83a24SMaria Kustova                    # If fields with the requested name were not generated
49994c83a24SMaria Kustova                    # getattr(self, item[0])[item[1]] returns an empty list
50094c83a24SMaria Kustova                    for field in getattr(self, item[0])[item[1]]:
50194c83a24SMaria Kustova                        field.value = getattr(fuzz, field.name)(field.value)
50294c83a24SMaria Kustova
50394c83a24SMaria Kustova    def write(self, filename):
50494c83a24SMaria Kustova        """Write an entire image to the file."""
50560d3af55SEduardo Habkost        image_file = open(filename, 'wb')
50694c83a24SMaria Kustova        for field in self:
50794c83a24SMaria Kustova            image_file.seek(field.offset)
50894c83a24SMaria Kustova            image_file.write(struct.pack(field.fmt, field.value))
50994c83a24SMaria Kustova
51094c83a24SMaria Kustova        for cluster in sorted(self.data_clusters):
51194c83a24SMaria Kustova            image_file.seek(cluster * self.cluster_size)
51294c83a24SMaria Kustova            image_file.write(urandom(self.cluster_size))
51394c83a24SMaria Kustova
51494c83a24SMaria Kustova        # Align the real image size to the cluster size
51594c83a24SMaria Kustova        image_file.seek(0, 2)
51694c83a24SMaria Kustova        size = image_file.tell()
51794c83a24SMaria Kustova        rounded = (size + self.cluster_size - 1) & ~(self.cluster_size - 1)
51894c83a24SMaria Kustova        if rounded > size:
51994c83a24SMaria Kustova            image_file.seek(rounded - 1)
520c314e50bSEduardo Habkost            image_file.write(b'\x00')
52194c83a24SMaria Kustova        image_file.close()
52294c83a24SMaria Kustova
523e1232323SMaria Kustova    @staticmethod
524e1232323SMaria Kustova    def _size_params():
525e1232323SMaria Kustova        """Generate a random image size aligned to a random correct
526e1232323SMaria Kustova        cluster size.
527e1232323SMaria Kustova        """
528e1232323SMaria Kustova        cluster_bits = random.randrange(9, 21)
529e1232323SMaria Kustova        cluster_size = 1 << cluster_bits
530e1232323SMaria Kustova        img_size = random.randrange(0, MAX_IMAGE_SIZE + 1, cluster_size)
531e1232323SMaria Kustova        return (cluster_bits, img_size)
532e1232323SMaria Kustova
533e1232323SMaria Kustova    @staticmethod
53438eb193bSMaria Kustova    def _get_available_clusters(used, number):
53538eb193bSMaria Kustova        """Return a set of indices of not allocated clusters.
53638eb193bSMaria Kustova
53738eb193bSMaria Kustova        'used' contains indices of currently allocated clusters.
53838eb193bSMaria Kustova        All clusters that cannot be allocated between 'used' clusters will have
53938eb193bSMaria Kustova        indices appended to the end of 'used'.
54038eb193bSMaria Kustova        """
54138eb193bSMaria Kustova        append_id = max(used) + 1
54238eb193bSMaria Kustova        free = set(range(1, append_id)) - used
54338eb193bSMaria Kustova        if len(free) >= number:
54438eb193bSMaria Kustova            return set(random.sample(free, number))
54538eb193bSMaria Kustova        else:
54638eb193bSMaria Kustova            return free | set(range(append_id, append_id + number - len(free)))
54738eb193bSMaria Kustova
54838eb193bSMaria Kustova    @staticmethod
54938eb193bSMaria Kustova    def _get_adjacent_clusters(used, size):
55038eb193bSMaria Kustova        """Return an index of the first cluster in the sequence of free ones.
55138eb193bSMaria Kustova
55238eb193bSMaria Kustova        'used' contains indices of currently allocated clusters. 'size' is the
55338eb193bSMaria Kustova        length of the sequence of free clusters.
55438eb193bSMaria Kustova        If the sequence of 'size' is not available between 'used' clusters, its
55538eb193bSMaria Kustova        first index will be append to the end of 'used'.
55638eb193bSMaria Kustova        """
55738eb193bSMaria Kustova        def get_cluster_id(lst, length):
55838eb193bSMaria Kustova            """Return the first index of the sequence of the specified length
55938eb193bSMaria Kustova            or None if the sequence cannot be inserted in the list.
56038eb193bSMaria Kustova            """
56138eb193bSMaria Kustova            if len(lst) != 0:
56238eb193bSMaria Kustova                pairs = []
56338eb193bSMaria Kustova                pair = (lst[0], 1)
56438eb193bSMaria Kustova                for i in range(1, len(lst)):
56538eb193bSMaria Kustova                    if lst[i] == lst[i-1] + 1:
56638eb193bSMaria Kustova                        pair = (lst[i], pair[1] + 1)
56738eb193bSMaria Kustova                    else:
56838eb193bSMaria Kustova                        pairs.append(pair)
56938eb193bSMaria Kustova                        pair = (lst[i], 1)
57038eb193bSMaria Kustova                pairs.append(pair)
57138eb193bSMaria Kustova                random.shuffle(pairs)
57238eb193bSMaria Kustova                for x, s in pairs:
57338eb193bSMaria Kustova                    if s >= length:
57438eb193bSMaria Kustova                        return x - length + 1
57538eb193bSMaria Kustova            return None
57638eb193bSMaria Kustova
57738eb193bSMaria Kustova        append_id = max(used) + 1
57838eb193bSMaria Kustova        free = list(set(range(1, append_id)) - used)
57938eb193bSMaria Kustova        idx = get_cluster_id(free, size)
58038eb193bSMaria Kustova        if idx is None:
58138eb193bSMaria Kustova            return append_id
58238eb193bSMaria Kustova        else:
58338eb193bSMaria Kustova            return idx
58438eb193bSMaria Kustova
58538eb193bSMaria Kustova    @staticmethod
58638eb193bSMaria Kustova    def _alloc_data(img_size, cluster_size):
58738eb193bSMaria Kustova        """Return a set of random indices of clusters allocated for guest data.
58838eb193bSMaria Kustova        """
589d974451cSEduardo Habkost        num_of_cls = img_size // cluster_size
59038eb193bSMaria Kustova        return set(random.sample(range(1, num_of_cls + 1),
59138eb193bSMaria Kustova                                 random.randint(0, num_of_cls)))
59238eb193bSMaria Kustova
59394c83a24SMaria Kustova    def _get_metadata(self):
59494c83a24SMaria Kustova        """Return indices of clusters allocated for image metadata."""
59594c83a24SMaria Kustova        ids = set()
59694c83a24SMaria Kustova        for x in self:
597d974451cSEduardo Habkost            ids.add(x.offset // self.cluster_size)
59894c83a24SMaria Kustova        return ids
599e1232323SMaria Kustova
600e1232323SMaria Kustova
601e1232323SMaria Kustovadef create_image(test_img_path, backing_file_name=None, backing_file_fmt=None,
602e1232323SMaria Kustova                 fields_to_fuzz=None):
603e1232323SMaria Kustova    """Create a fuzzed image and write it to the specified file."""
604*58b818d5SEduardo Habkost    image = Image(backing_file_name.encode())
605*58b818d5SEduardo Habkost    image.set_backing_file_format(backing_file_fmt.encode())
60694c83a24SMaria Kustova    image.create_feature_name_table()
60794c83a24SMaria Kustova    image.set_end_of_extension_area()
60894c83a24SMaria Kustova    image.create_l_structures()
6091e8fd8d4SMaria Kustova    image.create_refcount_structures()
610e1232323SMaria Kustova    image.fuzz(fields_to_fuzz)
611e1232323SMaria Kustova    image.write(test_img_path)
612e1232323SMaria Kustova    return image.image_size
613