qemu

FORK: QEMU emulator
git clone https://git.neptards.moe/neptards/qemu.git
Log | Files | Refs | Submodules | LICENSE

layout.py (27123B)


      1 # Generator of fuzzed qcow2 images
      2 #
      3 # Copyright (C) 2014 Maria Kustova <maria.k@catit.be>
      4 #
      5 # This program is free software: you can redistribute it and/or modify
      6 # it under the terms of the GNU General Public License as published by
      7 # the Free Software Foundation, either version 2 of the License, or
      8 # (at your option) any later version.
      9 #
     10 # This program is distributed in the hope that it will be useful,
     11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 # GNU General Public License for more details.
     14 #
     15 # You should have received a copy of the GNU General Public License
     16 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 #
     18 
     19 import random
     20 import struct
     21 from . import fuzz
     22 from math import ceil
     23 from os import urandom
     24 from itertools import chain
     25 
     26 MAX_IMAGE_SIZE = 10 * (1 << 20)
     27 # Standard sizes
     28 UINT32_S = 4
     29 UINT64_S = 8
     30 
     31 
     32 class Field(object):
     33 
     34     """Atomic image element (field).
     35 
     36     The class represents an image field as quadruple of a data format
     37     of value necessary for its packing to binary form, an offset from
     38     the beginning of the image, a value and a name.
     39 
     40     The field can be iterated as a list [format, offset, value, name].
     41     """
     42 
     43     __slots__ = ('fmt', 'offset', 'value', 'name')
     44 
     45     def __init__(self, fmt, offset, val, name):
     46         self.fmt = fmt
     47         self.offset = offset
     48         self.value = val
     49         self.name = name
     50 
     51     def __iter__(self):
     52         return iter([self.fmt, self.offset, self.value, self.name])
     53 
     54     def __repr__(self):
     55         return "Field(fmt=%r, offset=%r, value=%r, name=%r)" % \
     56             (self.fmt, self.offset, self.value, self.name)
     57 
     58 
     59 class FieldsList(object):
     60 
     61     """List of fields.
     62 
     63     The class allows access to a field in the list by its name.
     64     """
     65 
     66     def __init__(self, meta_data=None):
     67         if meta_data is None:
     68             self.data = []
     69         else:
     70             self.data = [Field(*f)
     71                          for f in meta_data]
     72 
     73     def __getitem__(self, name):
     74         return [x for x in self.data if x.name == name]
     75 
     76     def __iter__(self):
     77         return iter(self.data)
     78 
     79     def __len__(self):
     80         return len(self.data)
     81 
     82 
     83 class Image(object):
     84 
     85     """ Qcow2 image object.
     86 
     87     This class allows to create qcow2 images with random valid structures and
     88     values, fuzz them via external qcow2.fuzz module and write the result to
     89     a file.
     90     """
     91 
     92     def __init__(self, backing_file_name=None):
     93         """Create a random valid qcow2 image with the correct header and stored
     94         backing file name.
     95         """
     96         cluster_bits, self.image_size = self._size_params()
     97         self.cluster_size = 1 << cluster_bits
     98         self.header = FieldsList()
     99         self.backing_file_name = FieldsList()
    100         self.backing_file_format = FieldsList()
    101         self.feature_name_table = FieldsList()
    102         self.end_of_extension_area = FieldsList()
    103         self.l2_tables = FieldsList()
    104         self.l1_table = FieldsList()
    105         self.refcount_table = FieldsList()
    106         self.refcount_blocks = FieldsList()
    107         self.ext_offset = 0
    108         self.create_header(cluster_bits, backing_file_name)
    109         self.set_backing_file_name(backing_file_name)
    110         self.data_clusters = self._alloc_data(self.image_size,
    111                                               self.cluster_size)
    112         # Percentage of fields will be fuzzed
    113         self.bias = random.uniform(0.2, 0.5)
    114 
    115     def __iter__(self):
    116         return chain(self.header, self.backing_file_format,
    117                      self.feature_name_table, self.end_of_extension_area,
    118                      self.backing_file_name, self.l1_table, self.l2_tables,
    119                      self.refcount_table, self.refcount_blocks)
    120 
    121     def create_header(self, cluster_bits, backing_file_name=None):
    122         """Generate a random valid header."""
    123         meta_header = [
    124             ['>4s', 0, b"QFI\xfb", 'magic'],
    125             ['>I', 4, random.randint(2, 3), 'version'],
    126             ['>Q', 8, 0, 'backing_file_offset'],
    127             ['>I', 16, 0, 'backing_file_size'],
    128             ['>I', 20, cluster_bits, 'cluster_bits'],
    129             ['>Q', 24, self.image_size, 'size'],
    130             ['>I', 32, 0, 'crypt_method'],
    131             ['>I', 36, 0, 'l1_size'],
    132             ['>Q', 40, 0, 'l1_table_offset'],
    133             ['>Q', 48, 0, 'refcount_table_offset'],
    134             ['>I', 56, 0, 'refcount_table_clusters'],
    135             ['>I', 60, 0, 'nb_snapshots'],
    136             ['>Q', 64, 0, 'snapshots_offset'],
    137             ['>Q', 72, 0, 'incompatible_features'],
    138             ['>Q', 80, 0, 'compatible_features'],
    139             ['>Q', 88, 0, 'autoclear_features'],
    140             # Only refcount_order = 4 is supported by current (07.2014)
    141             # implementation of QEMU
    142             ['>I', 96, 4, 'refcount_order'],
    143             ['>I', 100, 0, 'header_length']
    144         ]
    145         self.header = FieldsList(meta_header)
    146 
    147         if self.header['version'][0].value == 2:
    148             self.header['header_length'][0].value = 72
    149         else:
    150             self.header['incompatible_features'][0].value = \
    151                                                         random.getrandbits(2)
    152             self.header['compatible_features'][0].value = random.getrandbits(1)
    153             self.header['header_length'][0].value = 104
    154         # Extensions start at the header last field offset and the field size
    155         self.ext_offset = struct.calcsize(
    156             self.header['header_length'][0].fmt) + \
    157             self.header['header_length'][0].offset
    158         end_of_extension_area_len = 2 * UINT32_S
    159         free_space = self.cluster_size - self.ext_offset - \
    160                      end_of_extension_area_len
    161         # If the backing file name specified and there is enough space for it
    162         # in the first cluster, then it's placed in the very end of the first
    163         # cluster.
    164         if (backing_file_name is not None) and \
    165            (free_space >= len(backing_file_name)):
    166             self.header['backing_file_size'][0].value = len(backing_file_name)
    167             self.header['backing_file_offset'][0].value = \
    168                                     self.cluster_size - len(backing_file_name)
    169 
    170     def set_backing_file_name(self, backing_file_name=None):
    171         """Add the name of the backing file at the offset specified
    172         in the header.
    173         """
    174         if (backing_file_name is not None) and \
    175            (not self.header['backing_file_offset'][0].value == 0):
    176             data_len = len(backing_file_name)
    177             data_fmt = '>' + str(data_len) + 's'
    178             self.backing_file_name = FieldsList([
    179                 [data_fmt, self.header['backing_file_offset'][0].value,
    180                  backing_file_name, 'bf_name']
    181             ])
    182 
    183     def set_backing_file_format(self, backing_file_fmt=None):
    184         """Generate the header extension for the backing file format."""
    185         if backing_file_fmt is not None:
    186             # Calculation of the free space available in the first cluster
    187             end_of_extension_area_len = 2 * UINT32_S
    188             high_border = (self.header['backing_file_offset'][0].value or
    189                            (self.cluster_size - 1)) - \
    190                 end_of_extension_area_len
    191             free_space = high_border - self.ext_offset
    192             ext_size = 2 * UINT32_S + ((len(backing_file_fmt) + 7) & ~7)
    193 
    194             if free_space >= ext_size:
    195                 ext_data_len = len(backing_file_fmt)
    196                 ext_data_fmt = '>' + str(ext_data_len) + 's'
    197                 ext_padding_len = 7 - (ext_data_len - 1) % 8
    198                 self.backing_file_format = FieldsList([
    199                     ['>I', self.ext_offset, 0xE2792ACA, 'ext_magic'],
    200                     ['>I', self.ext_offset + UINT32_S, ext_data_len,
    201                      'ext_length'],
    202                     [ext_data_fmt, self.ext_offset + UINT32_S * 2,
    203                      backing_file_fmt, 'bf_format']
    204                 ])
    205                 self.ext_offset = \
    206                         struct.calcsize(
    207                             self.backing_file_format['bf_format'][0].fmt) + \
    208                         ext_padding_len + \
    209                         self.backing_file_format['bf_format'][0].offset
    210 
    211     def create_feature_name_table(self):
    212         """Generate a random header extension for names of features used in
    213         the image.
    214         """
    215         def gen_feat_ids():
    216             """Return random feature type and feature bit."""
    217             return (random.randint(0, 2), random.randint(0, 63))
    218 
    219         end_of_extension_area_len = 2 * UINT32_S
    220         high_border = (self.header['backing_file_offset'][0].value or
    221                        (self.cluster_size - 1)) - \
    222             end_of_extension_area_len
    223         free_space = high_border - self.ext_offset
    224         # Sum of sizes of 'magic' and 'length' header extension fields
    225         ext_header_len = 2 * UINT32_S
    226         fnt_entry_size = 6 * UINT64_S
    227         num_fnt_entries = min(10, (free_space - ext_header_len) /
    228                               fnt_entry_size)
    229         if not num_fnt_entries == 0:
    230             feature_tables = []
    231             feature_ids = []
    232             inner_offset = self.ext_offset + ext_header_len
    233             feat_name = b'some cool feature'
    234             while len(feature_tables) < num_fnt_entries * 3:
    235                 feat_type, feat_bit = gen_feat_ids()
    236                 # Remove duplicates
    237                 while (feat_type, feat_bit) in feature_ids:
    238                     feat_type, feat_bit = gen_feat_ids()
    239                 feature_ids.append((feat_type, feat_bit))
    240                 feat_fmt = '>' + str(len(feat_name)) + 's'
    241                 feature_tables += [['B', inner_offset,
    242                                     feat_type, 'feature_type'],
    243                                    ['B', inner_offset + 1, feat_bit,
    244                                     'feature_bit_number'],
    245                                    [feat_fmt, inner_offset + 2,
    246                                     feat_name, 'feature_name']
    247                 ]
    248                 inner_offset += fnt_entry_size
    249             # No padding for the extension is necessary, because
    250             # the extension length is multiple of 8
    251             self.feature_name_table = FieldsList([
    252                 ['>I', self.ext_offset, 0x6803f857, 'ext_magic'],
    253                 # One feature table contains 3 fields and takes 48 bytes
    254                 ['>I', self.ext_offset + UINT32_S,
    255                  len(feature_tables) // 3 * 48, 'ext_length']
    256             ] + feature_tables)
    257             self.ext_offset = inner_offset
    258 
    259     def set_end_of_extension_area(self):
    260         """Generate a mandatory header extension marking end of header
    261         extensions.
    262         """
    263         self.end_of_extension_area = FieldsList([
    264             ['>I', self.ext_offset, 0, 'ext_magic'],
    265             ['>I', self.ext_offset + UINT32_S, 0, 'ext_length']
    266         ])
    267 
    268     def create_l_structures(self):
    269         """Generate random valid L1 and L2 tables."""
    270         def create_l2_entry(host, guest, l2_cluster):
    271             """Generate one L2 entry."""
    272             offset = l2_cluster * self.cluster_size
    273             l2_size = self.cluster_size // UINT64_S
    274             entry_offset = offset + UINT64_S * (guest % l2_size)
    275             cluster_descriptor = host * self.cluster_size
    276             if not self.header['version'][0].value == 2:
    277                 cluster_descriptor += random.randint(0, 1)
    278             # While snapshots are not supported, bit #63 = 1
    279             # Compressed clusters are not supported => bit #62 = 0
    280             entry_val = (1 << 63) + cluster_descriptor
    281             return ['>Q', entry_offset, entry_val, 'l2_entry']
    282 
    283         def create_l1_entry(l2_cluster, l1_offset, guest):
    284             """Generate one L1 entry."""
    285             l2_size = self.cluster_size // UINT64_S
    286             entry_offset = l1_offset + UINT64_S * (guest // l2_size)
    287             # While snapshots are not supported bit #63 = 1
    288             entry_val = (1 << 63) + l2_cluster * self.cluster_size
    289             return ['>Q', entry_offset, entry_val, 'l1_entry']
    290 
    291         if len(self.data_clusters) == 0:
    292             # All metadata for an empty guest image needs 4 clusters:
    293             # header, rfc table, rfc block, L1 table.
    294             # Header takes cluster #0, other clusters ##1-3 can be used
    295             l1_offset = random.randint(1, 3) * self.cluster_size
    296             l1 = [['>Q', l1_offset, 0, 'l1_entry']]
    297             l2 = []
    298         else:
    299             meta_data = self._get_metadata()
    300             guest_clusters = random.sample(range(self.image_size //
    301                                                  self.cluster_size),
    302                                            len(self.data_clusters))
    303             # Number of entries in a L1/L2 table
    304             l_size = self.cluster_size // UINT64_S
    305             # Number of clusters necessary for L1 table
    306             l1_size = int(ceil((max(guest_clusters) + 1) / float(l_size**2)))
    307             l1_start = self._get_adjacent_clusters(self.data_clusters |
    308                                                    meta_data, l1_size)
    309             meta_data |= set(range(l1_start, l1_start + l1_size))
    310             l1_offset = l1_start * self.cluster_size
    311             # Indices of L2 tables
    312             l2_ids = []
    313             # Host clusters allocated for L2 tables
    314             l2_clusters = []
    315             # L1 entries
    316             l1 = []
    317             # L2 entries
    318             l2 = []
    319             for host, guest in zip(self.data_clusters, guest_clusters):
    320                 l2_id = guest // l_size
    321                 if l2_id not in l2_ids:
    322                     l2_ids.append(l2_id)
    323                     l2_clusters.append(self._get_adjacent_clusters(
    324                         self.data_clusters | meta_data | set(l2_clusters),
    325                         1))
    326                     l1.append(create_l1_entry(l2_clusters[-1], l1_offset,
    327                                               guest))
    328                 l2.append(create_l2_entry(host, guest,
    329                                           l2_clusters[l2_ids.index(l2_id)]))
    330         self.l2_tables = FieldsList(l2)
    331         self.l1_table = FieldsList(l1)
    332         self.header['l1_size'][0].value = int(ceil(UINT64_S * self.image_size /
    333                                                 float(self.cluster_size**2)))
    334         self.header['l1_table_offset'][0].value = l1_offset
    335 
    336     def create_refcount_structures(self):
    337         """Generate random refcount blocks and refcount table."""
    338         def allocate_rfc_blocks(data, size):
    339             """Return indices of clusters allocated for refcount blocks."""
    340             cluster_ids = set()
    341             diff = block_ids = set([x // size for x in data])
    342             while len(diff) != 0:
    343                 # Allocate all yet not allocated clusters
    344                 new = self._get_available_clusters(data | cluster_ids,
    345                                                    len(diff))
    346                 # Indices of new refcount blocks necessary to cover clusters
    347                 # in 'new'
    348                 diff = set([x // size for x in new]) - block_ids
    349                 cluster_ids |= new
    350                 block_ids |= diff
    351             return cluster_ids, block_ids
    352 
    353         def allocate_rfc_table(data, init_blocks, block_size):
    354             """Return indices of clusters allocated for the refcount table
    355             and updated indices of clusters allocated for blocks and indices
    356             of blocks.
    357             """
    358             blocks = set(init_blocks)
    359             clusters = set()
    360             # Number of entries in one cluster of the refcount table
    361             size = self.cluster_size // UINT64_S
    362             # Number of clusters necessary for the refcount table based on
    363             # the current number of refcount blocks
    364             table_size = int(ceil((max(blocks) + 1) / float(size)))
    365             # Index of the first cluster of the refcount table
    366             table_start = self._get_adjacent_clusters(data, table_size + 1)
    367             # Clusters allocated for the current length of the refcount table
    368             table_clusters = set(range(table_start, table_start + table_size))
    369             # Clusters allocated for the refcount table including
    370             # last optional one for potential l1 growth
    371             table_clusters_allocated = set(range(table_start, table_start +
    372                                                  table_size + 1))
    373             # New refcount blocks necessary for clusters occupied by the
    374             # refcount table
    375             diff = set([c // block_size for c in table_clusters]) - blocks
    376             blocks |= diff
    377             while len(diff) != 0:
    378                 # Allocate clusters for new refcount blocks
    379                 new = self._get_available_clusters((data | clusters) |
    380                                                    table_clusters_allocated,
    381                                                    len(diff))
    382                 # Indices of new refcount blocks necessary to cover
    383                 # clusters in 'new'
    384                 diff = set([x // block_size for x in new]) - blocks
    385                 clusters |= new
    386                 blocks |= diff
    387                 # Check if the refcount table needs one more cluster
    388                 if int(ceil((max(blocks) + 1) / float(size))) > table_size:
    389                     new_block_id = (table_start + table_size) // block_size
    390                     # Check if the additional table cluster needs
    391                     # one more refcount block
    392                     if new_block_id not in blocks:
    393                         diff.add(new_block_id)
    394                     table_clusters.add(table_start + table_size)
    395                     table_size += 1
    396             return table_clusters, blocks, clusters
    397 
    398         def create_table_entry(table_offset, block_cluster, block_size,
    399                                cluster):
    400             """Generate a refcount table entry."""
    401             offset = table_offset + UINT64_S * (cluster // block_size)
    402             return ['>Q', offset, block_cluster * self.cluster_size,
    403                     'refcount_table_entry']
    404 
    405         def create_block_entry(block_cluster, block_size, cluster):
    406             """Generate a list of entries for the current block."""
    407             entry_size = self.cluster_size // block_size
    408             offset = block_cluster * self.cluster_size
    409             entry_offset = offset + entry_size * (cluster % block_size)
    410             # While snapshots are not supported all refcounts are set to 1
    411             return ['>H', entry_offset, 1, 'refcount_block_entry']
    412         # Size of a block entry in bits
    413         refcount_bits = 1 << self.header['refcount_order'][0].value
    414         # Number of refcount entries per refcount block
    415         # Convert self.cluster_size from bytes to bits to have the same
    416         # base for the numerator and denominator
    417         block_size = self.cluster_size * 8 // refcount_bits
    418         meta_data = self._get_metadata()
    419         if len(self.data_clusters) == 0:
    420             # All metadata for an empty guest image needs 4 clusters:
    421             # header, rfc table, rfc block, L1 table.
    422             # Header takes cluster #0, other clusters ##1-3 can be used
    423             block_clusters = set([random.choice(list(set(range(1, 4)) -
    424                                                      meta_data))])
    425             block_ids = set([0])
    426             table_clusters = set([random.choice(list(set(range(1, 4)) -
    427                                                      meta_data -
    428                                                      block_clusters))])
    429         else:
    430             block_clusters, block_ids = \
    431                                 allocate_rfc_blocks(self.data_clusters |
    432                                                     meta_data, block_size)
    433             table_clusters, block_ids, new_clusters = \
    434                                     allocate_rfc_table(self.data_clusters |
    435                                                        meta_data |
    436                                                        block_clusters,
    437                                                        block_ids,
    438                                                        block_size)
    439             block_clusters |= new_clusters
    440 
    441         meta_data |= block_clusters | table_clusters
    442         table_offset = min(table_clusters) * self.cluster_size
    443         block_id = None
    444         # Clusters allocated for refcount blocks
    445         block_clusters = list(block_clusters)
    446         # Indices of refcount blocks
    447         block_ids = list(block_ids)
    448         # Refcount table entries
    449         rfc_table = []
    450         # Refcount entries
    451         rfc_blocks = []
    452 
    453         for cluster in sorted(self.data_clusters | meta_data):
    454             if cluster // block_size != block_id:
    455                 block_id = cluster // block_size
    456                 block_cluster = block_clusters[block_ids.index(block_id)]
    457                 rfc_table.append(create_table_entry(table_offset,
    458                                                     block_cluster,
    459                                                     block_size, cluster))
    460             rfc_blocks.append(create_block_entry(block_cluster, block_size,
    461                                                  cluster))
    462         self.refcount_table = FieldsList(rfc_table)
    463         self.refcount_blocks = FieldsList(rfc_blocks)
    464 
    465         self.header['refcount_table_offset'][0].value = table_offset
    466         self.header['refcount_table_clusters'][0].value = len(table_clusters)
    467 
    468     def fuzz(self, fields_to_fuzz=None):
    469         """Fuzz an image by corrupting values of a random subset of its fields.
    470 
    471         Without parameters the method fuzzes an entire image.
    472 
    473         If 'fields_to_fuzz' is specified then only fields in this list will be
    474         fuzzed. 'fields_to_fuzz' can contain both individual fields and more
    475         general image elements as a header or tables.
    476 
    477         In the first case the field will be fuzzed always.
    478         In the second a random subset of fields will be selected and fuzzed.
    479         """
    480         def coin():
    481             """Return boolean value proportional to a portion of fields to be
    482             fuzzed.
    483             """
    484             return random.random() < self.bias
    485 
    486         if fields_to_fuzz is None:
    487             for field in self:
    488                 if coin():
    489                     field.value = getattr(fuzz, field.name)(field.value)
    490         else:
    491             for item in fields_to_fuzz:
    492                 if len(item) == 1:
    493                     for field in getattr(self, item[0]):
    494                         if coin():
    495                             field.value = getattr(fuzz,
    496                                                   field.name)(field.value)
    497                 else:
    498                     # If fields with the requested name were not generated
    499                     # getattr(self, item[0])[item[1]] returns an empty list
    500                     for field in getattr(self, item[0])[item[1]]:
    501                         field.value = getattr(fuzz, field.name)(field.value)
    502 
    503     def write(self, filename):
    504         """Write an entire image to the file."""
    505         image_file = open(filename, 'wb')
    506         for field in self:
    507             image_file.seek(field.offset)
    508             image_file.write(struct.pack(field.fmt, field.value))
    509 
    510         for cluster in sorted(self.data_clusters):
    511             image_file.seek(cluster * self.cluster_size)
    512             image_file.write(urandom(self.cluster_size))
    513 
    514         # Align the real image size to the cluster size
    515         image_file.seek(0, 2)
    516         size = image_file.tell()
    517         rounded = (size + self.cluster_size - 1) & ~(self.cluster_size - 1)
    518         if rounded > size:
    519             image_file.seek(rounded - 1)
    520             image_file.write(b'\x00')
    521         image_file.close()
    522 
    523     @staticmethod
    524     def _size_params():
    525         """Generate a random image size aligned to a random correct
    526         cluster size.
    527         """
    528         cluster_bits = random.randrange(9, 21)
    529         cluster_size = 1 << cluster_bits
    530         img_size = random.randrange(0, MAX_IMAGE_SIZE + 1, cluster_size)
    531         return (cluster_bits, img_size)
    532 
    533     @staticmethod
    534     def _get_available_clusters(used, number):
    535         """Return a set of indices of not allocated clusters.
    536 
    537         'used' contains indices of currently allocated clusters.
    538         All clusters that cannot be allocated between 'used' clusters will have
    539         indices appended to the end of 'used'.
    540         """
    541         append_id = max(used) + 1
    542         free = set(range(1, append_id)) - used
    543         if len(free) >= number:
    544             return set(random.sample(free, number))
    545         else:
    546             return free | set(range(append_id, append_id + number - len(free)))
    547 
    548     @staticmethod
    549     def _get_adjacent_clusters(used, size):
    550         """Return an index of the first cluster in the sequence of free ones.
    551 
    552         'used' contains indices of currently allocated clusters. 'size' is the
    553         length of the sequence of free clusters.
    554         If the sequence of 'size' is not available between 'used' clusters, its
    555         first index will be append to the end of 'used'.
    556         """
    557         def get_cluster_id(lst, length):
    558             """Return the first index of the sequence of the specified length
    559             or None if the sequence cannot be inserted in the list.
    560             """
    561             if len(lst) != 0:
    562                 pairs = []
    563                 pair = (lst[0], 1)
    564                 for i in range(1, len(lst)):
    565                     if lst[i] == lst[i-1] + 1:
    566                         pair = (lst[i], pair[1] + 1)
    567                     else:
    568                         pairs.append(pair)
    569                         pair = (lst[i], 1)
    570                 pairs.append(pair)
    571                 random.shuffle(pairs)
    572                 for x, s in pairs:
    573                     if s >= length:
    574                         return x - length + 1
    575             return None
    576 
    577         append_id = max(used) + 1
    578         free = list(set(range(1, append_id)) - used)
    579         idx = get_cluster_id(free, size)
    580         if idx is None:
    581             return append_id
    582         else:
    583             return idx
    584 
    585     @staticmethod
    586     def _alloc_data(img_size, cluster_size):
    587         """Return a set of random indices of clusters allocated for guest data.
    588         """
    589         num_of_cls = img_size // cluster_size
    590         return set(random.sample(range(1, num_of_cls + 1),
    591                                  random.randint(0, num_of_cls)))
    592 
    593     def _get_metadata(self):
    594         """Return indices of clusters allocated for image metadata."""
    595         ids = set()
    596         for x in self:
    597             ids.add(x.offset // self.cluster_size)
    598         return ids
    599 
    600 
    601 def create_image(test_img_path, backing_file_name=None, backing_file_fmt=None,
    602                  fields_to_fuzz=None):
    603     """Create a fuzzed image and write it to the specified file."""
    604     image = Image(backing_file_name.encode())
    605     image.set_backing_file_format(backing_file_fmt.encode())
    606     image.create_feature_name_table()
    607     image.set_end_of_extension_area()
    608     image.create_l_structures()
    609     image.create_refcount_structures()
    610     image.fuzz(fields_to_fuzz)
    611     image.write(test_img_path)
    612     return image.image_size