1*e1232323SMaria Kustova# Generator of fuzzed qcow2 images 2*e1232323SMaria Kustova# 3*e1232323SMaria Kustova# Copyright (C) 2014 Maria Kustova <maria.k@catit.be> 4*e1232323SMaria Kustova# 5*e1232323SMaria Kustova# This program is free software: you can redistribute it and/or modify 6*e1232323SMaria Kustova# it under the terms of the GNU General Public License as published by 7*e1232323SMaria Kustova# the Free Software Foundation, either version 2 of the License, or 8*e1232323SMaria Kustova# (at your option) any later version. 9*e1232323SMaria Kustova# 10*e1232323SMaria Kustova# This program is distributed in the hope that it will be useful, 11*e1232323SMaria Kustova# but WITHOUT ANY WARRANTY; without even the implied warranty of 12*e1232323SMaria Kustova# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13*e1232323SMaria Kustova# GNU General Public License for more details. 14*e1232323SMaria Kustova# 15*e1232323SMaria Kustova# You should have received a copy of the GNU General Public License 16*e1232323SMaria Kustova# along with this program. If not, see <http://www.gnu.org/licenses/>. 17*e1232323SMaria Kustova# 18*e1232323SMaria Kustova 19*e1232323SMaria Kustovaimport random 20*e1232323SMaria Kustovaimport struct 21*e1232323SMaria Kustovaimport fuzz 22*e1232323SMaria Kustova 23*e1232323SMaria KustovaMAX_IMAGE_SIZE = 10 * (1 << 20) 24*e1232323SMaria Kustova# Standard sizes 25*e1232323SMaria KustovaUINT32_S = 4 26*e1232323SMaria KustovaUINT64_S = 8 27*e1232323SMaria Kustova 28*e1232323SMaria Kustova 29*e1232323SMaria Kustovaclass Field(object): 30*e1232323SMaria Kustova 31*e1232323SMaria Kustova """Atomic image element (field). 32*e1232323SMaria Kustova 33*e1232323SMaria Kustova The class represents an image field as quadruple of a data format 34*e1232323SMaria Kustova of value necessary for its packing to binary form, an offset from 35*e1232323SMaria Kustova the beginning of the image, a value and a name. 36*e1232323SMaria Kustova 37*e1232323SMaria Kustova The field can be iterated as a list [format, offset, value]. 38*e1232323SMaria Kustova """ 39*e1232323SMaria Kustova 40*e1232323SMaria Kustova __slots__ = ('fmt', 'offset', 'value', 'name') 41*e1232323SMaria Kustova 42*e1232323SMaria Kustova def __init__(self, fmt, offset, val, name): 43*e1232323SMaria Kustova self.fmt = fmt 44*e1232323SMaria Kustova self.offset = offset 45*e1232323SMaria Kustova self.value = val 46*e1232323SMaria Kustova self.name = name 47*e1232323SMaria Kustova 48*e1232323SMaria Kustova def __iter__(self): 49*e1232323SMaria Kustova return iter([self.fmt, self.offset, self.value]) 50*e1232323SMaria Kustova 51*e1232323SMaria Kustova def __repr__(self): 52*e1232323SMaria Kustova return "Field(fmt='%s', offset=%d, value=%s, name=%s)" % \ 53*e1232323SMaria Kustova (self.fmt, self.offset, str(self.value), self.name) 54*e1232323SMaria Kustova 55*e1232323SMaria Kustova 56*e1232323SMaria Kustovaclass FieldsList(object): 57*e1232323SMaria Kustova 58*e1232323SMaria Kustova """List of fields. 59*e1232323SMaria Kustova 60*e1232323SMaria Kustova The class allows access to a field in the list by its name and joins 61*e1232323SMaria Kustova several list in one via in-place addition. 62*e1232323SMaria Kustova """ 63*e1232323SMaria Kustova 64*e1232323SMaria Kustova def __init__(self, meta_data=None): 65*e1232323SMaria Kustova if meta_data is None: 66*e1232323SMaria Kustova self.data = [] 67*e1232323SMaria Kustova else: 68*e1232323SMaria Kustova self.data = [Field(f[0], f[1], f[2], f[3]) 69*e1232323SMaria Kustova for f in meta_data] 70*e1232323SMaria Kustova 71*e1232323SMaria Kustova def __getitem__(self, name): 72*e1232323SMaria Kustova return [x for x in self.data if x.name == name] 73*e1232323SMaria Kustova 74*e1232323SMaria Kustova def __iter__(self): 75*e1232323SMaria Kustova return iter(self.data) 76*e1232323SMaria Kustova 77*e1232323SMaria Kustova def __iadd__(self, other): 78*e1232323SMaria Kustova self.data += other.data 79*e1232323SMaria Kustova return self 80*e1232323SMaria Kustova 81*e1232323SMaria Kustova def __len__(self): 82*e1232323SMaria Kustova return len(self.data) 83*e1232323SMaria Kustova 84*e1232323SMaria Kustova 85*e1232323SMaria Kustovaclass Image(object): 86*e1232323SMaria Kustova 87*e1232323SMaria Kustova """ Qcow2 image object. 88*e1232323SMaria Kustova 89*e1232323SMaria Kustova This class allows to create qcow2 images with random valid structures and 90*e1232323SMaria Kustova values, fuzz them via external qcow2.fuzz module and write the result to 91*e1232323SMaria Kustova a file. 92*e1232323SMaria Kustova """ 93*e1232323SMaria Kustova 94*e1232323SMaria Kustova @staticmethod 95*e1232323SMaria Kustova def _size_params(): 96*e1232323SMaria Kustova """Generate a random image size aligned to a random correct 97*e1232323SMaria Kustova cluster size. 98*e1232323SMaria Kustova """ 99*e1232323SMaria Kustova cluster_bits = random.randrange(9, 21) 100*e1232323SMaria Kustova cluster_size = 1 << cluster_bits 101*e1232323SMaria Kustova img_size = random.randrange(0, MAX_IMAGE_SIZE + 1, cluster_size) 102*e1232323SMaria Kustova return (cluster_bits, img_size) 103*e1232323SMaria Kustova 104*e1232323SMaria Kustova @staticmethod 105*e1232323SMaria Kustova def _header(cluster_bits, img_size, backing_file_name=None): 106*e1232323SMaria Kustova """Generate a random valid header.""" 107*e1232323SMaria Kustova meta_header = [ 108*e1232323SMaria Kustova ['>4s', 0, "QFI\xfb", 'magic'], 109*e1232323SMaria Kustova ['>I', 4, random.randint(2, 3), 'version'], 110*e1232323SMaria Kustova ['>Q', 8, 0, 'backing_file_offset'], 111*e1232323SMaria Kustova ['>I', 16, 0, 'backing_file_size'], 112*e1232323SMaria Kustova ['>I', 20, cluster_bits, 'cluster_bits'], 113*e1232323SMaria Kustova ['>Q', 24, img_size, 'size'], 114*e1232323SMaria Kustova ['>I', 32, 0, 'crypt_method'], 115*e1232323SMaria Kustova ['>I', 36, 0, 'l1_size'], 116*e1232323SMaria Kustova ['>Q', 40, 0, 'l1_table_offset'], 117*e1232323SMaria Kustova ['>Q', 48, 0, 'refcount_table_offset'], 118*e1232323SMaria Kustova ['>I', 56, 0, 'refcount_table_clusters'], 119*e1232323SMaria Kustova ['>I', 60, 0, 'nb_snapshots'], 120*e1232323SMaria Kustova ['>Q', 64, 0, 'snapshots_offset'], 121*e1232323SMaria Kustova ['>Q', 72, 0, 'incompatible_features'], 122*e1232323SMaria Kustova ['>Q', 80, 0, 'compatible_features'], 123*e1232323SMaria Kustova ['>Q', 88, 0, 'autoclear_features'], 124*e1232323SMaria Kustova # Only refcount_order = 4 is supported by current (07.2014) 125*e1232323SMaria Kustova # implementation of QEMU 126*e1232323SMaria Kustova ['>I', 96, 4, 'refcount_order'], 127*e1232323SMaria Kustova ['>I', 100, 0, 'header_length'] 128*e1232323SMaria Kustova ] 129*e1232323SMaria Kustova v_header = FieldsList(meta_header) 130*e1232323SMaria Kustova 131*e1232323SMaria Kustova if v_header['version'][0].value == 2: 132*e1232323SMaria Kustova v_header['header_length'][0].value = 72 133*e1232323SMaria Kustova else: 134*e1232323SMaria Kustova v_header['incompatible_features'][0].value = random.getrandbits(2) 135*e1232323SMaria Kustova v_header['compatible_features'][0].value = random.getrandbits(1) 136*e1232323SMaria Kustova v_header['header_length'][0].value = 104 137*e1232323SMaria Kustova 138*e1232323SMaria Kustova max_header_len = struct.calcsize(v_header['header_length'][0].fmt) + \ 139*e1232323SMaria Kustova v_header['header_length'][0].offset 140*e1232323SMaria Kustova end_of_extension_area_len = 2 * UINT32_S 141*e1232323SMaria Kustova free_space = (1 << cluster_bits) - (max_header_len + 142*e1232323SMaria Kustova end_of_extension_area_len) 143*e1232323SMaria Kustova # If the backing file name specified and there is enough space for it 144*e1232323SMaria Kustova # in the first cluster, then it's placed in the very end of the first 145*e1232323SMaria Kustova # cluster. 146*e1232323SMaria Kustova if (backing_file_name is not None) and \ 147*e1232323SMaria Kustova (free_space >= len(backing_file_name)): 148*e1232323SMaria Kustova v_header['backing_file_size'][0].value = len(backing_file_name) 149*e1232323SMaria Kustova v_header['backing_file_offset'][0].value = (1 << cluster_bits) - \ 150*e1232323SMaria Kustova len(backing_file_name) 151*e1232323SMaria Kustova 152*e1232323SMaria Kustova return v_header 153*e1232323SMaria Kustova 154*e1232323SMaria Kustova @staticmethod 155*e1232323SMaria Kustova def _backing_file_name(header, backing_file_name=None): 156*e1232323SMaria Kustova """Add the name of the backing file at the offset specified 157*e1232323SMaria Kustova in the header. 158*e1232323SMaria Kustova """ 159*e1232323SMaria Kustova if (backing_file_name is not None) and \ 160*e1232323SMaria Kustova (not header['backing_file_offset'][0].value == 0): 161*e1232323SMaria Kustova data_len = len(backing_file_name) 162*e1232323SMaria Kustova data_fmt = '>' + str(data_len) + 's' 163*e1232323SMaria Kustova data_field = FieldsList([ 164*e1232323SMaria Kustova [data_fmt, header['backing_file_offset'][0].value, 165*e1232323SMaria Kustova backing_file_name, 'bf_name'] 166*e1232323SMaria Kustova ]) 167*e1232323SMaria Kustova else: 168*e1232323SMaria Kustova data_field = FieldsList() 169*e1232323SMaria Kustova 170*e1232323SMaria Kustova return data_field 171*e1232323SMaria Kustova 172*e1232323SMaria Kustova @staticmethod 173*e1232323SMaria Kustova def _backing_file_format(header, backing_file_fmt=None): 174*e1232323SMaria Kustova """Generate the header extension for the backing file 175*e1232323SMaria Kustova format. 176*e1232323SMaria Kustova """ 177*e1232323SMaria Kustova ext = FieldsList() 178*e1232323SMaria Kustova offset = struct.calcsize(header['header_length'][0].fmt) + \ 179*e1232323SMaria Kustova header['header_length'][0].offset 180*e1232323SMaria Kustova 181*e1232323SMaria Kustova if backing_file_fmt is not None: 182*e1232323SMaria Kustova # Calculation of the free space available in the first cluster 183*e1232323SMaria Kustova end_of_extension_area_len = 2 * UINT32_S 184*e1232323SMaria Kustova high_border = (header['backing_file_offset'][0].value or 185*e1232323SMaria Kustova ((1 << header['cluster_bits'][0].value) - 1)) - \ 186*e1232323SMaria Kustova end_of_extension_area_len 187*e1232323SMaria Kustova free_space = high_border - offset 188*e1232323SMaria Kustova ext_size = 2 * UINT32_S + ((len(backing_file_fmt) + 7) & ~7) 189*e1232323SMaria Kustova 190*e1232323SMaria Kustova if free_space >= ext_size: 191*e1232323SMaria Kustova ext_data_len = len(backing_file_fmt) 192*e1232323SMaria Kustova ext_data_fmt = '>' + str(ext_data_len) + 's' 193*e1232323SMaria Kustova ext_padding_len = 7 - (ext_data_len - 1) % 8 194*e1232323SMaria Kustova ext = FieldsList([ 195*e1232323SMaria Kustova ['>I', offset, 0xE2792ACA, 'ext_magic'], 196*e1232323SMaria Kustova ['>I', offset + UINT32_S, ext_data_len, 'ext_length'], 197*e1232323SMaria Kustova [ext_data_fmt, offset + UINT32_S * 2, backing_file_fmt, 198*e1232323SMaria Kustova 'bf_format'] 199*e1232323SMaria Kustova ]) 200*e1232323SMaria Kustova offset = ext['bf_format'][0].offset + \ 201*e1232323SMaria Kustova struct.calcsize(ext['bf_format'][0].fmt) + \ 202*e1232323SMaria Kustova ext_padding_len 203*e1232323SMaria Kustova return (ext, offset) 204*e1232323SMaria Kustova 205*e1232323SMaria Kustova @staticmethod 206*e1232323SMaria Kustova def _feature_name_table(header, offset): 207*e1232323SMaria Kustova """Generate a random header extension for names of features used in 208*e1232323SMaria Kustova the image. 209*e1232323SMaria Kustova """ 210*e1232323SMaria Kustova def gen_feat_ids(): 211*e1232323SMaria Kustova """Return random feature type and feature bit.""" 212*e1232323SMaria Kustova return (random.randint(0, 2), random.randint(0, 63)) 213*e1232323SMaria Kustova 214*e1232323SMaria Kustova end_of_extension_area_len = 2 * UINT32_S 215*e1232323SMaria Kustova high_border = (header['backing_file_offset'][0].value or 216*e1232323SMaria Kustova (1 << header['cluster_bits'][0].value) - 1) - \ 217*e1232323SMaria Kustova end_of_extension_area_len 218*e1232323SMaria Kustova free_space = high_border - offset 219*e1232323SMaria Kustova # Sum of sizes of 'magic' and 'length' header extension fields 220*e1232323SMaria Kustova ext_header_len = 2 * UINT32_S 221*e1232323SMaria Kustova fnt_entry_size = 6 * UINT64_S 222*e1232323SMaria Kustova num_fnt_entries = min(10, (free_space - ext_header_len) / 223*e1232323SMaria Kustova fnt_entry_size) 224*e1232323SMaria Kustova if not num_fnt_entries == 0: 225*e1232323SMaria Kustova feature_tables = [] 226*e1232323SMaria Kustova feature_ids = [] 227*e1232323SMaria Kustova inner_offset = offset + ext_header_len 228*e1232323SMaria Kustova feat_name = 'some cool feature' 229*e1232323SMaria Kustova while len(feature_tables) < num_fnt_entries * 3: 230*e1232323SMaria Kustova feat_type, feat_bit = gen_feat_ids() 231*e1232323SMaria Kustova # Remove duplicates 232*e1232323SMaria Kustova while (feat_type, feat_bit) in feature_ids: 233*e1232323SMaria Kustova feat_type, feat_bit = gen_feat_ids() 234*e1232323SMaria Kustova feature_ids.append((feat_type, feat_bit)) 235*e1232323SMaria Kustova feat_fmt = '>' + str(len(feat_name)) + 's' 236*e1232323SMaria Kustova feature_tables += [['B', inner_offset, 237*e1232323SMaria Kustova feat_type, 'feature_type'], 238*e1232323SMaria Kustova ['B', inner_offset + 1, feat_bit, 239*e1232323SMaria Kustova 'feature_bit_number'], 240*e1232323SMaria Kustova [feat_fmt, inner_offset + 2, 241*e1232323SMaria Kustova feat_name, 'feature_name'] 242*e1232323SMaria Kustova ] 243*e1232323SMaria Kustova inner_offset += fnt_entry_size 244*e1232323SMaria Kustova # No padding for the extension is necessary, because 245*e1232323SMaria Kustova # the extension length is multiple of 8 246*e1232323SMaria Kustova ext = FieldsList([ 247*e1232323SMaria Kustova ['>I', offset, 0x6803f857, 'ext_magic'], 248*e1232323SMaria Kustova # One feature table contains 3 fields and takes 48 bytes 249*e1232323SMaria Kustova ['>I', offset + UINT32_S, len(feature_tables) / 3 * 48, 250*e1232323SMaria Kustova 'ext_length'] 251*e1232323SMaria Kustova ] + feature_tables) 252*e1232323SMaria Kustova offset = inner_offset 253*e1232323SMaria Kustova else: 254*e1232323SMaria Kustova ext = FieldsList() 255*e1232323SMaria Kustova 256*e1232323SMaria Kustova return (ext, offset) 257*e1232323SMaria Kustova 258*e1232323SMaria Kustova @staticmethod 259*e1232323SMaria Kustova def _end_of_extension_area(offset): 260*e1232323SMaria Kustova """Generate a mandatory header extension marking end of header 261*e1232323SMaria Kustova extensions. 262*e1232323SMaria Kustova """ 263*e1232323SMaria Kustova ext = FieldsList([ 264*e1232323SMaria Kustova ['>I', offset, 0, 'ext_magic'], 265*e1232323SMaria Kustova ['>I', offset + UINT32_S, 0, 'ext_length'] 266*e1232323SMaria Kustova ]) 267*e1232323SMaria Kustova return ext 268*e1232323SMaria Kustova 269*e1232323SMaria Kustova def __init__(self, backing_file_name=None, backing_file_fmt=None): 270*e1232323SMaria Kustova """Create a random valid qcow2 image with the correct inner structure 271*e1232323SMaria Kustova and allowable values. 272*e1232323SMaria Kustova """ 273*e1232323SMaria Kustova # Image size is saved as an attribute for the runner needs 274*e1232323SMaria Kustova cluster_bits, self.image_size = self._size_params() 275*e1232323SMaria Kustova # Saved as an attribute, because it's necessary for writing 276*e1232323SMaria Kustova self.cluster_size = 1 << cluster_bits 277*e1232323SMaria Kustova self.header = self._header(cluster_bits, self.image_size, 278*e1232323SMaria Kustova backing_file_name) 279*e1232323SMaria Kustova self.backing_file_name = self._backing_file_name(self.header, 280*e1232323SMaria Kustova backing_file_name) 281*e1232323SMaria Kustova self.backing_file_format, \ 282*e1232323SMaria Kustova offset = self._backing_file_format(self.header, 283*e1232323SMaria Kustova backing_file_fmt) 284*e1232323SMaria Kustova self.feature_name_table, \ 285*e1232323SMaria Kustova offset = self._feature_name_table(self.header, offset) 286*e1232323SMaria Kustova self.end_of_extension_area = self._end_of_extension_area(offset) 287*e1232323SMaria Kustova # Container for entire image 288*e1232323SMaria Kustova self.data = FieldsList() 289*e1232323SMaria Kustova # Percentage of fields will be fuzzed 290*e1232323SMaria Kustova self.bias = random.uniform(0.2, 0.5) 291*e1232323SMaria Kustova 292*e1232323SMaria Kustova def __iter__(self): 293*e1232323SMaria Kustova return iter([self.header, 294*e1232323SMaria Kustova self.backing_file_format, 295*e1232323SMaria Kustova self.feature_name_table, 296*e1232323SMaria Kustova self.end_of_extension_area, 297*e1232323SMaria Kustova self.backing_file_name]) 298*e1232323SMaria Kustova 299*e1232323SMaria Kustova def _join(self): 300*e1232323SMaria Kustova """Join all image structure elements as header, tables, etc in one 301*e1232323SMaria Kustova list of fields. 302*e1232323SMaria Kustova """ 303*e1232323SMaria Kustova if len(self.data) == 0: 304*e1232323SMaria Kustova for v in self: 305*e1232323SMaria Kustova self.data += v 306*e1232323SMaria Kustova 307*e1232323SMaria Kustova def fuzz(self, fields_to_fuzz=None): 308*e1232323SMaria Kustova """Fuzz an image by corrupting values of a random subset of its fields. 309*e1232323SMaria Kustova 310*e1232323SMaria Kustova Without parameters the method fuzzes an entire image. 311*e1232323SMaria Kustova If 'fields_to_fuzz' is specified then only fields in this list will be 312*e1232323SMaria Kustova fuzzed. 'fields_to_fuzz' can contain both individual fields and more 313*e1232323SMaria Kustova general image elements as a header or tables. 314*e1232323SMaria Kustova In the first case the field will be fuzzed always. 315*e1232323SMaria Kustova In the second a random subset of fields will be selected and fuzzed. 316*e1232323SMaria Kustova """ 317*e1232323SMaria Kustova def coin(): 318*e1232323SMaria Kustova """Return boolean value proportional to a portion of fields to be 319*e1232323SMaria Kustova fuzzed. 320*e1232323SMaria Kustova """ 321*e1232323SMaria Kustova return random.random() < self.bias 322*e1232323SMaria Kustova 323*e1232323SMaria Kustova if fields_to_fuzz is None: 324*e1232323SMaria Kustova self._join() 325*e1232323SMaria Kustova for field in self.data: 326*e1232323SMaria Kustova if coin(): 327*e1232323SMaria Kustova field.value = getattr(fuzz, field.name)(field.value) 328*e1232323SMaria Kustova else: 329*e1232323SMaria Kustova for item in fields_to_fuzz: 330*e1232323SMaria Kustova if len(item) == 1: 331*e1232323SMaria Kustova for field in getattr(self, item[0]): 332*e1232323SMaria Kustova if coin(): 333*e1232323SMaria Kustova field.value = getattr(fuzz, 334*e1232323SMaria Kustova field.name)(field.value) 335*e1232323SMaria Kustova else: 336*e1232323SMaria Kustova for field in getattr(self, item[0])[item[1]]: 337*e1232323SMaria Kustova try: 338*e1232323SMaria Kustova field.value = getattr(fuzz, field.name)( 339*e1232323SMaria Kustova field.value) 340*e1232323SMaria Kustova except AttributeError: 341*e1232323SMaria Kustova # Some fields can be skipped depending on 342*e1232323SMaria Kustova # references, e.g. FNT header extension is not 343*e1232323SMaria Kustova # generated for a feature mask header field 344*e1232323SMaria Kustova # equal to zero 345*e1232323SMaria Kustova pass 346*e1232323SMaria Kustova 347*e1232323SMaria Kustova def write(self, filename): 348*e1232323SMaria Kustova """Write an entire image to the file.""" 349*e1232323SMaria Kustova image_file = open(filename, 'w') 350*e1232323SMaria Kustova self._join() 351*e1232323SMaria Kustova for field in self.data: 352*e1232323SMaria Kustova image_file.seek(field.offset) 353*e1232323SMaria Kustova image_file.write(struct.pack(field.fmt, field.value)) 354*e1232323SMaria Kustova image_file.seek(0, 2) 355*e1232323SMaria Kustova size = image_file.tell() 356*e1232323SMaria Kustova rounded = (size + self.cluster_size - 1) & ~(self.cluster_size - 1) 357*e1232323SMaria Kustova if rounded > size: 358*e1232323SMaria Kustova image_file.seek(rounded - 1) 359*e1232323SMaria Kustova image_file.write("\0") 360*e1232323SMaria Kustova image_file.close() 361*e1232323SMaria Kustova 362*e1232323SMaria Kustova 363*e1232323SMaria Kustovadef create_image(test_img_path, backing_file_name=None, backing_file_fmt=None, 364*e1232323SMaria Kustova fields_to_fuzz=None): 365*e1232323SMaria Kustova """Create a fuzzed image and write it to the specified file.""" 366*e1232323SMaria Kustova image = Image(backing_file_name, backing_file_fmt) 367*e1232323SMaria Kustova image.fuzz(fields_to_fuzz) 368*e1232323SMaria Kustova image.write(test_img_path) 369*e1232323SMaria Kustova return image.image_size 370