diff options
Diffstat (limited to 'tests/image-fuzzer')
-rw-r--r-- | tests/image-fuzzer/qcow2/fuzz.py | 26 | ||||
-rw-r--r-- | tests/image-fuzzer/qcow2/layout.py | 138 | ||||
-rwxr-xr-x | tests/image-fuzzer/runner.py | 54 |
3 files changed, 182 insertions, 36 deletions
diff --git a/tests/image-fuzzer/qcow2/fuzz.py b/tests/image-fuzzer/qcow2/fuzz.py index 57527f9b4a..20eba6bc1b 100644 --- a/tests/image-fuzzer/qcow2/fuzz.py +++ b/tests/image-fuzzer/qcow2/fuzz.py @@ -18,8 +18,8 @@ import random - UINT8 = 0xff +UINT16 = 0xffff UINT32 = 0xffffffff UINT64 = 0xffffffffffffffff # Most significant bit orders @@ -28,6 +28,8 @@ UINT64_M = 63 # Fuzz vectors UINT8_V = [0, 0x10, UINT8/4, UINT8/2 - 1, UINT8/2, UINT8/2 + 1, UINT8 - 1, UINT8] +UINT16_V = [0, 0x100, 0x1000, UINT16/4, UINT16/2 - 1, UINT16/2, UINT16/2 + 1, + UINT16 - 1, UINT16] UINT32_V = [0, 0x100, 0x1000, 0x10000, 0x100000, UINT32/4, UINT32/2 - 1, UINT32/2, UINT32/2 + 1, UINT32 - 1, UINT32] UINT64_V = UINT32_V + [0x1000000, 0x10000000, 0x100000000, UINT64/4, @@ -332,9 +334,8 @@ def l1_entry(current): constraints = UINT64_V # Reserved bits are ignored # Added a possibility when only flags are fuzzed - offset = 0x7fffffffffffffff & random.choice([selector(current, - constraints), - current]) + offset = 0x7fffffffffffffff & \ + random.choice([selector(current, constraints), current]) is_cow = random.randint(0, 1) return offset + (is_cow << UINT64_M) @@ -344,12 +345,23 @@ def l2_entry(current): constraints = UINT64_V # Reserved bits are ignored # Add a possibility when only flags are fuzzed - offset = 0x3ffffffffffffffe & random.choice([selector(current, - constraints), - current]) + offset = 0x3ffffffffffffffe & \ + random.choice([selector(current, constraints), current]) is_compressed = random.randint(0, 1) is_cow = random.randint(0, 1) is_zero = random.randint(0, 1) value = offset + (is_cow << UINT64_M) + \ (is_compressed << UINT64_M - 1) + is_zero return value + + +def refcount_table_entry(current): + """Fuzz an entry of the refcount table.""" + constraints = UINT64_V + return selector(current, constraints) + + +def refcount_block_entry(current): + """Fuzz an entry of a refcount block.""" + constraints = UINT16_V + return selector(current, constraints) diff --git a/tests/image-fuzzer/qcow2/layout.py b/tests/image-fuzzer/qcow2/layout.py index 730c771d3c..63e801f4e8 100644 --- a/tests/image-fuzzer/qcow2/layout.py +++ b/tests/image-fuzzer/qcow2/layout.py @@ -102,6 +102,8 @@ class Image(object): 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) @@ -113,7 +115,8 @@ class Image(object): 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.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.""" @@ -330,6 +333,138 @@ class Image(object): 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. @@ -471,6 +606,7 @@ def create_image(test_img_path, backing_file_name=None, backing_file_fmt=None, 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 diff --git a/tests/image-fuzzer/runner.py b/tests/image-fuzzer/runner.py index c903c8a342..0a8743ef41 100755 --- a/tests/image-fuzzer/runner.py +++ b/tests/image-fuzzer/runner.py @@ -70,7 +70,7 @@ def run_app(fd, q_args): """Exception for signal.alarm events.""" pass - def handler(*arg): + def handler(*args): """Notify that an alarm event occurred.""" raise Alarm @@ -134,8 +134,8 @@ class TestEnv(object): self.init_path = os.getcwd() self.work_dir = work_dir self.current_dir = os.path.join(work_dir, 'test-' + test_id) - self.qemu_img = os.environ.get('QEMU_IMG', 'qemu-img')\ - .strip().split(' ') + self.qemu_img = \ + os.environ.get('QEMU_IMG', 'qemu-img').strip().split(' ') self.qemu_io = os.environ.get('QEMU_IO', 'qemu-io').strip().split(' ') self.commands = [['qemu-img', 'check', '-f', 'qcow2', '$test_img'], ['qemu-img', 'info', '-f', 'qcow2', '$test_img'], @@ -150,8 +150,7 @@ class TestEnv(object): 'discard $off $len'], ['qemu-io', '$test_img', '-c', 'truncate $off']] - for fmt in ['raw', 'vmdk', 'vdi', 'cow', 'qcow2', 'file', - 'qed', 'vpc']: + for fmt in ['raw', 'vmdk', 'vdi', 'qcow2', 'file', 'qed', 'vpc']: self.commands.append( ['qemu-img', 'convert', '-f', 'qcow2', '-O', fmt, '$test_img', 'converted_image.' + fmt]) @@ -178,7 +177,7 @@ class TestEnv(object): by 'qemu-img create'. """ # All formats supported by the 'qemu-img create' command. - backing_file_fmt = random.choice(['raw', 'vmdk', 'vdi', 'cow', 'qcow2', + backing_file_fmt = random.choice(['raw', 'vmdk', 'vdi', 'qcow2', 'file', 'qed', 'vpc']) backing_file_name = 'backing_img.' + backing_file_fmt backing_file_size = random.randint(MIN_BACKING_FILE_SIZE, @@ -212,10 +211,8 @@ class TestEnv(object): os.chdir(self.current_dir) backing_file_name, backing_file_fmt = self._create_backing_file() - img_size = image_generator.create_image('test.img', - backing_file_name, - backing_file_fmt, - fuzz_config) + img_size = image_generator.create_image( + 'test.img', backing_file_name, backing_file_fmt, fuzz_config) for item in commands: shutil.copy('test.img', 'copy.img') # 'off' and 'len' are multiple of the sector size @@ -228,7 +225,7 @@ class TestEnv(object): elif item[0] == 'qemu-io': current_cmd = list(self.qemu_io) else: - multilog("Warning: test command '%s' is not defined.\n" \ + multilog("Warning: test command '%s' is not defined.\n" % item[0], sys.stderr, self.log, self.parent_log) continue # Replace all placeholders with their real values @@ -244,29 +241,28 @@ class TestEnv(object): "Backing file: %s\n" \ % (self.seed, " ".join(current_cmd), self.current_dir, backing_file_name) - temp_log = StringIO.StringIO() try: retcode = run_app(temp_log, current_cmd) except OSError, e: - multilog(test_summary + "Error: Start of '%s' failed. " \ - "Reason: %s\n\n" % (os.path.basename( - current_cmd[0]), e[1]), + multilog("%sError: Start of '%s' failed. Reason: %s\n\n" + % (test_summary, os.path.basename(current_cmd[0]), + e[1]), sys.stderr, self.log, self.parent_log) raise TestException if retcode < 0: self.log.write(temp_log.getvalue()) - multilog(test_summary + "FAIL: Test terminated by signal " + - "%s\n\n" % str_signal(-retcode), sys.stderr, self.log, - self.parent_log) + multilog("%sFAIL: Test terminated by signal %s\n\n" + % (test_summary, str_signal(-retcode)), + sys.stderr, self.log, self.parent_log) self.failed = True else: if self.log_all: self.log.write(temp_log.getvalue()) - multilog(test_summary + "PASS: Application exited with" + - " the code '%d'\n\n" % retcode, sys.stdout, - self.log, self.parent_log) + multilog("%sPASS: Application exited with the code " \ + "'%d'\n\n" % (test_summary, retcode), + sys.stdout, self.log, self.parent_log) temp_log.close() os.remove('copy.img') @@ -286,8 +282,9 @@ if __name__ == '__main__': Set up test environment in TEST_DIR and run a test in it. A module for test image generation should be specified via IMG_GENERATOR. + Example: - runner.py -c '[["qemu-img", "info", "$test_img"]]' /tmp/test qcow2 + runner.py -c '[["qemu-img", "info", "$test_img"]]' /tmp/test qcow2 Optional arguments: -h, --help display this help and exit @@ -305,20 +302,22 @@ if __name__ == '__main__': '--command' accepts a JSON array of commands. Each command presents an application under test with all its paramaters as a list of strings, - e.g. - ["qemu-io", "$test_img", "-c", "write $off $len"] + e.g. ["qemu-io", "$test_img", "-c", "write $off $len"]. Supported application aliases: 'qemu-img' and 'qemu-io'. + Supported argument aliases: $test_img for the fuzzed image, $off for an offset, $len for length. Values for $off and $len will be generated based on the virtual disk - size of the fuzzed image + size of the fuzzed image. + Paths to 'qemu-img' and 'qemu-io' are retrevied from 'QEMU_IMG' and - 'QEMU_IO' environment variables + 'QEMU_IO' environment variables. '--config' accepts a JSON array of fields to be fuzzed, e.g. - '[["header"], ["header", "version"]]' + '[["header"], ["header", "version"]]'. + Each of the list elements can consist of a complex image element only as ["header"] or ["feature_name_table"] or an exact field as ["header", "version"]. In the first case random portion of the element @@ -368,7 +367,6 @@ if __name__ == '__main__': seed = None config = None duration = None - for opt, arg in opts: if opt in ('-h', '--help'): usage() |