613 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			613 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Python
		
	
	
	
| # Generator of fuzzed qcow2 images
 | |
| #
 | |
| # Copyright (C) 2014 Maria Kustova <maria.k@catit.be>
 | |
| #
 | |
| # This program is free software: you can redistribute it and/or modify
 | |
| # it under the terms of the GNU General Public License as published by
 | |
| # the Free Software Foundation, either version 2 of the License, or
 | |
| # (at your option) any later version.
 | |
| #
 | |
| # This program is distributed in the hope that it will be useful,
 | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of
 | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | |
| # GNU General Public License for more details.
 | |
| #
 | |
| # You should have received a copy of the GNU General Public License
 | |
| # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
| #
 | |
| 
 | |
| import random
 | |
| import struct
 | |
| import fuzz
 | |
| from math import ceil
 | |
| from os import urandom
 | |
| from itertools import chain
 | |
| 
 | |
| MAX_IMAGE_SIZE = 10 * (1 << 20)
 | |
| # Standard sizes
 | |
| UINT32_S = 4
 | |
| UINT64_S = 8
 | |
| 
 | |
| 
 | |
| class Field(object):
 | |
| 
 | |
|     """Atomic image element (field).
 | |
| 
 | |
|     The class represents an image field as quadruple of a data format
 | |
|     of value necessary for its packing to binary form, an offset from
 | |
|     the beginning of the image, a value and a name.
 | |
| 
 | |
|     The field can be iterated as a list [format, offset, value, name].
 | |
|     """
 | |
| 
 | |
|     __slots__ = ('fmt', 'offset', 'value', 'name')
 | |
| 
 | |
|     def __init__(self, fmt, offset, val, name):
 | |
|         self.fmt = fmt
 | |
|         self.offset = offset
 | |
|         self.value = val
 | |
|         self.name = name
 | |
| 
 | |
|     def __iter__(self):
 | |
|         return iter([self.fmt, self.offset, self.value, self.name])
 | |
| 
 | |
|     def __repr__(self):
 | |
|         return "Field(fmt='%s', offset=%d, value=%s, name=%s)" % \
 | |
|             (self.fmt, self.offset, str(self.value), self.name)
 | |
| 
 | |
| 
 | |
| class FieldsList(object):
 | |
| 
 | |
|     """List of fields.
 | |
| 
 | |
|     The class allows access to a field in the list by its name.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, meta_data=None):
 | |
|         if meta_data is None:
 | |
|             self.data = []
 | |
|         else:
 | |
|             self.data = [Field(*f)
 | |
|                          for f in meta_data]
 | |
| 
 | |
|     def __getitem__(self, name):
 | |
|         return [x for x in self.data if x.name == name]
 | |
| 
 | |
|     def __iter__(self):
 | |
|         return iter(self.data)
 | |
| 
 | |
|     def __len__(self):
 | |
|         return len(self.data)
 | |
| 
 | |
| 
 | |
| class Image(object):
 | |
| 
 | |
|     """ Qcow2 image object.
 | |
| 
 | |
|     This class allows to create qcow2 images with random valid structures and
 | |
|     values, fuzz them via external qcow2.fuzz module and write the result to
 | |
|     a file.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, backing_file_name=None):
 | |
|         """Create a random valid qcow2 image with the correct header and stored
 | |
|         backing file name.
 | |
|         """
 | |
|         cluster_bits, self.image_size = self._size_params()
 | |
|         self.cluster_size = 1 << cluster_bits
 | |
|         self.header = FieldsList()
 | |
|         self.backing_file_name = FieldsList()
 | |
|         self.backing_file_format = FieldsList()
 | |
|         self.feature_name_table = FieldsList()
 | |
|         self.end_of_extension_area = FieldsList()
 | |
|         self.l2_tables = FieldsList()
 | |
|         self.l1_table = FieldsList()
 | |
|         self.refcount_table = FieldsList()
 | |
|         self.refcount_blocks = FieldsList()
 | |
|         self.ext_offset = 0
 | |
|         self.create_header(cluster_bits, backing_file_name)
 | |
|         self.set_backing_file_name(backing_file_name)
 | |
|         self.data_clusters = self._alloc_data(self.image_size,
 | |
|                                               self.cluster_size)
 | |
|         # Percentage of fields will be fuzzed
 | |
|         self.bias = random.uniform(0.2, 0.5)
 | |
| 
 | |
|     def __iter__(self):
 | |
|         return chain(self.header, self.backing_file_format,
 | |
|                      self.feature_name_table, self.end_of_extension_area,
 | |
|                      self.backing_file_name, self.l1_table, self.l2_tables,
 | |
|                      self.refcount_table, self.refcount_blocks)
 | |
| 
 | |
|     def create_header(self, cluster_bits, backing_file_name=None):
 | |
|         """Generate a random valid header."""
 | |
|         meta_header = [
 | |
|             ['>4s', 0, "QFI\xfb", 'magic'],
 | |
|             ['>I', 4, random.randint(2, 3), 'version'],
 | |
|             ['>Q', 8, 0, 'backing_file_offset'],
 | |
|             ['>I', 16, 0, 'backing_file_size'],
 | |
|             ['>I', 20, cluster_bits, 'cluster_bits'],
 | |
|             ['>Q', 24, self.image_size, 'size'],
 | |
|             ['>I', 32, 0, 'crypt_method'],
 | |
|             ['>I', 36, 0, 'l1_size'],
 | |
|             ['>Q', 40, 0, 'l1_table_offset'],
 | |
|             ['>Q', 48, 0, 'refcount_table_offset'],
 | |
|             ['>I', 56, 0, 'refcount_table_clusters'],
 | |
|             ['>I', 60, 0, 'nb_snapshots'],
 | |
|             ['>Q', 64, 0, 'snapshots_offset'],
 | |
|             ['>Q', 72, 0, 'incompatible_features'],
 | |
|             ['>Q', 80, 0, 'compatible_features'],
 | |
|             ['>Q', 88, 0, 'autoclear_features'],
 | |
|             # Only refcount_order = 4 is supported by current (07.2014)
 | |
|             # implementation of QEMU
 | |
|             ['>I', 96, 4, 'refcount_order'],
 | |
|             ['>I', 100, 0, 'header_length']
 | |
|         ]
 | |
|         self.header = FieldsList(meta_header)
 | |
| 
 | |
|         if self.header['version'][0].value == 2:
 | |
|             self.header['header_length'][0].value = 72
 | |
|         else:
 | |
|             self.header['incompatible_features'][0].value = \
 | |
|                                                         random.getrandbits(2)
 | |
|             self.header['compatible_features'][0].value = random.getrandbits(1)
 | |
|             self.header['header_length'][0].value = 104
 | |
|         # Extensions start at the header last field offset and the field size
 | |
|         self.ext_offset = struct.calcsize(
 | |
|             self.header['header_length'][0].fmt) + \
 | |
|             self.header['header_length'][0].offset
 | |
|         end_of_extension_area_len = 2 * UINT32_S
 | |
|         free_space = self.cluster_size - self.ext_offset - \
 | |
|                      end_of_extension_area_len
 | |
|         # If the backing file name specified and there is enough space for it
 | |
|         # in the first cluster, then it's placed in the very end of the first
 | |
|         # cluster.
 | |
|         if (backing_file_name is not None) and \
 | |
|            (free_space >= len(backing_file_name)):
 | |
|             self.header['backing_file_size'][0].value = len(backing_file_name)
 | |
|             self.header['backing_file_offset'][0].value = \
 | |
|                                     self.cluster_size - len(backing_file_name)
 | |
| 
 | |
|     def set_backing_file_name(self, backing_file_name=None):
 | |
|         """Add the name of the backing file at the offset specified
 | |
|         in the header.
 | |
|         """
 | |
|         if (backing_file_name is not None) and \
 | |
|            (not self.header['backing_file_offset'][0].value == 0):
 | |
|             data_len = len(backing_file_name)
 | |
|             data_fmt = '>' + str(data_len) + 's'
 | |
|             self.backing_file_name = FieldsList([
 | |
|                 [data_fmt, self.header['backing_file_offset'][0].value,
 | |
|                  backing_file_name, 'bf_name']
 | |
|             ])
 | |
| 
 | |
|     def set_backing_file_format(self, backing_file_fmt=None):
 | |
|         """Generate the header extension for the backing file format."""
 | |
|         if backing_file_fmt is not None:
 | |
|             # Calculation of the free space available in the first cluster
 | |
|             end_of_extension_area_len = 2 * UINT32_S
 | |
|             high_border = (self.header['backing_file_offset'][0].value or
 | |
|                            (self.cluster_size - 1)) - \
 | |
|                 end_of_extension_area_len
 | |
|             free_space = high_border - self.ext_offset
 | |
|             ext_size = 2 * UINT32_S + ((len(backing_file_fmt) + 7) & ~7)
 | |
| 
 | |
|             if free_space >= ext_size:
 | |
|                 ext_data_len = len(backing_file_fmt)
 | |
|                 ext_data_fmt = '>' + str(ext_data_len) + 's'
 | |
|                 ext_padding_len = 7 - (ext_data_len - 1) % 8
 | |
|                 self.backing_file_format = FieldsList([
 | |
|                     ['>I', self.ext_offset, 0xE2792ACA, 'ext_magic'],
 | |
|                     ['>I', self.ext_offset + UINT32_S, ext_data_len,
 | |
|                      'ext_length'],
 | |
|                     [ext_data_fmt, self.ext_offset + UINT32_S * 2,
 | |
|                      backing_file_fmt, 'bf_format']
 | |
|                 ])
 | |
|                 self.ext_offset = \
 | |
|                         struct.calcsize(
 | |
|                             self.backing_file_format['bf_format'][0].fmt) + \
 | |
|                         ext_padding_len + \
 | |
|                         self.backing_file_format['bf_format'][0].offset
 | |
| 
 | |
|     def create_feature_name_table(self):
 | |
|         """Generate a random header extension for names of features used in
 | |
|         the image.
 | |
|         """
 | |
|         def gen_feat_ids():
 | |
|             """Return random feature type and feature bit."""
 | |
|             return (random.randint(0, 2), random.randint(0, 63))
 | |
| 
 | |
|         end_of_extension_area_len = 2 * UINT32_S
 | |
|         high_border = (self.header['backing_file_offset'][0].value or
 | |
|                        (self.cluster_size - 1)) - \
 | |
|             end_of_extension_area_len
 | |
|         free_space = high_border - self.ext_offset
 | |
|         # Sum of sizes of 'magic' and 'length' header extension fields
 | |
|         ext_header_len = 2 * UINT32_S
 | |
|         fnt_entry_size = 6 * UINT64_S
 | |
|         num_fnt_entries = min(10, (free_space - ext_header_len) /
 | |
|                               fnt_entry_size)
 | |
|         if not num_fnt_entries == 0:
 | |
|             feature_tables = []
 | |
|             feature_ids = []
 | |
|             inner_offset = self.ext_offset + ext_header_len
 | |
|             feat_name = 'some cool feature'
 | |
|             while len(feature_tables) < num_fnt_entries * 3:
 | |
|                 feat_type, feat_bit = gen_feat_ids()
 | |
|                 # Remove duplicates
 | |
|                 while (feat_type, feat_bit) in feature_ids:
 | |
|                     feat_type, feat_bit = gen_feat_ids()
 | |
|                 feature_ids.append((feat_type, feat_bit))
 | |
|                 feat_fmt = '>' + str(len(feat_name)) + 's'
 | |
|                 feature_tables += [['B', inner_offset,
 | |
|                                     feat_type, 'feature_type'],
 | |
|                                    ['B', inner_offset + 1, feat_bit,
 | |
|                                     'feature_bit_number'],
 | |
|                                    [feat_fmt, inner_offset + 2,
 | |
|                                     feat_name, 'feature_name']
 | |
|                 ]
 | |
|                 inner_offset += fnt_entry_size
 | |
|             # No padding for the extension is necessary, because
 | |
|             # the extension length is multiple of 8
 | |
|             self.feature_name_table = FieldsList([
 | |
|                 ['>I', self.ext_offset, 0x6803f857, 'ext_magic'],
 | |
|                 # One feature table contains 3 fields and takes 48 bytes
 | |
|                 ['>I', self.ext_offset + UINT32_S,
 | |
|                  len(feature_tables) / 3 * 48, 'ext_length']
 | |
|             ] + feature_tables)
 | |
|             self.ext_offset = inner_offset
 | |
| 
 | |
|     def set_end_of_extension_area(self):
 | |
|         """Generate a mandatory header extension marking end of header
 | |
|         extensions.
 | |
|         """
 | |
|         self.end_of_extension_area = FieldsList([
 | |
|             ['>I', self.ext_offset, 0, 'ext_magic'],
 | |
|             ['>I', self.ext_offset + UINT32_S, 0, 'ext_length']
 | |
|         ])
 | |
| 
 | |
|     def create_l_structures(self):
 | |
|         """Generate random valid L1 and L2 tables."""
 | |
|         def create_l2_entry(host, guest, l2_cluster):
 | |
|             """Generate one L2 entry."""
 | |
|             offset = l2_cluster * self.cluster_size
 | |
|             l2_size = self.cluster_size / UINT64_S
 | |
|             entry_offset = offset + UINT64_S * (guest % l2_size)
 | |
|             cluster_descriptor = host * self.cluster_size
 | |
|             if not self.header['version'][0].value == 2:
 | |
|                 cluster_descriptor += random.randint(0, 1)
 | |
|             # While snapshots are not supported, bit #63 = 1
 | |
|             # Compressed clusters are not supported => bit #62 = 0
 | |
|             entry_val = (1 << 63) + cluster_descriptor
 | |
|             return ['>Q', entry_offset, entry_val, 'l2_entry']
 | |
| 
 | |
|         def create_l1_entry(l2_cluster, l1_offset, guest):
 | |
|             """Generate one L1 entry."""
 | |
|             l2_size = self.cluster_size / UINT64_S
 | |
|             entry_offset = l1_offset + UINT64_S * (guest / l2_size)
 | |
|             # While snapshots are not supported bit #63 = 1
 | |
|             entry_val = (1 << 63) + l2_cluster * self.cluster_size
 | |
|             return ['>Q', entry_offset, entry_val, 'l1_entry']
 | |
| 
 | |
|         if len(self.data_clusters) == 0:
 | |
|             # All metadata for an empty guest image needs 4 clusters:
 | |
|             # header, rfc table, rfc block, L1 table.
 | |
|             # Header takes cluster #0, other clusters ##1-3 can be used
 | |
|             l1_offset = random.randint(1, 3) * self.cluster_size
 | |
|             l1 = [['>Q', l1_offset, 0, 'l1_entry']]
 | |
|             l2 = []
 | |
|         else:
 | |
|             meta_data = self._get_metadata()
 | |
|             guest_clusters = random.sample(range(self.image_size /
 | |
|                                                  self.cluster_size),
 | |
|                                            len(self.data_clusters))
 | |
|             # Number of entries in a L1/L2 table
 | |
|             l_size = self.cluster_size / UINT64_S
 | |
|             # Number of clusters necessary for L1 table
 | |
|             l1_size = int(ceil((max(guest_clusters) + 1) / float(l_size**2)))
 | |
|             l1_start = self._get_adjacent_clusters(self.data_clusters |
 | |
|                                                    meta_data, l1_size)
 | |
|             meta_data |= set(range(l1_start, l1_start + l1_size))
 | |
|             l1_offset = l1_start * self.cluster_size
 | |
|             # Indices of L2 tables
 | |
|             l2_ids = []
 | |
|             # Host clusters allocated for L2 tables
 | |
|             l2_clusters = []
 | |
|             # L1 entries
 | |
|             l1 = []
 | |
|             # L2 entries
 | |
|             l2 = []
 | |
|             for host, guest in zip(self.data_clusters, guest_clusters):
 | |
|                 l2_id = guest / l_size
 | |
|                 if l2_id not in l2_ids:
 | |
|                     l2_ids.append(l2_id)
 | |
|                     l2_clusters.append(self._get_adjacent_clusters(
 | |
|                         self.data_clusters | meta_data | set(l2_clusters),
 | |
|                         1))
 | |
|                     l1.append(create_l1_entry(l2_clusters[-1], l1_offset,
 | |
|                                               guest))
 | |
|                 l2.append(create_l2_entry(host, guest,
 | |
|                                           l2_clusters[l2_ids.index(l2_id)]))
 | |
|         self.l2_tables = FieldsList(l2)
 | |
|         self.l1_table = FieldsList(l1)
 | |
|         self.header['l1_size'][0].value = int(ceil(UINT64_S * self.image_size /
 | |
|                                                 float(self.cluster_size**2)))
 | |
|         self.header['l1_table_offset'][0].value = l1_offset
 | |
| 
 | |
|     def create_refcount_structures(self):
 | |
|         """Generate random refcount blocks and refcount table."""
 | |
|         def allocate_rfc_blocks(data, size):
 | |
|             """Return indices of clusters allocated for refcount blocks."""
 | |
|             cluster_ids = set()
 | |
|             diff = block_ids = set([x / size for x in data])
 | |
|             while len(diff) != 0:
 | |
|                 # Allocate all yet not allocated clusters
 | |
|                 new = self._get_available_clusters(data | cluster_ids,
 | |
|                                                    len(diff))
 | |
|                 # Indices of new refcount blocks necessary to cover clusters
 | |
|                 # in 'new'
 | |
|                 diff = set([x / size for x in new]) - block_ids
 | |
|                 cluster_ids |= new
 | |
|                 block_ids |= diff
 | |
|             return cluster_ids, block_ids
 | |
| 
 | |
|         def allocate_rfc_table(data, init_blocks, block_size):
 | |
|             """Return indices of clusters allocated for the refcount table
 | |
|             and updated indices of clusters allocated for blocks and indices
 | |
|             of blocks.
 | |
|             """
 | |
|             blocks = set(init_blocks)
 | |
|             clusters = set()
 | |
|             # Number of entries in one cluster of the refcount table
 | |
|             size = self.cluster_size / UINT64_S
 | |
|             # Number of clusters necessary for the refcount table based on
 | |
|             # the current number of refcount blocks
 | |
|             table_size = int(ceil((max(blocks) + 1) / float(size)))
 | |
|             # Index of the first cluster of the refcount table
 | |
|             table_start = self._get_adjacent_clusters(data, table_size + 1)
 | |
|             # Clusters allocated for the current length of the refcount table
 | |
|             table_clusters = set(range(table_start, table_start + table_size))
 | |
|             # Clusters allocated for the refcount table including
 | |
|             # last optional one for potential l1 growth
 | |
|             table_clusters_allocated = set(range(table_start, table_start +
 | |
|                                                  table_size + 1))
 | |
|             # New refcount blocks necessary for clusters occupied by the
 | |
|             # refcount table
 | |
|             diff = set([c / block_size for c in table_clusters]) - blocks
 | |
|             blocks |= diff
 | |
|             while len(diff) != 0:
 | |
|                 # Allocate clusters for new refcount blocks
 | |
|                 new = self._get_available_clusters((data | clusters) |
 | |
|                                                    table_clusters_allocated,
 | |
|                                                    len(diff))
 | |
|                 # Indices of new refcount blocks necessary to cover
 | |
|                 # clusters in 'new'
 | |
|                 diff = set([x / block_size for x in new]) - blocks
 | |
|                 clusters |= new
 | |
|                 blocks |= diff
 | |
|                 # Check if the refcount table needs one more cluster
 | |
|                 if int(ceil((max(blocks) + 1) / float(size))) > table_size:
 | |
|                     new_block_id = (table_start + table_size) / block_size
 | |
|                     # Check if the additional table cluster needs
 | |
|                     # one more refcount block
 | |
|                     if new_block_id not in blocks:
 | |
|                         diff.add(new_block_id)
 | |
|                     table_clusters.add(table_start + table_size)
 | |
|                     table_size += 1
 | |
|             return table_clusters, blocks, clusters
 | |
| 
 | |
|         def create_table_entry(table_offset, block_cluster, block_size,
 | |
|                                cluster):
 | |
|             """Generate a refcount table entry."""
 | |
|             offset = table_offset + UINT64_S * (cluster / block_size)
 | |
|             return ['>Q', offset, block_cluster * self.cluster_size,
 | |
|                     'refcount_table_entry']
 | |
| 
 | |
|         def create_block_entry(block_cluster, block_size, cluster):
 | |
|             """Generate a list of entries for the current block."""
 | |
|             entry_size = self.cluster_size / block_size
 | |
|             offset = block_cluster * self.cluster_size
 | |
|             entry_offset = offset + entry_size * (cluster % block_size)
 | |
|             # While snapshots are not supported all refcounts are set to 1
 | |
|             return ['>H', entry_offset, 1, 'refcount_block_entry']
 | |
|         # Size of a block entry in bits
 | |
|         refcount_bits = 1 << self.header['refcount_order'][0].value
 | |
|         # Number of refcount entries per refcount block
 | |
|         # Convert self.cluster_size from bytes to bits to have the same
 | |
|         # base for the numerator and denominator
 | |
|         block_size = self.cluster_size * 8 / refcount_bits
 | |
|         meta_data = self._get_metadata()
 | |
|         if len(self.data_clusters) == 0:
 | |
|             # All metadata for an empty guest image needs 4 clusters:
 | |
|             # header, rfc table, rfc block, L1 table.
 | |
|             # Header takes cluster #0, other clusters ##1-3 can be used
 | |
|             block_clusters = set([random.choice(list(set(range(1, 4)) -
 | |
|                                                      meta_data))])
 | |
|             block_ids = set([0])
 | |
|             table_clusters = set([random.choice(list(set(range(1, 4)) -
 | |
|                                                      meta_data -
 | |
|                                                      block_clusters))])
 | |
|         else:
 | |
|             block_clusters, block_ids = \
 | |
|                                 allocate_rfc_blocks(self.data_clusters |
 | |
|                                                     meta_data, block_size)
 | |
|             table_clusters, block_ids, new_clusters = \
 | |
|                                     allocate_rfc_table(self.data_clusters |
 | |
|                                                        meta_data |
 | |
|                                                        block_clusters,
 | |
|                                                        block_ids,
 | |
|                                                        block_size)
 | |
|             block_clusters |= new_clusters
 | |
| 
 | |
|         meta_data |= block_clusters | table_clusters
 | |
|         table_offset = min(table_clusters) * self.cluster_size
 | |
|         block_id = None
 | |
|         # Clusters allocated for refcount blocks
 | |
|         block_clusters = list(block_clusters)
 | |
|         # Indices of refcount blocks
 | |
|         block_ids = list(block_ids)
 | |
|         # Refcount table entries
 | |
|         rfc_table = []
 | |
|         # Refcount entries
 | |
|         rfc_blocks = []
 | |
| 
 | |
|         for cluster in sorted(self.data_clusters | meta_data):
 | |
|             if cluster / block_size != block_id:
 | |
|                 block_id = cluster / block_size
 | |
|                 block_cluster = block_clusters[block_ids.index(block_id)]
 | |
|                 rfc_table.append(create_table_entry(table_offset,
 | |
|                                                     block_cluster,
 | |
|                                                     block_size, cluster))
 | |
|             rfc_blocks.append(create_block_entry(block_cluster, block_size,
 | |
|                                                  cluster))
 | |
|         self.refcount_table = FieldsList(rfc_table)
 | |
|         self.refcount_blocks = FieldsList(rfc_blocks)
 | |
| 
 | |
|         self.header['refcount_table_offset'][0].value = table_offset
 | |
|         self.header['refcount_table_clusters'][0].value = len(table_clusters)
 | |
| 
 | |
|     def fuzz(self, fields_to_fuzz=None):
 | |
|         """Fuzz an image by corrupting values of a random subset of its fields.
 | |
| 
 | |
|         Without parameters the method fuzzes an entire image.
 | |
| 
 | |
|         If 'fields_to_fuzz' is specified then only fields in this list will be
 | |
|         fuzzed. 'fields_to_fuzz' can contain both individual fields and more
 | |
|         general image elements as a header or tables.
 | |
| 
 | |
|         In the first case the field will be fuzzed always.
 | |
|         In the second a random subset of fields will be selected and fuzzed.
 | |
|         """
 | |
|         def coin():
 | |
|             """Return boolean value proportional to a portion of fields to be
 | |
|             fuzzed.
 | |
|             """
 | |
|             return random.random() < self.bias
 | |
| 
 | |
|         if fields_to_fuzz is None:
 | |
|             for field in self:
 | |
|                 if coin():
 | |
|                     field.value = getattr(fuzz, field.name)(field.value)
 | |
|         else:
 | |
|             for item in fields_to_fuzz:
 | |
|                 if len(item) == 1:
 | |
|                     for field in getattr(self, item[0]):
 | |
|                         if coin():
 | |
|                             field.value = getattr(fuzz,
 | |
|                                                   field.name)(field.value)
 | |
|                 else:
 | |
|                     # If fields with the requested name were not generated
 | |
|                     # getattr(self, item[0])[item[1]] returns an empty list
 | |
|                     for field in getattr(self, item[0])[item[1]]:
 | |
|                         field.value = getattr(fuzz, field.name)(field.value)
 | |
| 
 | |
|     def write(self, filename):
 | |
|         """Write an entire image to the file."""
 | |
|         image_file = open(filename, 'w')
 | |
|         for field in self:
 | |
|             image_file.seek(field.offset)
 | |
|             image_file.write(struct.pack(field.fmt, field.value))
 | |
| 
 | |
|         for cluster in sorted(self.data_clusters):
 | |
|             image_file.seek(cluster * self.cluster_size)
 | |
|             image_file.write(urandom(self.cluster_size))
 | |
| 
 | |
|         # Align the real image size to the cluster size
 | |
|         image_file.seek(0, 2)
 | |
|         size = image_file.tell()
 | |
|         rounded = (size + self.cluster_size - 1) & ~(self.cluster_size - 1)
 | |
|         if rounded > size:
 | |
|             image_file.seek(rounded - 1)
 | |
|             image_file.write("\0")
 | |
|         image_file.close()
 | |
| 
 | |
|     @staticmethod
 | |
|     def _size_params():
 | |
|         """Generate a random image size aligned to a random correct
 | |
|         cluster size.
 | |
|         """
 | |
|         cluster_bits = random.randrange(9, 21)
 | |
|         cluster_size = 1 << cluster_bits
 | |
|         img_size = random.randrange(0, MAX_IMAGE_SIZE + 1, cluster_size)
 | |
|         return (cluster_bits, img_size)
 | |
| 
 | |
|     @staticmethod
 | |
|     def _get_available_clusters(used, number):
 | |
|         """Return a set of indices of not allocated clusters.
 | |
| 
 | |
|         'used' contains indices of currently allocated clusters.
 | |
|         All clusters that cannot be allocated between 'used' clusters will have
 | |
|         indices appended to the end of 'used'.
 | |
|         """
 | |
|         append_id = max(used) + 1
 | |
|         free = set(range(1, append_id)) - used
 | |
|         if len(free) >= number:
 | |
|             return set(random.sample(free, number))
 | |
|         else:
 | |
|             return free | set(range(append_id, append_id + number - len(free)))
 | |
| 
 | |
|     @staticmethod
 | |
|     def _get_adjacent_clusters(used, size):
 | |
|         """Return an index of the first cluster in the sequence of free ones.
 | |
| 
 | |
|         'used' contains indices of currently allocated clusters. 'size' is the
 | |
|         length of the sequence of free clusters.
 | |
|         If the sequence of 'size' is not available between 'used' clusters, its
 | |
|         first index will be append to the end of 'used'.
 | |
|         """
 | |
|         def get_cluster_id(lst, length):
 | |
|             """Return the first index of the sequence of the specified length
 | |
|             or None if the sequence cannot be inserted in the list.
 | |
|             """
 | |
|             if len(lst) != 0:
 | |
|                 pairs = []
 | |
|                 pair = (lst[0], 1)
 | |
|                 for i in range(1, len(lst)):
 | |
|                     if lst[i] == lst[i-1] + 1:
 | |
|                         pair = (lst[i], pair[1] + 1)
 | |
|                     else:
 | |
|                         pairs.append(pair)
 | |
|                         pair = (lst[i], 1)
 | |
|                 pairs.append(pair)
 | |
|                 random.shuffle(pairs)
 | |
|                 for x, s in pairs:
 | |
|                     if s >= length:
 | |
|                         return x - length + 1
 | |
|             return None
 | |
| 
 | |
|         append_id = max(used) + 1
 | |
|         free = list(set(range(1, append_id)) - used)
 | |
|         idx = get_cluster_id(free, size)
 | |
|         if idx is None:
 | |
|             return append_id
 | |
|         else:
 | |
|             return idx
 | |
| 
 | |
|     @staticmethod
 | |
|     def _alloc_data(img_size, cluster_size):
 | |
|         """Return a set of random indices of clusters allocated for guest data.
 | |
|         """
 | |
|         num_of_cls = img_size/cluster_size
 | |
|         return set(random.sample(range(1, num_of_cls + 1),
 | |
|                                  random.randint(0, num_of_cls)))
 | |
| 
 | |
|     def _get_metadata(self):
 | |
|         """Return indices of clusters allocated for image metadata."""
 | |
|         ids = set()
 | |
|         for x in self:
 | |
|             ids.add(x.offset/self.cluster_size)
 | |
|         return ids
 | |
| 
 | |
| 
 | |
| def create_image(test_img_path, backing_file_name=None, backing_file_fmt=None,
 | |
|                  fields_to_fuzz=None):
 | |
|     """Create a fuzzed image and write it to the specified file."""
 | |
|     image = Image(backing_file_name)
 | |
|     image.set_backing_file_format(backing_file_fmt)
 | |
|     image.create_feature_name_table()
 | |
|     image.set_end_of_extension_area()
 | |
|     image.create_l_structures()
 | |
|     image.create_refcount_structures()
 | |
|     image.fuzz(fields_to_fuzz)
 | |
|     image.write(test_img_path)
 | |
|     return image.image_size
 |