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