xref: /openbmc/qemu/tests/image-fuzzer/qcow2/layout.py (revision 38eb193b8b14844a5d5ef65b54a4926e931ac311)
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
21e1232323SMaria Kustovaimport fuzz
22*38eb193bSMaria Kustovafrom math import ceil
23*38eb193bSMaria Kustovafrom os import urandom
24e1232323SMaria Kustova
25e1232323SMaria KustovaMAX_IMAGE_SIZE = 10 * (1 << 20)
26e1232323SMaria Kustova# Standard sizes
27e1232323SMaria KustovaUINT32_S = 4
28e1232323SMaria KustovaUINT64_S = 8
29e1232323SMaria Kustova
30e1232323SMaria Kustova
31e1232323SMaria Kustovaclass Field(object):
32e1232323SMaria Kustova
33e1232323SMaria Kustova    """Atomic image element (field).
34e1232323SMaria Kustova
35e1232323SMaria Kustova    The class represents an image field as quadruple of a data format
36e1232323SMaria Kustova    of value necessary for its packing to binary form, an offset from
37e1232323SMaria Kustova    the beginning of the image, a value and a name.
38e1232323SMaria Kustova
39e1232323SMaria Kustova    The field can be iterated as a list [format, offset, value].
40e1232323SMaria Kustova    """
41e1232323SMaria Kustova
42e1232323SMaria Kustova    __slots__ = ('fmt', 'offset', 'value', 'name')
43e1232323SMaria Kustova
44e1232323SMaria Kustova    def __init__(self, fmt, offset, val, name):
45e1232323SMaria Kustova        self.fmt = fmt
46e1232323SMaria Kustova        self.offset = offset
47e1232323SMaria Kustova        self.value = val
48e1232323SMaria Kustova        self.name = name
49e1232323SMaria Kustova
50e1232323SMaria Kustova    def __iter__(self):
51e1232323SMaria Kustova        return iter([self.fmt, self.offset, self.value])
52e1232323SMaria Kustova
53e1232323SMaria Kustova    def __repr__(self):
54e1232323SMaria Kustova        return "Field(fmt='%s', offset=%d, value=%s, name=%s)" % \
55e1232323SMaria Kustova            (self.fmt, self.offset, str(self.value), self.name)
56e1232323SMaria Kustova
57e1232323SMaria Kustova
58e1232323SMaria Kustovaclass FieldsList(object):
59e1232323SMaria Kustova
60e1232323SMaria Kustova    """List of fields.
61e1232323SMaria Kustova
62e1232323SMaria Kustova    The class allows access to a field in the list by its name and joins
63e1232323SMaria Kustova    several list in one via in-place addition.
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:
70e1232323SMaria Kustova            self.data = [Field(f[0], f[1], f[2], f[3])
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 __iadd__(self, other):
80e1232323SMaria Kustova        self.data += other.data
81e1232323SMaria Kustova        return self
82e1232323SMaria Kustova
83e1232323SMaria Kustova    def __len__(self):
84e1232323SMaria Kustova        return len(self.data)
85e1232323SMaria Kustova
86e1232323SMaria Kustova
87e1232323SMaria Kustovaclass Image(object):
88e1232323SMaria Kustova
89e1232323SMaria Kustova    """ Qcow2 image object.
90e1232323SMaria Kustova
91e1232323SMaria Kustova    This class allows to create qcow2 images with random valid structures and
92e1232323SMaria Kustova    values, fuzz them via external qcow2.fuzz module and write the result to
93e1232323SMaria Kustova    a file.
94e1232323SMaria Kustova    """
95e1232323SMaria Kustova
96e1232323SMaria Kustova    @staticmethod
97e1232323SMaria Kustova    def _size_params():
98e1232323SMaria Kustova        """Generate a random image size aligned to a random correct
99e1232323SMaria Kustova        cluster size.
100e1232323SMaria Kustova        """
101e1232323SMaria Kustova        cluster_bits = random.randrange(9, 21)
102e1232323SMaria Kustova        cluster_size = 1 << cluster_bits
103e1232323SMaria Kustova        img_size = random.randrange(0, MAX_IMAGE_SIZE + 1, cluster_size)
104e1232323SMaria Kustova        return (cluster_bits, img_size)
105e1232323SMaria Kustova
106e1232323SMaria Kustova    @staticmethod
107*38eb193bSMaria Kustova    def _get_available_clusters(used, number):
108*38eb193bSMaria Kustova        """Return a set of indices of not allocated clusters.
109*38eb193bSMaria Kustova
110*38eb193bSMaria Kustova        'used' contains indices of currently allocated clusters.
111*38eb193bSMaria Kustova        All clusters that cannot be allocated between 'used' clusters will have
112*38eb193bSMaria Kustova        indices appended to the end of 'used'.
113*38eb193bSMaria Kustova        """
114*38eb193bSMaria Kustova        append_id = max(used) + 1
115*38eb193bSMaria Kustova        free = set(range(1, append_id)) - used
116*38eb193bSMaria Kustova        if len(free) >= number:
117*38eb193bSMaria Kustova            return set(random.sample(free, number))
118*38eb193bSMaria Kustova        else:
119*38eb193bSMaria Kustova            return free | set(range(append_id, append_id + number - len(free)))
120*38eb193bSMaria Kustova
121*38eb193bSMaria Kustova    @staticmethod
122*38eb193bSMaria Kustova    def _get_adjacent_clusters(used, size):
123*38eb193bSMaria Kustova        """Return an index of the first cluster in the sequence of free ones.
124*38eb193bSMaria Kustova
125*38eb193bSMaria Kustova        'used' contains indices of currently allocated clusters. 'size' is the
126*38eb193bSMaria Kustova        length of the sequence of free clusters.
127*38eb193bSMaria Kustova        If the sequence of 'size' is not available between 'used' clusters, its
128*38eb193bSMaria Kustova        first index will be append to the end of 'used'.
129*38eb193bSMaria Kustova        """
130*38eb193bSMaria Kustova        def get_cluster_id(lst, length):
131*38eb193bSMaria Kustova            """Return the first index of the sequence of the specified length
132*38eb193bSMaria Kustova            or None if the sequence cannot be inserted in the list.
133*38eb193bSMaria Kustova            """
134*38eb193bSMaria Kustova            if len(lst) != 0:
135*38eb193bSMaria Kustova                pairs = []
136*38eb193bSMaria Kustova                pair = (lst[0], 1)
137*38eb193bSMaria Kustova                for i in range(1, len(lst)):
138*38eb193bSMaria Kustova                    if lst[i] == lst[i-1] + 1:
139*38eb193bSMaria Kustova                        pair = (lst[i], pair[1] + 1)
140*38eb193bSMaria Kustova                    else:
141*38eb193bSMaria Kustova                        pairs.append(pair)
142*38eb193bSMaria Kustova                        pair = (lst[i], 1)
143*38eb193bSMaria Kustova                pairs.append(pair)
144*38eb193bSMaria Kustova                random.shuffle(pairs)
145*38eb193bSMaria Kustova                for x, s in pairs:
146*38eb193bSMaria Kustova                    if s >= length:
147*38eb193bSMaria Kustova                        return x - length + 1
148*38eb193bSMaria Kustova            return None
149*38eb193bSMaria Kustova
150*38eb193bSMaria Kustova        append_id = max(used) + 1
151*38eb193bSMaria Kustova        free = list(set(range(1, append_id)) - used)
152*38eb193bSMaria Kustova        idx = get_cluster_id(free, size)
153*38eb193bSMaria Kustova        if idx is None:
154*38eb193bSMaria Kustova            return append_id
155*38eb193bSMaria Kustova        else:
156*38eb193bSMaria Kustova            return idx
157*38eb193bSMaria Kustova
158*38eb193bSMaria Kustova    @staticmethod
159*38eb193bSMaria Kustova    def _alloc_data(img_size, cluster_size):
160*38eb193bSMaria Kustova        """Return a set of random indices of clusters allocated for guest data.
161*38eb193bSMaria Kustova        """
162*38eb193bSMaria Kustova        num_of_cls = img_size/cluster_size
163*38eb193bSMaria Kustova        return set(random.sample(range(1, num_of_cls + 1),
164*38eb193bSMaria Kustova                                 random.randint(0, num_of_cls)))
165*38eb193bSMaria Kustova
166*38eb193bSMaria Kustova    def create_header(self, cluster_bits, backing_file_name=None):
167e1232323SMaria Kustova        """Generate a random valid header."""
168e1232323SMaria Kustova        meta_header = [
169e1232323SMaria Kustova            ['>4s', 0, "QFI\xfb", 'magic'],
170e1232323SMaria Kustova            ['>I', 4, random.randint(2, 3), 'version'],
171e1232323SMaria Kustova            ['>Q', 8, 0, 'backing_file_offset'],
172e1232323SMaria Kustova            ['>I', 16, 0, 'backing_file_size'],
173e1232323SMaria Kustova            ['>I', 20, cluster_bits, 'cluster_bits'],
174*38eb193bSMaria Kustova            ['>Q', 24, self.image_size, 'size'],
175e1232323SMaria Kustova            ['>I', 32, 0, 'crypt_method'],
176e1232323SMaria Kustova            ['>I', 36, 0, 'l1_size'],
177e1232323SMaria Kustova            ['>Q', 40, 0, 'l1_table_offset'],
178e1232323SMaria Kustova            ['>Q', 48, 0, 'refcount_table_offset'],
179e1232323SMaria Kustova            ['>I', 56, 0, 'refcount_table_clusters'],
180e1232323SMaria Kustova            ['>I', 60, 0, 'nb_snapshots'],
181e1232323SMaria Kustova            ['>Q', 64, 0, 'snapshots_offset'],
182e1232323SMaria Kustova            ['>Q', 72, 0, 'incompatible_features'],
183e1232323SMaria Kustova            ['>Q', 80, 0, 'compatible_features'],
184e1232323SMaria Kustova            ['>Q', 88, 0, 'autoclear_features'],
185e1232323SMaria Kustova            # Only refcount_order = 4 is supported by current (07.2014)
186e1232323SMaria Kustova            # implementation of QEMU
187e1232323SMaria Kustova            ['>I', 96, 4, 'refcount_order'],
188e1232323SMaria Kustova            ['>I', 100, 0, 'header_length']
189e1232323SMaria Kustova        ]
190*38eb193bSMaria Kustova        self.header = FieldsList(meta_header)
191e1232323SMaria Kustova
192*38eb193bSMaria Kustova        if self.header['version'][0].value == 2:
193*38eb193bSMaria Kustova            self.header['header_length'][0].value = 72
194e1232323SMaria Kustova        else:
195*38eb193bSMaria Kustova            self.header['incompatible_features'][0].value = \
196*38eb193bSMaria Kustova                                                        random.getrandbits(2)
197*38eb193bSMaria Kustova            self.header['compatible_features'][0].value = random.getrandbits(1)
198*38eb193bSMaria Kustova            self.header['header_length'][0].value = 104
199e1232323SMaria Kustova
200*38eb193bSMaria Kustova        max_header_len = struct.calcsize(
201*38eb193bSMaria Kustova            self.header['header_length'][0].fmt) + \
202*38eb193bSMaria Kustova            self.header['header_length'][0].offset
203e1232323SMaria Kustova        end_of_extension_area_len = 2 * UINT32_S
204*38eb193bSMaria Kustova        free_space = self.cluster_size - max_header_len - \
205*38eb193bSMaria Kustova                     end_of_extension_area_len
206e1232323SMaria Kustova        # If the backing file name specified and there is enough space for it
207e1232323SMaria Kustova        # in the first cluster, then it's placed in the very end of the first
208e1232323SMaria Kustova        # cluster.
209e1232323SMaria Kustova        if (backing_file_name is not None) and \
210e1232323SMaria Kustova           (free_space >= len(backing_file_name)):
211*38eb193bSMaria Kustova            self.header['backing_file_size'][0].value = len(backing_file_name)
212*38eb193bSMaria Kustova            self.header['backing_file_offset'][0].value = \
213*38eb193bSMaria Kustova                                    self.cluster_size - len(backing_file_name)
214e1232323SMaria Kustova
215*38eb193bSMaria Kustova    def set_backing_file_name(self, backing_file_name=None):
216e1232323SMaria Kustova        """Add the name of the backing file at the offset specified
217e1232323SMaria Kustova        in the header.
218e1232323SMaria Kustova        """
219e1232323SMaria Kustova        if (backing_file_name is not None) and \
220*38eb193bSMaria Kustova           (not self.header['backing_file_offset'][0].value == 0):
221e1232323SMaria Kustova            data_len = len(backing_file_name)
222e1232323SMaria Kustova            data_fmt = '>' + str(data_len) + 's'
223*38eb193bSMaria Kustova            self.backing_file_name = FieldsList([
224*38eb193bSMaria Kustova                [data_fmt, self.header['backing_file_offset'][0].value,
225e1232323SMaria Kustova                 backing_file_name, 'bf_name']
226e1232323SMaria Kustova            ])
227e1232323SMaria Kustova        else:
228*38eb193bSMaria Kustova            self.backing_file_name = FieldsList()
229e1232323SMaria Kustova
230*38eb193bSMaria Kustova    def set_backing_file_format(self, backing_file_fmt=None):
231e1232323SMaria Kustova        """Generate the header extension for the backing file
232e1232323SMaria Kustova        format.
233e1232323SMaria Kustova        """
234*38eb193bSMaria Kustova        self.backing_file_format = FieldsList()
235*38eb193bSMaria Kustova        offset = struct.calcsize(self.header['header_length'][0].fmt) + \
236*38eb193bSMaria Kustova                 self.header['header_length'][0].offset
237e1232323SMaria Kustova
238e1232323SMaria Kustova        if backing_file_fmt is not None:
239e1232323SMaria Kustova            # Calculation of the free space available in the first cluster
240e1232323SMaria Kustova            end_of_extension_area_len = 2 * UINT32_S
241*38eb193bSMaria Kustova            high_border = (self.header['backing_file_offset'][0].value or
242*38eb193bSMaria Kustova                           (self.cluster_size - 1)) - \
243e1232323SMaria Kustova                end_of_extension_area_len
244e1232323SMaria Kustova            free_space = high_border - offset
245e1232323SMaria Kustova            ext_size = 2 * UINT32_S + ((len(backing_file_fmt) + 7) & ~7)
246e1232323SMaria Kustova
247e1232323SMaria Kustova            if free_space >= ext_size:
248e1232323SMaria Kustova                ext_data_len = len(backing_file_fmt)
249e1232323SMaria Kustova                ext_data_fmt = '>' + str(ext_data_len) + 's'
250e1232323SMaria Kustova                ext_padding_len = 7 - (ext_data_len - 1) % 8
251*38eb193bSMaria Kustova                self.backing_file_format = FieldsList([
252e1232323SMaria Kustova                    ['>I', offset, 0xE2792ACA, 'ext_magic'],
253e1232323SMaria Kustova                    ['>I', offset + UINT32_S, ext_data_len, 'ext_length'],
254e1232323SMaria Kustova                    [ext_data_fmt, offset + UINT32_S * 2, backing_file_fmt,
255e1232323SMaria Kustova                     'bf_format']
256e1232323SMaria Kustova                ])
257*38eb193bSMaria Kustova                offset = self.backing_file_format['bf_format'][0].offset + \
258*38eb193bSMaria Kustova                         struct.calcsize(self.backing_file_format[
259*38eb193bSMaria Kustova                             'bf_format'][0].fmt) + ext_padding_len
260e1232323SMaria Kustova
261*38eb193bSMaria Kustova        return offset
262*38eb193bSMaria Kustova
263*38eb193bSMaria Kustova    def create_feature_name_table(self, offset):
264e1232323SMaria Kustova        """Generate a random header extension for names of features used in
265e1232323SMaria Kustova        the image.
266e1232323SMaria Kustova        """
267e1232323SMaria Kustova        def gen_feat_ids():
268e1232323SMaria Kustova            """Return random feature type and feature bit."""
269e1232323SMaria Kustova            return (random.randint(0, 2), random.randint(0, 63))
270e1232323SMaria Kustova
271e1232323SMaria Kustova        end_of_extension_area_len = 2 * UINT32_S
272*38eb193bSMaria Kustova        high_border = (self.header['backing_file_offset'][0].value or
273*38eb193bSMaria Kustova                       (self.cluster_size - 1)) - \
274e1232323SMaria Kustova            end_of_extension_area_len
275e1232323SMaria Kustova        free_space = high_border - offset
276e1232323SMaria Kustova        # Sum of sizes of 'magic' and 'length' header extension fields
277e1232323SMaria Kustova        ext_header_len = 2 * UINT32_S
278e1232323SMaria Kustova        fnt_entry_size = 6 * UINT64_S
279e1232323SMaria Kustova        num_fnt_entries = min(10, (free_space - ext_header_len) /
280e1232323SMaria Kustova                              fnt_entry_size)
281e1232323SMaria Kustova        if not num_fnt_entries == 0:
282e1232323SMaria Kustova            feature_tables = []
283e1232323SMaria Kustova            feature_ids = []
284e1232323SMaria Kustova            inner_offset = offset + ext_header_len
285e1232323SMaria Kustova            feat_name = 'some cool feature'
286e1232323SMaria Kustova            while len(feature_tables) < num_fnt_entries * 3:
287e1232323SMaria Kustova                feat_type, feat_bit = gen_feat_ids()
288e1232323SMaria Kustova                # Remove duplicates
289e1232323SMaria Kustova                while (feat_type, feat_bit) in feature_ids:
290e1232323SMaria Kustova                    feat_type, feat_bit = gen_feat_ids()
291e1232323SMaria Kustova                feature_ids.append((feat_type, feat_bit))
292e1232323SMaria Kustova                feat_fmt = '>' + str(len(feat_name)) + 's'
293e1232323SMaria Kustova                feature_tables += [['B', inner_offset,
294e1232323SMaria Kustova                                    feat_type, 'feature_type'],
295e1232323SMaria Kustova                                   ['B', inner_offset + 1, feat_bit,
296e1232323SMaria Kustova                                    'feature_bit_number'],
297e1232323SMaria Kustova                                   [feat_fmt, inner_offset + 2,
298e1232323SMaria Kustova                                    feat_name, 'feature_name']
299e1232323SMaria Kustova                ]
300e1232323SMaria Kustova                inner_offset += fnt_entry_size
301e1232323SMaria Kustova            # No padding for the extension is necessary, because
302e1232323SMaria Kustova            # the extension length is multiple of 8
303*38eb193bSMaria Kustova            self.feature_name_table = FieldsList([
304e1232323SMaria Kustova                ['>I', offset, 0x6803f857, 'ext_magic'],
305e1232323SMaria Kustova                # One feature table contains 3 fields and takes 48 bytes
306e1232323SMaria Kustova                ['>I', offset + UINT32_S, len(feature_tables) / 3 * 48,
307e1232323SMaria Kustova                 'ext_length']
308e1232323SMaria Kustova            ] + feature_tables)
309e1232323SMaria Kustova            offset = inner_offset
310e1232323SMaria Kustova        else:
311*38eb193bSMaria Kustova            self.feature_name_table = FieldsList()
312e1232323SMaria Kustova
313*38eb193bSMaria Kustova        return offset
314e1232323SMaria Kustova
315*38eb193bSMaria Kustova    def set_end_of_extension_area(self, offset):
316e1232323SMaria Kustova        """Generate a mandatory header extension marking end of header
317e1232323SMaria Kustova        extensions.
318e1232323SMaria Kustova        """
319*38eb193bSMaria Kustova        self.end_of_extension_area = FieldsList([
320e1232323SMaria Kustova            ['>I', offset, 0, 'ext_magic'],
321e1232323SMaria Kustova            ['>I', offset + UINT32_S, 0, 'ext_length']
322e1232323SMaria Kustova        ])
323*38eb193bSMaria Kustova
324*38eb193bSMaria Kustova    def create_l_structures(self):
325*38eb193bSMaria Kustova        """Generate random valid L1 and L2 tables."""
326*38eb193bSMaria Kustova        def create_l2_entry(host, guest, l2_cluster):
327*38eb193bSMaria Kustova            """Generate one L2 entry."""
328*38eb193bSMaria Kustova            offset = l2_cluster * self.cluster_size
329*38eb193bSMaria Kustova            l2_size = self.cluster_size / UINT64_S
330*38eb193bSMaria Kustova            entry_offset = offset + UINT64_S * (guest % l2_size)
331*38eb193bSMaria Kustova            cluster_descriptor = host * self.cluster_size
332*38eb193bSMaria Kustova            if not self.header['version'][0].value == 2:
333*38eb193bSMaria Kustova                cluster_descriptor += random.randint(0, 1)
334*38eb193bSMaria Kustova            # While snapshots are not supported, bit #63 = 1
335*38eb193bSMaria Kustova            # Compressed clusters are not supported => bit #62 = 0
336*38eb193bSMaria Kustova            entry_val = (1 << 63) + cluster_descriptor
337*38eb193bSMaria Kustova            return ['>Q', entry_offset, entry_val, 'l2_entry']
338*38eb193bSMaria Kustova
339*38eb193bSMaria Kustova        def create_l1_entry(l2_cluster, l1_offset, guest):
340*38eb193bSMaria Kustova            """Generate one L1 entry."""
341*38eb193bSMaria Kustova            l2_size = self.cluster_size / UINT64_S
342*38eb193bSMaria Kustova            entry_offset = l1_offset + UINT64_S * (guest / l2_size)
343*38eb193bSMaria Kustova            # While snapshots are not supported bit #63 = 1
344*38eb193bSMaria Kustova            entry_val = (1 << 63) + l2_cluster * self.cluster_size
345*38eb193bSMaria Kustova            return ['>Q', entry_offset, entry_val, 'l1_entry']
346*38eb193bSMaria Kustova
347*38eb193bSMaria Kustova        if len(self.data_clusters) == 0:
348*38eb193bSMaria Kustova            # All metadata for an empty guest image needs 4 clusters:
349*38eb193bSMaria Kustova            # header, rfc table, rfc block, L1 table.
350*38eb193bSMaria Kustova            # Header takes cluster #0, other clusters ##1-3 can be used
351*38eb193bSMaria Kustova            l1_offset = random.randint(1, 3) * self.cluster_size
352*38eb193bSMaria Kustova            l1 = [['>Q', l1_offset, 0, 'l1_entry']]
353*38eb193bSMaria Kustova            l2 = []
354*38eb193bSMaria Kustova        else:
355*38eb193bSMaria Kustova            meta_data = set([0])
356*38eb193bSMaria Kustova            guest_clusters = random.sample(range(self.image_size /
357*38eb193bSMaria Kustova                                                 self.cluster_size),
358*38eb193bSMaria Kustova                                           len(self.data_clusters))
359*38eb193bSMaria Kustova            # Number of entries in a L1/L2 table
360*38eb193bSMaria Kustova            l_size = self.cluster_size / UINT64_S
361*38eb193bSMaria Kustova            # Number of clusters necessary for L1 table
362*38eb193bSMaria Kustova            l1_size = int(ceil((max(guest_clusters) + 1) / float(l_size**2)))
363*38eb193bSMaria Kustova            l1_start = self._get_adjacent_clusters(self.data_clusters |
364*38eb193bSMaria Kustova                                                   meta_data, l1_size)
365*38eb193bSMaria Kustova            meta_data |= set(range(l1_start, l1_start + l1_size))
366*38eb193bSMaria Kustova            l1_offset = l1_start * self.cluster_size
367*38eb193bSMaria Kustova            # Indices of L2 tables
368*38eb193bSMaria Kustova            l2_ids = []
369*38eb193bSMaria Kustova            # Host clusters allocated for L2 tables
370*38eb193bSMaria Kustova            l2_clusters = []
371*38eb193bSMaria Kustova            # L1 entries
372*38eb193bSMaria Kustova            l1 = []
373*38eb193bSMaria Kustova            # L2 entries
374*38eb193bSMaria Kustova            l2 = []
375*38eb193bSMaria Kustova            for host, guest in zip(self.data_clusters, guest_clusters):
376*38eb193bSMaria Kustova                l2_id = guest / l_size
377*38eb193bSMaria Kustova                if l2_id not in l2_ids:
378*38eb193bSMaria Kustova                    l2_ids.append(l2_id)
379*38eb193bSMaria Kustova                    l2_clusters.append(self._get_adjacent_clusters(
380*38eb193bSMaria Kustova                        self.data_clusters | meta_data | set(l2_clusters),
381*38eb193bSMaria Kustova                        1))
382*38eb193bSMaria Kustova                    l1.append(create_l1_entry(l2_clusters[-1], l1_offset,
383*38eb193bSMaria Kustova                                              guest))
384*38eb193bSMaria Kustova                l2.append(create_l2_entry(host, guest,
385*38eb193bSMaria Kustova                                          l2_clusters[l2_ids.index(l2_id)]))
386*38eb193bSMaria Kustova        self.l2_tables = FieldsList(l2)
387*38eb193bSMaria Kustova        self.l1_table = FieldsList(l1)
388*38eb193bSMaria Kustova        self.header['l1_size'][0].value = int(ceil(UINT64_S * self.image_size /
389*38eb193bSMaria Kustova                                                float(self.cluster_size**2)))
390*38eb193bSMaria Kustova        self.header['l1_table_offset'][0].value = l1_offset
391e1232323SMaria Kustova
392e1232323SMaria Kustova    def __init__(self, backing_file_name=None, backing_file_fmt=None):
393e1232323SMaria Kustova        """Create a random valid qcow2 image with the correct inner structure
394e1232323SMaria Kustova        and allowable values.
395e1232323SMaria Kustova        """
396e1232323SMaria Kustova        cluster_bits, self.image_size = self._size_params()
397e1232323SMaria Kustova        self.cluster_size = 1 << cluster_bits
398*38eb193bSMaria Kustova        self.create_header(cluster_bits, backing_file_name)
399*38eb193bSMaria Kustova        self.set_backing_file_name(backing_file_name)
400*38eb193bSMaria Kustova        offset = self.set_backing_file_format(backing_file_fmt)
401*38eb193bSMaria Kustova        offset = self.create_feature_name_table(offset)
402*38eb193bSMaria Kustova        self.set_end_of_extension_area(offset)
403*38eb193bSMaria Kustova        self.data_clusters = self._alloc_data(self.image_size,
404*38eb193bSMaria Kustova                                              self.cluster_size)
405*38eb193bSMaria Kustova        self.create_l_structures()
406e1232323SMaria Kustova        # Container for entire image
407e1232323SMaria Kustova        self.data = FieldsList()
408e1232323SMaria Kustova        # Percentage of fields will be fuzzed
409e1232323SMaria Kustova        self.bias = random.uniform(0.2, 0.5)
410e1232323SMaria Kustova
411e1232323SMaria Kustova    def __iter__(self):
412e1232323SMaria Kustova        return iter([self.header,
413e1232323SMaria Kustova                     self.backing_file_format,
414e1232323SMaria Kustova                     self.feature_name_table,
415e1232323SMaria Kustova                     self.end_of_extension_area,
416*38eb193bSMaria Kustova                     self.backing_file_name,
417*38eb193bSMaria Kustova                     self.l1_table,
418*38eb193bSMaria Kustova                     self.l2_tables])
419e1232323SMaria Kustova
420e1232323SMaria Kustova    def _join(self):
421e1232323SMaria Kustova        """Join all image structure elements as header, tables, etc in one
422e1232323SMaria Kustova        list of fields.
423e1232323SMaria Kustova        """
424e1232323SMaria Kustova        if len(self.data) == 0:
425e1232323SMaria Kustova            for v in self:
426e1232323SMaria Kustova                self.data += v
427e1232323SMaria Kustova
428e1232323SMaria Kustova    def fuzz(self, fields_to_fuzz=None):
429e1232323SMaria Kustova        """Fuzz an image by corrupting values of a random subset of its fields.
430e1232323SMaria Kustova
431e1232323SMaria Kustova        Without parameters the method fuzzes an entire image.
432e1232323SMaria Kustova        If 'fields_to_fuzz' is specified then only fields in this list will be
433e1232323SMaria Kustova        fuzzed. 'fields_to_fuzz' can contain both individual fields and more
434e1232323SMaria Kustova        general image elements as a header or tables.
435e1232323SMaria Kustova        In the first case the field will be fuzzed always.
436e1232323SMaria Kustova        In the second a random subset of fields will be selected and fuzzed.
437e1232323SMaria Kustova        """
438e1232323SMaria Kustova        def coin():
439e1232323SMaria Kustova            """Return boolean value proportional to a portion of fields to be
440e1232323SMaria Kustova            fuzzed.
441e1232323SMaria Kustova            """
442e1232323SMaria Kustova            return random.random() < self.bias
443e1232323SMaria Kustova
444e1232323SMaria Kustova        if fields_to_fuzz is None:
445e1232323SMaria Kustova            self._join()
446e1232323SMaria Kustova            for field in self.data:
447e1232323SMaria Kustova                if coin():
448e1232323SMaria Kustova                    field.value = getattr(fuzz, field.name)(field.value)
449e1232323SMaria Kustova        else:
450e1232323SMaria Kustova            for item in fields_to_fuzz:
451e1232323SMaria Kustova                if len(item) == 1:
452e1232323SMaria Kustova                    for field in getattr(self, item[0]):
453e1232323SMaria Kustova                        if coin():
454e1232323SMaria Kustova                            field.value = getattr(fuzz,
455e1232323SMaria Kustova                                                  field.name)(field.value)
456e1232323SMaria Kustova                else:
457e1232323SMaria Kustova                    for field in getattr(self, item[0])[item[1]]:
458e1232323SMaria Kustova                        try:
459e1232323SMaria Kustova                            field.value = getattr(fuzz, field.name)(
460e1232323SMaria Kustova                                field.value)
461e1232323SMaria Kustova                        except AttributeError:
462e1232323SMaria Kustova                            # Some fields can be skipped depending on
463*38eb193bSMaria Kustova                            # their prerequisites
464e1232323SMaria Kustova                            pass
465e1232323SMaria Kustova
466e1232323SMaria Kustova    def write(self, filename):
467e1232323SMaria Kustova        """Write an entire image to the file."""
468e1232323SMaria Kustova        image_file = open(filename, 'w')
469e1232323SMaria Kustova        self._join()
470e1232323SMaria Kustova        for field in self.data:
471e1232323SMaria Kustova            image_file.seek(field.offset)
472e1232323SMaria Kustova            image_file.write(struct.pack(field.fmt, field.value))
473*38eb193bSMaria Kustova
474*38eb193bSMaria Kustova        for cluster in sorted(self.data_clusters):
475*38eb193bSMaria Kustova            image_file.seek(cluster * self.cluster_size)
476*38eb193bSMaria Kustova            image_file.write(urandom(self.cluster_size))
477*38eb193bSMaria Kustova
478*38eb193bSMaria Kustova        # Align the real image size to the cluster size
479e1232323SMaria Kustova        image_file.seek(0, 2)
480e1232323SMaria Kustova        size = image_file.tell()
481e1232323SMaria Kustova        rounded = (size + self.cluster_size - 1) & ~(self.cluster_size - 1)
482e1232323SMaria Kustova        if rounded > size:
483e1232323SMaria Kustova            image_file.seek(rounded - 1)
484e1232323SMaria Kustova            image_file.write("\0")
485e1232323SMaria Kustova        image_file.close()
486e1232323SMaria Kustova
487e1232323SMaria Kustova
488e1232323SMaria Kustovadef create_image(test_img_path, backing_file_name=None, backing_file_fmt=None,
489e1232323SMaria Kustova                 fields_to_fuzz=None):
490e1232323SMaria Kustova    """Create a fuzzed image and write it to the specified file."""
491e1232323SMaria Kustova    image = Image(backing_file_name, backing_file_fmt)
492e1232323SMaria Kustova    image.fuzz(fields_to_fuzz)
493e1232323SMaria Kustova    image.write(test_img_path)
494e1232323SMaria Kustova    return image.image_size
495