1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3 4""" 5This takes a crashing qtest trace and tries to remove superflous operations 6""" 7 8import sys 9import os 10import subprocess 11import time 12import struct 13 14QEMU_ARGS = None 15QEMU_PATH = None 16TIMEOUT = 5 17CRASH_TOKEN = None 18 19write_suffix_lookup = {"b": (1, "B"), 20 "w": (2, "H"), 21 "l": (4, "L"), 22 "q": (8, "Q")} 23 24def usage(): 25 sys.exit("""\ 26Usage: QEMU_PATH="/path/to/qemu" QEMU_ARGS="args" {} input_trace output_trace 27By default, will try to use the second-to-last line in the output to identify 28whether the crash occred. Optionally, manually set a string that idenitifes the 29crash by setting CRASH_TOKEN= 30""".format((sys.argv[0]))) 31 32def check_if_trace_crashes(trace, path): 33 global CRASH_TOKEN 34 with open(path, "w") as tracefile: 35 tracefile.write("".join(trace)) 36 37 rc = subprocess.Popen("timeout -s 9 {timeout}s {qemu_path} {qemu_args} 2>&1\ 38 < {trace_path}".format(timeout=TIMEOUT, 39 qemu_path=QEMU_PATH, 40 qemu_args=QEMU_ARGS, 41 trace_path=path), 42 shell=True, 43 stdin=subprocess.PIPE, 44 stdout=subprocess.PIPE) 45 stdo = rc.communicate()[0] 46 output = stdo.decode('unicode_escape') 47 if rc.returncode == 137: # Timed Out 48 return False 49 if len(output.splitlines()) < 2: 50 return False 51 52 if CRASH_TOKEN is None: 53 CRASH_TOKEN = output.splitlines()[-2] 54 55 return CRASH_TOKEN in output 56 57 58def minimize_trace(inpath, outpath): 59 global TIMEOUT 60 with open(inpath) as f: 61 trace = f.readlines() 62 start = time.time() 63 if not check_if_trace_crashes(trace, outpath): 64 sys.exit("The input qtest trace didn't cause a crash...") 65 end = time.time() 66 print("Crashed in {} seconds".format(end-start)) 67 TIMEOUT = (end-start)*5 68 print("Setting the timeout for {} seconds".format(TIMEOUT)) 69 print("Identifying Crashes by this string: {}".format(CRASH_TOKEN)) 70 71 i = 0 72 newtrace = trace[:] 73 # For each line 74 while i < len(newtrace): 75 # 1.) Try to remove it completely and reproduce the crash. If it works, 76 # we're done. 77 prior = newtrace[i] 78 print("Trying to remove {}".format(newtrace[i])) 79 # Try to remove the line completely 80 newtrace[i] = "" 81 if check_if_trace_crashes(newtrace, outpath): 82 i += 1 83 continue 84 newtrace[i] = prior 85 86 # 2.) Try to replace write{bwlq} commands with a write addr, len 87 # command. Since this can require swapping endianness, try both LE and 88 # BE options. We do this, so we can "trim" the writes in (3) 89 if (newtrace[i].startswith("write") and not 90 newtrace[i].startswith("write ")): 91 suffix = newtrace[i].split()[0][-1] 92 assert(suffix in write_suffix_lookup) 93 addr = int(newtrace[i].split()[1], 16) 94 value = int(newtrace[i].split()[2], 16) 95 for endianness in ['<', '>']: 96 data = struct.pack("{end}{size}".format(end=endianness, 97 size=write_suffix_lookup[suffix][1]), 98 value) 99 newtrace[i] = "write {addr} {size} 0x{data}\n".format( 100 addr=hex(addr), 101 size=hex(write_suffix_lookup[suffix][0]), 102 data=data.hex()) 103 if(check_if_trace_crashes(newtrace, outpath)): 104 break 105 else: 106 newtrace[i] = prior 107 108 # 3.) If it is a qtest write command: write addr len data, try to split 109 # it into two separate write commands. If splitting the write down the 110 # middle does not work, try to move the pivot "left" and retry, until 111 # there is no space left. The idea is to prune unneccessary bytes from 112 # long writes, while accommodating arbitrary MemoryRegion access sizes 113 # and alignments. 114 if newtrace[i].startswith("write "): 115 addr = int(newtrace[i].split()[1], 16) 116 length = int(newtrace[i].split()[2], 16) 117 data = newtrace[i].split()[3][2:] 118 if length > 1: 119 leftlength = int(length/2) 120 rightlength = length - leftlength 121 newtrace.insert(i+1, "") 122 while leftlength > 0: 123 newtrace[i] = "write {addr} {size} 0x{data}\n".format( 124 addr=hex(addr), 125 size=hex(leftlength), 126 data=data[:leftlength*2]) 127 newtrace[i+1] = "write {addr} {size} 0x{data}\n".format( 128 addr=hex(addr+leftlength), 129 size=hex(rightlength), 130 data=data[leftlength*2:]) 131 if check_if_trace_crashes(newtrace, outpath): 132 break 133 else: 134 leftlength -= 1 135 rightlength += 1 136 if check_if_trace_crashes(newtrace, outpath): 137 i -= 1 138 else: 139 newtrace[i] = prior 140 del newtrace[i+1] 141 i += 1 142 check_if_trace_crashes(newtrace, outpath) 143 144 145if __name__ == '__main__': 146 if len(sys.argv) < 3: 147 usage() 148 149 QEMU_PATH = os.getenv("QEMU_PATH") 150 QEMU_ARGS = os.getenv("QEMU_ARGS") 151 if QEMU_PATH is None or QEMU_ARGS is None: 152 usage() 153 # if "accel" not in QEMU_ARGS: 154 # QEMU_ARGS += " -accel qtest" 155 CRASH_TOKEN = os.getenv("CRASH_TOKEN") 156 QEMU_ARGS += " -qtest stdio -monitor none -serial none " 157 minimize_trace(sys.argv[1], sys.argv[2]) 158