xref: /openbmc/qemu/tests/image-fuzzer/qcow2/layout.py (revision 94c83a24c1956cd50ab979725a730f7d8649ac15)
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
2238eb193bSMaria Kustovafrom math import ceil
2338eb193bSMaria Kustovafrom os import urandom
24*94c83a24SMaria 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
40*94c83a24SMaria 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):
52*94c83a24SMaria Kustova        return iter([self.fmt, self.offset, self.value, self.name])
53e1232323SMaria Kustova
54e1232323SMaria Kustova    def __repr__(self):
55e1232323SMaria Kustova        return "Field(fmt='%s', offset=%d, value=%s, name=%s)" % \
56e1232323SMaria Kustova            (self.fmt, self.offset, str(self.value), self.name)
57e1232323SMaria Kustova
58e1232323SMaria Kustova
59e1232323SMaria Kustovaclass FieldsList(object):
60e1232323SMaria Kustova
61e1232323SMaria Kustova    """List of fields.
62e1232323SMaria Kustova
63*94c83a24SMaria 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:
70*94c83a24SMaria 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
92*94c83a24SMaria Kustova    def __init__(self, backing_file_name=None):
93*94c83a24SMaria Kustova        """Create a random valid qcow2 image with the correct header and stored
94*94c83a24SMaria Kustova        backing file name.
95*94c83a24SMaria Kustova        """
96*94c83a24SMaria Kustova        cluster_bits, self.image_size = self._size_params()
97*94c83a24SMaria Kustova        self.cluster_size = 1 << cluster_bits
98*94c83a24SMaria Kustova        self.header = FieldsList()
99*94c83a24SMaria Kustova        self.backing_file_name = FieldsList()
100*94c83a24SMaria Kustova        self.backing_file_format = FieldsList()
101*94c83a24SMaria Kustova        self.feature_name_table = FieldsList()
102*94c83a24SMaria Kustova        self.end_of_extension_area = FieldsList()
103*94c83a24SMaria Kustova        self.l2_tables = FieldsList()
104*94c83a24SMaria Kustova        self.l1_table = FieldsList()
105*94c83a24SMaria Kustova        self.ext_offset = 0
106*94c83a24SMaria Kustova        self.create_header(cluster_bits, backing_file_name)
107*94c83a24SMaria Kustova        self.set_backing_file_name(backing_file_name)
108*94c83a24SMaria Kustova        self.data_clusters = self._alloc_data(self.image_size,
109*94c83a24SMaria Kustova                                              self.cluster_size)
110*94c83a24SMaria Kustova        # Percentage of fields will be fuzzed
111*94c83a24SMaria Kustova        self.bias = random.uniform(0.2, 0.5)
112*94c83a24SMaria Kustova
113*94c83a24SMaria Kustova    def __iter__(self):
114*94c83a24SMaria Kustova        return chain(self.header, self.backing_file_format,
115*94c83a24SMaria Kustova                     self.feature_name_table, self.end_of_extension_area,
116*94c83a24SMaria Kustova                     self.backing_file_name, self.l1_table, self.l2_tables)
117*94c83a24SMaria Kustova
118*94c83a24SMaria Kustova    def create_header(self, cluster_bits, backing_file_name=None):
119*94c83a24SMaria Kustova        """Generate a random valid header."""
120*94c83a24SMaria Kustova        meta_header = [
121*94c83a24SMaria Kustova            ['>4s', 0, "QFI\xfb", 'magic'],
122*94c83a24SMaria Kustova            ['>I', 4, random.randint(2, 3), 'version'],
123*94c83a24SMaria Kustova            ['>Q', 8, 0, 'backing_file_offset'],
124*94c83a24SMaria Kustova            ['>I', 16, 0, 'backing_file_size'],
125*94c83a24SMaria Kustova            ['>I', 20, cluster_bits, 'cluster_bits'],
126*94c83a24SMaria Kustova            ['>Q', 24, self.image_size, 'size'],
127*94c83a24SMaria Kustova            ['>I', 32, 0, 'crypt_method'],
128*94c83a24SMaria Kustova            ['>I', 36, 0, 'l1_size'],
129*94c83a24SMaria Kustova            ['>Q', 40, 0, 'l1_table_offset'],
130*94c83a24SMaria Kustova            ['>Q', 48, 0, 'refcount_table_offset'],
131*94c83a24SMaria Kustova            ['>I', 56, 0, 'refcount_table_clusters'],
132*94c83a24SMaria Kustova            ['>I', 60, 0, 'nb_snapshots'],
133*94c83a24SMaria Kustova            ['>Q', 64, 0, 'snapshots_offset'],
134*94c83a24SMaria Kustova            ['>Q', 72, 0, 'incompatible_features'],
135*94c83a24SMaria Kustova            ['>Q', 80, 0, 'compatible_features'],
136*94c83a24SMaria Kustova            ['>Q', 88, 0, 'autoclear_features'],
137*94c83a24SMaria Kustova            # Only refcount_order = 4 is supported by current (07.2014)
138*94c83a24SMaria Kustova            # implementation of QEMU
139*94c83a24SMaria Kustova            ['>I', 96, 4, 'refcount_order'],
140*94c83a24SMaria Kustova            ['>I', 100, 0, 'header_length']
141*94c83a24SMaria Kustova        ]
142*94c83a24SMaria Kustova        self.header = FieldsList(meta_header)
143*94c83a24SMaria Kustova
144*94c83a24SMaria Kustova        if self.header['version'][0].value == 2:
145*94c83a24SMaria Kustova            self.header['header_length'][0].value = 72
146*94c83a24SMaria Kustova        else:
147*94c83a24SMaria Kustova            self.header['incompatible_features'][0].value = \
148*94c83a24SMaria Kustova                                                        random.getrandbits(2)
149*94c83a24SMaria Kustova            self.header['compatible_features'][0].value = random.getrandbits(1)
150*94c83a24SMaria Kustova            self.header['header_length'][0].value = 104
151*94c83a24SMaria Kustova        # Extensions start at the header last field offset and the field size
152*94c83a24SMaria Kustova        self.ext_offset = struct.calcsize(
153*94c83a24SMaria Kustova            self.header['header_length'][0].fmt) + \
154*94c83a24SMaria Kustova            self.header['header_length'][0].offset
155*94c83a24SMaria Kustova        end_of_extension_area_len = 2 * UINT32_S
156*94c83a24SMaria Kustova        free_space = self.cluster_size - self.ext_offset - \
157*94c83a24SMaria Kustova                     end_of_extension_area_len
158*94c83a24SMaria Kustova        # If the backing file name specified and there is enough space for it
159*94c83a24SMaria Kustova        # in the first cluster, then it's placed in the very end of the first
160*94c83a24SMaria Kustova        # cluster.
161*94c83a24SMaria Kustova        if (backing_file_name is not None) and \
162*94c83a24SMaria Kustova           (free_space >= len(backing_file_name)):
163*94c83a24SMaria Kustova            self.header['backing_file_size'][0].value = len(backing_file_name)
164*94c83a24SMaria Kustova            self.header['backing_file_offset'][0].value = \
165*94c83a24SMaria Kustova                                    self.cluster_size - len(backing_file_name)
166*94c83a24SMaria Kustova
167*94c83a24SMaria Kustova    def set_backing_file_name(self, backing_file_name=None):
168*94c83a24SMaria Kustova        """Add the name of the backing file at the offset specified
169*94c83a24SMaria Kustova        in the header.
170*94c83a24SMaria Kustova        """
171*94c83a24SMaria Kustova        if (backing_file_name is not None) and \
172*94c83a24SMaria Kustova           (not self.header['backing_file_offset'][0].value == 0):
173*94c83a24SMaria Kustova            data_len = len(backing_file_name)
174*94c83a24SMaria Kustova            data_fmt = '>' + str(data_len) + 's'
175*94c83a24SMaria Kustova            self.backing_file_name = FieldsList([
176*94c83a24SMaria Kustova                [data_fmt, self.header['backing_file_offset'][0].value,
177*94c83a24SMaria Kustova                 backing_file_name, 'bf_name']
178*94c83a24SMaria Kustova            ])
179*94c83a24SMaria Kustova
180*94c83a24SMaria Kustova    def set_backing_file_format(self, backing_file_fmt=None):
181*94c83a24SMaria Kustova        """Generate the header extension for the backing file format."""
182*94c83a24SMaria Kustova        if backing_file_fmt is not None:
183*94c83a24SMaria Kustova            # Calculation of the free space available in the first cluster
184*94c83a24SMaria Kustova            end_of_extension_area_len = 2 * UINT32_S
185*94c83a24SMaria Kustova            high_border = (self.header['backing_file_offset'][0].value or
186*94c83a24SMaria Kustova                           (self.cluster_size - 1)) - \
187*94c83a24SMaria Kustova                end_of_extension_area_len
188*94c83a24SMaria Kustova            free_space = high_border - self.ext_offset
189*94c83a24SMaria Kustova            ext_size = 2 * UINT32_S + ((len(backing_file_fmt) + 7) & ~7)
190*94c83a24SMaria Kustova
191*94c83a24SMaria Kustova            if free_space >= ext_size:
192*94c83a24SMaria Kustova                ext_data_len = len(backing_file_fmt)
193*94c83a24SMaria Kustova                ext_data_fmt = '>' + str(ext_data_len) + 's'
194*94c83a24SMaria Kustova                ext_padding_len = 7 - (ext_data_len - 1) % 8
195*94c83a24SMaria Kustova                self.backing_file_format = FieldsList([
196*94c83a24SMaria Kustova                    ['>I', self.ext_offset, 0xE2792ACA, 'ext_magic'],
197*94c83a24SMaria Kustova                    ['>I', self.ext_offset + UINT32_S, ext_data_len,
198*94c83a24SMaria Kustova                     'ext_length'],
199*94c83a24SMaria Kustova                    [ext_data_fmt, self.ext_offset + UINT32_S * 2,
200*94c83a24SMaria Kustova                     backing_file_fmt, 'bf_format']
201*94c83a24SMaria Kustova                ])
202*94c83a24SMaria Kustova                self.ext_offset = \
203*94c83a24SMaria Kustova                        struct.calcsize(
204*94c83a24SMaria Kustova                            self.backing_file_format['bf_format'][0].fmt) + \
205*94c83a24SMaria Kustova                        ext_padding_len + \
206*94c83a24SMaria Kustova                        self.backing_file_format['bf_format'][0].offset
207*94c83a24SMaria Kustova
208*94c83a24SMaria Kustova    def create_feature_name_table(self):
209*94c83a24SMaria Kustova        """Generate a random header extension for names of features used in
210*94c83a24SMaria Kustova        the image.
211*94c83a24SMaria Kustova        """
212*94c83a24SMaria Kustova        def gen_feat_ids():
213*94c83a24SMaria Kustova            """Return random feature type and feature bit."""
214*94c83a24SMaria Kustova            return (random.randint(0, 2), random.randint(0, 63))
215*94c83a24SMaria Kustova
216*94c83a24SMaria Kustova        end_of_extension_area_len = 2 * UINT32_S
217*94c83a24SMaria Kustova        high_border = (self.header['backing_file_offset'][0].value or
218*94c83a24SMaria Kustova                       (self.cluster_size - 1)) - \
219*94c83a24SMaria Kustova            end_of_extension_area_len
220*94c83a24SMaria Kustova        free_space = high_border - self.ext_offset
221*94c83a24SMaria Kustova        # Sum of sizes of 'magic' and 'length' header extension fields
222*94c83a24SMaria Kustova        ext_header_len = 2 * UINT32_S
223*94c83a24SMaria Kustova        fnt_entry_size = 6 * UINT64_S
224*94c83a24SMaria Kustova        num_fnt_entries = min(10, (free_space - ext_header_len) /
225*94c83a24SMaria Kustova                              fnt_entry_size)
226*94c83a24SMaria Kustova        if not num_fnt_entries == 0:
227*94c83a24SMaria Kustova            feature_tables = []
228*94c83a24SMaria Kustova            feature_ids = []
229*94c83a24SMaria Kustova            inner_offset = self.ext_offset + ext_header_len
230*94c83a24SMaria Kustova            feat_name = 'some cool feature'
231*94c83a24SMaria Kustova            while len(feature_tables) < num_fnt_entries * 3:
232*94c83a24SMaria Kustova                feat_type, feat_bit = gen_feat_ids()
233*94c83a24SMaria Kustova                # Remove duplicates
234*94c83a24SMaria Kustova                while (feat_type, feat_bit) in feature_ids:
235*94c83a24SMaria Kustova                    feat_type, feat_bit = gen_feat_ids()
236*94c83a24SMaria Kustova                feature_ids.append((feat_type, feat_bit))
237*94c83a24SMaria Kustova                feat_fmt = '>' + str(len(feat_name)) + 's'
238*94c83a24SMaria Kustova                feature_tables += [['B', inner_offset,
239*94c83a24SMaria Kustova                                    feat_type, 'feature_type'],
240*94c83a24SMaria Kustova                                   ['B', inner_offset + 1, feat_bit,
241*94c83a24SMaria Kustova                                    'feature_bit_number'],
242*94c83a24SMaria Kustova                                   [feat_fmt, inner_offset + 2,
243*94c83a24SMaria Kustova                                    feat_name, 'feature_name']
244*94c83a24SMaria Kustova                ]
245*94c83a24SMaria Kustova                inner_offset += fnt_entry_size
246*94c83a24SMaria Kustova            # No padding for the extension is necessary, because
247*94c83a24SMaria Kustova            # the extension length is multiple of 8
248*94c83a24SMaria Kustova            self.feature_name_table = FieldsList([
249*94c83a24SMaria Kustova                ['>I', self.ext_offset, 0x6803f857, 'ext_magic'],
250*94c83a24SMaria Kustova                # One feature table contains 3 fields and takes 48 bytes
251*94c83a24SMaria Kustova                ['>I', self.ext_offset + UINT32_S,
252*94c83a24SMaria Kustova                 len(feature_tables) / 3 * 48, 'ext_length']
253*94c83a24SMaria Kustova            ] + feature_tables)
254*94c83a24SMaria Kustova            self.ext_offset = inner_offset
255*94c83a24SMaria Kustova
256*94c83a24SMaria Kustova    def set_end_of_extension_area(self):
257*94c83a24SMaria Kustova        """Generate a mandatory header extension marking end of header
258*94c83a24SMaria Kustova        extensions.
259*94c83a24SMaria Kustova        """
260*94c83a24SMaria Kustova        self.end_of_extension_area = FieldsList([
261*94c83a24SMaria Kustova            ['>I', self.ext_offset, 0, 'ext_magic'],
262*94c83a24SMaria Kustova            ['>I', self.ext_offset + UINT32_S, 0, 'ext_length']
263*94c83a24SMaria Kustova        ])
264*94c83a24SMaria Kustova
265*94c83a24SMaria Kustova    def create_l_structures(self):
266*94c83a24SMaria Kustova        """Generate random valid L1 and L2 tables."""
267*94c83a24SMaria Kustova        def create_l2_entry(host, guest, l2_cluster):
268*94c83a24SMaria Kustova            """Generate one L2 entry."""
269*94c83a24SMaria Kustova            offset = l2_cluster * self.cluster_size
270*94c83a24SMaria Kustova            l2_size = self.cluster_size / UINT64_S
271*94c83a24SMaria Kustova            entry_offset = offset + UINT64_S * (guest % l2_size)
272*94c83a24SMaria Kustova            cluster_descriptor = host * self.cluster_size
273*94c83a24SMaria Kustova            if not self.header['version'][0].value == 2:
274*94c83a24SMaria Kustova                cluster_descriptor += random.randint(0, 1)
275*94c83a24SMaria Kustova            # While snapshots are not supported, bit #63 = 1
276*94c83a24SMaria Kustova            # Compressed clusters are not supported => bit #62 = 0
277*94c83a24SMaria Kustova            entry_val = (1 << 63) + cluster_descriptor
278*94c83a24SMaria Kustova            return ['>Q', entry_offset, entry_val, 'l2_entry']
279*94c83a24SMaria Kustova
280*94c83a24SMaria Kustova        def create_l1_entry(l2_cluster, l1_offset, guest):
281*94c83a24SMaria Kustova            """Generate one L1 entry."""
282*94c83a24SMaria Kustova            l2_size = self.cluster_size / UINT64_S
283*94c83a24SMaria Kustova            entry_offset = l1_offset + UINT64_S * (guest / l2_size)
284*94c83a24SMaria Kustova            # While snapshots are not supported bit #63 = 1
285*94c83a24SMaria Kustova            entry_val = (1 << 63) + l2_cluster * self.cluster_size
286*94c83a24SMaria Kustova            return ['>Q', entry_offset, entry_val, 'l1_entry']
287*94c83a24SMaria Kustova
288*94c83a24SMaria Kustova        if len(self.data_clusters) == 0:
289*94c83a24SMaria Kustova            # All metadata for an empty guest image needs 4 clusters:
290*94c83a24SMaria Kustova            # header, rfc table, rfc block, L1 table.
291*94c83a24SMaria Kustova            # Header takes cluster #0, other clusters ##1-3 can be used
292*94c83a24SMaria Kustova            l1_offset = random.randint(1, 3) * self.cluster_size
293*94c83a24SMaria Kustova            l1 = [['>Q', l1_offset, 0, 'l1_entry']]
294*94c83a24SMaria Kustova            l2 = []
295*94c83a24SMaria Kustova        else:
296*94c83a24SMaria Kustova            meta_data = self._get_metadata()
297*94c83a24SMaria Kustova            guest_clusters = random.sample(range(self.image_size /
298*94c83a24SMaria Kustova                                                 self.cluster_size),
299*94c83a24SMaria Kustova                                           len(self.data_clusters))
300*94c83a24SMaria Kustova            # Number of entries in a L1/L2 table
301*94c83a24SMaria Kustova            l_size = self.cluster_size / UINT64_S
302*94c83a24SMaria Kustova            # Number of clusters necessary for L1 table
303*94c83a24SMaria Kustova            l1_size = int(ceil((max(guest_clusters) + 1) / float(l_size**2)))
304*94c83a24SMaria Kustova            l1_start = self._get_adjacent_clusters(self.data_clusters |
305*94c83a24SMaria Kustova                                                   meta_data, l1_size)
306*94c83a24SMaria Kustova            meta_data |= set(range(l1_start, l1_start + l1_size))
307*94c83a24SMaria Kustova            l1_offset = l1_start * self.cluster_size
308*94c83a24SMaria Kustova            # Indices of L2 tables
309*94c83a24SMaria Kustova            l2_ids = []
310*94c83a24SMaria Kustova            # Host clusters allocated for L2 tables
311*94c83a24SMaria Kustova            l2_clusters = []
312*94c83a24SMaria Kustova            # L1 entries
313*94c83a24SMaria Kustova            l1 = []
314*94c83a24SMaria Kustova            # L2 entries
315*94c83a24SMaria Kustova            l2 = []
316*94c83a24SMaria Kustova            for host, guest in zip(self.data_clusters, guest_clusters):
317*94c83a24SMaria Kustova                l2_id = guest / l_size
318*94c83a24SMaria Kustova                if l2_id not in l2_ids:
319*94c83a24SMaria Kustova                    l2_ids.append(l2_id)
320*94c83a24SMaria Kustova                    l2_clusters.append(self._get_adjacent_clusters(
321*94c83a24SMaria Kustova                        self.data_clusters | meta_data | set(l2_clusters),
322*94c83a24SMaria Kustova                        1))
323*94c83a24SMaria Kustova                    l1.append(create_l1_entry(l2_clusters[-1], l1_offset,
324*94c83a24SMaria Kustova                                              guest))
325*94c83a24SMaria Kustova                l2.append(create_l2_entry(host, guest,
326*94c83a24SMaria Kustova                                          l2_clusters[l2_ids.index(l2_id)]))
327*94c83a24SMaria Kustova        self.l2_tables = FieldsList(l2)
328*94c83a24SMaria Kustova        self.l1_table = FieldsList(l1)
329*94c83a24SMaria Kustova        self.header['l1_size'][0].value = int(ceil(UINT64_S * self.image_size /
330*94c83a24SMaria Kustova                                                float(self.cluster_size**2)))
331*94c83a24SMaria Kustova        self.header['l1_table_offset'][0].value = l1_offset
332*94c83a24SMaria Kustova
333*94c83a24SMaria Kustova    def fuzz(self, fields_to_fuzz=None):
334*94c83a24SMaria Kustova        """Fuzz an image by corrupting values of a random subset of its fields.
335*94c83a24SMaria Kustova
336*94c83a24SMaria Kustova        Without parameters the method fuzzes an entire image.
337*94c83a24SMaria Kustova
338*94c83a24SMaria Kustova        If 'fields_to_fuzz' is specified then only fields in this list will be
339*94c83a24SMaria Kustova        fuzzed. 'fields_to_fuzz' can contain both individual fields and more
340*94c83a24SMaria Kustova        general image elements as a header or tables.
341*94c83a24SMaria Kustova
342*94c83a24SMaria Kustova        In the first case the field will be fuzzed always.
343*94c83a24SMaria Kustova        In the second a random subset of fields will be selected and fuzzed.
344*94c83a24SMaria Kustova        """
345*94c83a24SMaria Kustova        def coin():
346*94c83a24SMaria Kustova            """Return boolean value proportional to a portion of fields to be
347*94c83a24SMaria Kustova            fuzzed.
348*94c83a24SMaria Kustova            """
349*94c83a24SMaria Kustova            return random.random() < self.bias
350*94c83a24SMaria Kustova
351*94c83a24SMaria Kustova        if fields_to_fuzz is None:
352*94c83a24SMaria Kustova            for field in self:
353*94c83a24SMaria Kustova                if coin():
354*94c83a24SMaria Kustova                    field.value = getattr(fuzz, field.name)(field.value)
355*94c83a24SMaria Kustova        else:
356*94c83a24SMaria Kustova            for item in fields_to_fuzz:
357*94c83a24SMaria Kustova                if len(item) == 1:
358*94c83a24SMaria Kustova                    for field in getattr(self, item[0]):
359*94c83a24SMaria Kustova                        if coin():
360*94c83a24SMaria Kustova                            field.value = getattr(fuzz,
361*94c83a24SMaria Kustova                                                  field.name)(field.value)
362*94c83a24SMaria Kustova                else:
363*94c83a24SMaria Kustova                    # If fields with the requested name were not generated
364*94c83a24SMaria Kustova                    # getattr(self, item[0])[item[1]] returns an empty list
365*94c83a24SMaria Kustova                    for field in getattr(self, item[0])[item[1]]:
366*94c83a24SMaria Kustova                        field.value = getattr(fuzz, field.name)(field.value)
367*94c83a24SMaria Kustova
368*94c83a24SMaria Kustova    def write(self, filename):
369*94c83a24SMaria Kustova        """Write an entire image to the file."""
370*94c83a24SMaria Kustova        image_file = open(filename, 'w')
371*94c83a24SMaria Kustova        for field in self:
372*94c83a24SMaria Kustova            image_file.seek(field.offset)
373*94c83a24SMaria Kustova            image_file.write(struct.pack(field.fmt, field.value))
374*94c83a24SMaria Kustova
375*94c83a24SMaria Kustova        for cluster in sorted(self.data_clusters):
376*94c83a24SMaria Kustova            image_file.seek(cluster * self.cluster_size)
377*94c83a24SMaria Kustova            image_file.write(urandom(self.cluster_size))
378*94c83a24SMaria Kustova
379*94c83a24SMaria Kustova        # Align the real image size to the cluster size
380*94c83a24SMaria Kustova        image_file.seek(0, 2)
381*94c83a24SMaria Kustova        size = image_file.tell()
382*94c83a24SMaria Kustova        rounded = (size + self.cluster_size - 1) & ~(self.cluster_size - 1)
383*94c83a24SMaria Kustova        if rounded > size:
384*94c83a24SMaria Kustova            image_file.seek(rounded - 1)
385*94c83a24SMaria Kustova            image_file.write("\0")
386*94c83a24SMaria Kustova        image_file.close()
387*94c83a24SMaria Kustova
388e1232323SMaria Kustova    @staticmethod
389e1232323SMaria Kustova    def _size_params():
390e1232323SMaria Kustova        """Generate a random image size aligned to a random correct
391e1232323SMaria Kustova        cluster size.
392e1232323SMaria Kustova        """
393e1232323SMaria Kustova        cluster_bits = random.randrange(9, 21)
394e1232323SMaria Kustova        cluster_size = 1 << cluster_bits
395e1232323SMaria Kustova        img_size = random.randrange(0, MAX_IMAGE_SIZE + 1, cluster_size)
396e1232323SMaria Kustova        return (cluster_bits, img_size)
397e1232323SMaria Kustova
398e1232323SMaria Kustova    @staticmethod
39938eb193bSMaria Kustova    def _get_available_clusters(used, number):
40038eb193bSMaria Kustova        """Return a set of indices of not allocated clusters.
40138eb193bSMaria Kustova
40238eb193bSMaria Kustova        'used' contains indices of currently allocated clusters.
40338eb193bSMaria Kustova        All clusters that cannot be allocated between 'used' clusters will have
40438eb193bSMaria Kustova        indices appended to the end of 'used'.
40538eb193bSMaria Kustova        """
40638eb193bSMaria Kustova        append_id = max(used) + 1
40738eb193bSMaria Kustova        free = set(range(1, append_id)) - used
40838eb193bSMaria Kustova        if len(free) >= number:
40938eb193bSMaria Kustova            return set(random.sample(free, number))
41038eb193bSMaria Kustova        else:
41138eb193bSMaria Kustova            return free | set(range(append_id, append_id + number - len(free)))
41238eb193bSMaria Kustova
41338eb193bSMaria Kustova    @staticmethod
41438eb193bSMaria Kustova    def _get_adjacent_clusters(used, size):
41538eb193bSMaria Kustova        """Return an index of the first cluster in the sequence of free ones.
41638eb193bSMaria Kustova
41738eb193bSMaria Kustova        'used' contains indices of currently allocated clusters. 'size' is the
41838eb193bSMaria Kustova        length of the sequence of free clusters.
41938eb193bSMaria Kustova        If the sequence of 'size' is not available between 'used' clusters, its
42038eb193bSMaria Kustova        first index will be append to the end of 'used'.
42138eb193bSMaria Kustova        """
42238eb193bSMaria Kustova        def get_cluster_id(lst, length):
42338eb193bSMaria Kustova            """Return the first index of the sequence of the specified length
42438eb193bSMaria Kustova            or None if the sequence cannot be inserted in the list.
42538eb193bSMaria Kustova            """
42638eb193bSMaria Kustova            if len(lst) != 0:
42738eb193bSMaria Kustova                pairs = []
42838eb193bSMaria Kustova                pair = (lst[0], 1)
42938eb193bSMaria Kustova                for i in range(1, len(lst)):
43038eb193bSMaria Kustova                    if lst[i] == lst[i-1] + 1:
43138eb193bSMaria Kustova                        pair = (lst[i], pair[1] + 1)
43238eb193bSMaria Kustova                    else:
43338eb193bSMaria Kustova                        pairs.append(pair)
43438eb193bSMaria Kustova                        pair = (lst[i], 1)
43538eb193bSMaria Kustova                pairs.append(pair)
43638eb193bSMaria Kustova                random.shuffle(pairs)
43738eb193bSMaria Kustova                for x, s in pairs:
43838eb193bSMaria Kustova                    if s >= length:
43938eb193bSMaria Kustova                        return x - length + 1
44038eb193bSMaria Kustova            return None
44138eb193bSMaria Kustova
44238eb193bSMaria Kustova        append_id = max(used) + 1
44338eb193bSMaria Kustova        free = list(set(range(1, append_id)) - used)
44438eb193bSMaria Kustova        idx = get_cluster_id(free, size)
44538eb193bSMaria Kustova        if idx is None:
44638eb193bSMaria Kustova            return append_id
44738eb193bSMaria Kustova        else:
44838eb193bSMaria Kustova            return idx
44938eb193bSMaria Kustova
45038eb193bSMaria Kustova    @staticmethod
45138eb193bSMaria Kustova    def _alloc_data(img_size, cluster_size):
45238eb193bSMaria Kustova        """Return a set of random indices of clusters allocated for guest data.
45338eb193bSMaria Kustova        """
45438eb193bSMaria Kustova        num_of_cls = img_size/cluster_size
45538eb193bSMaria Kustova        return set(random.sample(range(1, num_of_cls + 1),
45638eb193bSMaria Kustova                                 random.randint(0, num_of_cls)))
45738eb193bSMaria Kustova
458*94c83a24SMaria Kustova    def _get_metadata(self):
459*94c83a24SMaria Kustova        """Return indices of clusters allocated for image metadata."""
460*94c83a24SMaria Kustova        ids = set()
461*94c83a24SMaria Kustova        for x in self:
462*94c83a24SMaria Kustova            ids.add(x.offset/self.cluster_size)
463*94c83a24SMaria Kustova        return ids
464e1232323SMaria Kustova
465e1232323SMaria Kustova
466e1232323SMaria Kustovadef create_image(test_img_path, backing_file_name=None, backing_file_fmt=None,
467e1232323SMaria Kustova                 fields_to_fuzz=None):
468e1232323SMaria Kustova    """Create a fuzzed image and write it to the specified file."""
469*94c83a24SMaria Kustova    image = Image(backing_file_name)
470*94c83a24SMaria Kustova    image.set_backing_file_format(backing_file_fmt)
471*94c83a24SMaria Kustova    image.create_feature_name_table()
472*94c83a24SMaria Kustova    image.set_end_of_extension_area()
473*94c83a24SMaria Kustova    image.create_l_structures()
474e1232323SMaria Kustova    image.fuzz(fields_to_fuzz)
475e1232323SMaria Kustova    image.write(test_img_path)
476e1232323SMaria Kustova    return image.image_size
477