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