minimize_qtest_trace.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. This takes a crashing qtest trace and tries to remove superflous operations
  5. """
  6. import sys
  7. import os
  8. import subprocess
  9. import time
  10. import struct
  11. QEMU_ARGS = None
  12. QEMU_PATH = None
  13. TIMEOUT = 5
  14. CRASH_TOKEN = None
  15. # Minimization levels
  16. M1 = False # try removing IO commands iteratively
  17. M2 = False # try setting bits in operand of write/out to zero
  18. write_suffix_lookup = {"b": (1, "B"),
  19. "w": (2, "H"),
  20. "l": (4, "L"),
  21. "q": (8, "Q")}
  22. def usage():
  23. sys.exit("""\
  24. Usage:
  25. QEMU_PATH="/path/to/qemu" QEMU_ARGS="args" {} [Options] input_trace output_trace
  26. By default, will try to use the second-to-last line in the output to identify
  27. whether the crash occred. Optionally, manually set a string that idenitifes the
  28. crash by setting CRASH_TOKEN=
  29. Options:
  30. -M1: enable a loop around the remove minimizer, which may help decrease some
  31. timing dependant instructions. Off by default.
  32. -M2: try setting bits in operand of write/out to zero. Off by default.
  33. """.format((sys.argv[0])))
  34. deduplication_note = """\n\
  35. Note: While trimming the input, sometimes the mutated trace triggers a different
  36. type crash but indicates the same bug. Under this situation, our minimizer is
  37. incapable of recognizing and stopped from removing it. In the future, we may
  38. use a more sophisticated crash case deduplication method.
  39. \n"""
  40. def check_if_trace_crashes(trace, path):
  41. with open(path, "w") as tracefile:
  42. tracefile.write("".join(trace))
  43. rc = subprocess.Popen("timeout -s 9 {timeout}s {qemu_path} {qemu_args} 2>&1\
  44. < {trace_path}".format(timeout=TIMEOUT,
  45. qemu_path=QEMU_PATH,
  46. qemu_args=QEMU_ARGS,
  47. trace_path=path),
  48. shell=True,
  49. stdin=subprocess.PIPE,
  50. stdout=subprocess.PIPE,
  51. encoding="utf-8")
  52. global CRASH_TOKEN
  53. if CRASH_TOKEN is None:
  54. try:
  55. outs, _ = rc.communicate(timeout=5)
  56. CRASH_TOKEN = " ".join(outs.splitlines()[-2].split()[0:3])
  57. except subprocess.TimeoutExpired:
  58. print("subprocess.TimeoutExpired")
  59. return False
  60. print("Identifying Crashes by this string: {}".format(CRASH_TOKEN))
  61. global deduplication_note
  62. print(deduplication_note)
  63. return True
  64. for line in iter(rc.stdout.readline, ""):
  65. if "CLOSED" in line:
  66. return False
  67. if CRASH_TOKEN in line:
  68. return True
  69. print("\nWarning:")
  70. print(" There is no 'CLOSED'or CRASH_TOKEN in the stdout of subprocess.")
  71. print(" Usually this indicates a different type of crash.\n")
  72. return False
  73. # If previous write commands write the same length of data at the same
  74. # interval, we view it as a hint.
  75. def split_write_hint(newtrace, i):
  76. HINT_LEN = 3 # > 2
  77. if i <=(HINT_LEN-1):
  78. return None
  79. #find previous continuous write traces
  80. k = 0
  81. l = i-1
  82. writes = []
  83. while (k != HINT_LEN and l >= 0):
  84. if newtrace[l].startswith("write "):
  85. writes.append(newtrace[l])
  86. k += 1
  87. l -= 1
  88. elif newtrace[l] == "":
  89. l -= 1
  90. else:
  91. return None
  92. if k != HINT_LEN:
  93. return None
  94. length = int(writes[0].split()[2], 16)
  95. for j in range(1, HINT_LEN):
  96. if length != int(writes[j].split()[2], 16):
  97. return None
  98. step = int(writes[0].split()[1], 16) - int(writes[1].split()[1], 16)
  99. for j in range(1, HINT_LEN-1):
  100. if step != int(writes[j].split()[1], 16) - \
  101. int(writes[j+1].split()[1], 16):
  102. return None
  103. return (int(writes[0].split()[1], 16)+step, length)
  104. def remove_lines(newtrace, outpath):
  105. remove_step = 1
  106. i = 0
  107. while i < len(newtrace):
  108. # 1.) Try to remove lines completely and reproduce the crash.
  109. # If it works, we're done.
  110. if (i+remove_step) >= len(newtrace):
  111. remove_step = 1
  112. prior = newtrace[i:i+remove_step]
  113. for j in range(i, i+remove_step):
  114. newtrace[j] = ""
  115. print("Removing {lines} ...\n".format(lines=prior))
  116. if check_if_trace_crashes(newtrace, outpath):
  117. i += remove_step
  118. # Double the number of lines to remove for next round
  119. remove_step *= 2
  120. continue
  121. # Failed to remove multiple IOs, fast recovery
  122. if remove_step > 1:
  123. for j in range(i, i+remove_step):
  124. newtrace[j] = prior[j-i]
  125. remove_step = 1
  126. continue
  127. newtrace[i] = prior[0] # remove_step = 1
  128. # 2.) Try to replace write{bwlq} commands with a write addr, len
  129. # command. Since this can require swapping endianness, try both LE and
  130. # BE options. We do this, so we can "trim" the writes in (3)
  131. if (newtrace[i].startswith("write") and not
  132. newtrace[i].startswith("write ")):
  133. suffix = newtrace[i].split()[0][-1]
  134. assert(suffix in write_suffix_lookup)
  135. addr = int(newtrace[i].split()[1], 16)
  136. value = int(newtrace[i].split()[2], 16)
  137. for endianness in ['<', '>']:
  138. data = struct.pack("{end}{size}".format(end=endianness,
  139. size=write_suffix_lookup[suffix][1]),
  140. value)
  141. newtrace[i] = "write {addr} {size} 0x{data}\n".format(
  142. addr=hex(addr),
  143. size=hex(write_suffix_lookup[suffix][0]),
  144. data=data.hex())
  145. if(check_if_trace_crashes(newtrace, outpath)):
  146. break
  147. else:
  148. newtrace[i] = prior[0]
  149. # 3.) If it is a qtest write command: write addr len data, try to split
  150. # it into two separate write commands. If splitting the data operand
  151. # from length/2^n bytes to the left does not work, try to move the pivot
  152. # to the right side, then add one to n, until length/2^n == 0. The idea
  153. # is to prune unneccessary bytes from long writes, while accommodating
  154. # arbitrary MemoryRegion access sizes and alignments.
  155. # This algorithm will fail under some rare situations.
  156. # e.g., xxxxxxxxxuxxxxxx (u is the unnecessary byte)
  157. if newtrace[i].startswith("write "):
  158. addr = int(newtrace[i].split()[1], 16)
  159. length = int(newtrace[i].split()[2], 16)
  160. data = newtrace[i].split()[3][2:]
  161. if length > 1:
  162. # Can we get a hint from previous writes?
  163. hint = split_write_hint(newtrace, i)
  164. if hint is not None:
  165. hint_addr = hint[0]
  166. hint_len = hint[1]
  167. if hint_addr >= addr and hint_addr+hint_len <= addr+length:
  168. newtrace[i] = "write {addr} {size} 0x{data}\n".format(
  169. addr=hex(hint_addr),
  170. size=hex(hint_len),
  171. data=data[(hint_addr-addr)*2:\
  172. (hint_addr-addr)*2+hint_len*2])
  173. if check_if_trace_crashes(newtrace, outpath):
  174. # next round
  175. i += 1
  176. continue
  177. newtrace[i] = prior[0]
  178. # Try splitting it using a binary approach
  179. leftlength = int(length/2)
  180. rightlength = length - leftlength
  181. newtrace.insert(i+1, "")
  182. power = 1
  183. while leftlength > 0:
  184. newtrace[i] = "write {addr} {size} 0x{data}\n".format(
  185. addr=hex(addr),
  186. size=hex(leftlength),
  187. data=data[:leftlength*2])
  188. newtrace[i+1] = "write {addr} {size} 0x{data}\n".format(
  189. addr=hex(addr+leftlength),
  190. size=hex(rightlength),
  191. data=data[leftlength*2:])
  192. if check_if_trace_crashes(newtrace, outpath):
  193. break
  194. # move the pivot to right side
  195. if leftlength < rightlength:
  196. rightlength, leftlength = leftlength, rightlength
  197. continue
  198. power += 1
  199. leftlength = int(length/pow(2, power))
  200. rightlength = length - leftlength
  201. if check_if_trace_crashes(newtrace, outpath):
  202. i -= 1
  203. else:
  204. newtrace[i] = prior[0]
  205. del newtrace[i+1]
  206. i += 1
  207. def clear_bits(newtrace, outpath):
  208. # try setting bits in operands of out/write to zero
  209. i = 0
  210. while i < len(newtrace):
  211. if (not newtrace[i].startswith("write ") and not
  212. newtrace[i].startswith("out")):
  213. i += 1
  214. continue
  215. # write ADDR SIZE DATA
  216. # outx ADDR VALUE
  217. print("\nzero setting bits: {}".format(newtrace[i]))
  218. prefix = " ".join(newtrace[i].split()[:-1])
  219. data = newtrace[i].split()[-1]
  220. data_bin = bin(int(data, 16))
  221. data_bin_list = list(data_bin)
  222. for j in range(2, len(data_bin_list)):
  223. prior = newtrace[i]
  224. if (data_bin_list[j] == '1'):
  225. data_bin_list[j] = '0'
  226. data_try = hex(int("".join(data_bin_list), 2))
  227. # It seems qtest only accepts padded hex-values.
  228. if len(data_try) % 2 == 1:
  229. data_try = data_try[:2] + "0" + data_try[2:]
  230. newtrace[i] = "{prefix} {data_try}\n".format(
  231. prefix=prefix,
  232. data_try=data_try)
  233. if not check_if_trace_crashes(newtrace, outpath):
  234. data_bin_list[j] = '1'
  235. newtrace[i] = prior
  236. i += 1
  237. def minimize_trace(inpath, outpath):
  238. global TIMEOUT
  239. with open(inpath) as f:
  240. trace = f.readlines()
  241. start = time.time()
  242. if not check_if_trace_crashes(trace, outpath):
  243. sys.exit("The input qtest trace didn't cause a crash...")
  244. end = time.time()
  245. print("Crashed in {} seconds".format(end-start))
  246. TIMEOUT = (end-start)*5
  247. print("Setting the timeout for {} seconds".format(TIMEOUT))
  248. newtrace = trace[:]
  249. global M1, M2
  250. # remove lines
  251. old_len = len(newtrace) + 1
  252. while(old_len > len(newtrace)):
  253. old_len = len(newtrace)
  254. print("trace lenth = ", old_len)
  255. remove_lines(newtrace, outpath)
  256. if not M1 and not M2:
  257. break
  258. newtrace = list(filter(lambda s: s != "", newtrace))
  259. assert(check_if_trace_crashes(newtrace, outpath))
  260. # set bits to zero
  261. if M2:
  262. clear_bits(newtrace, outpath)
  263. assert(check_if_trace_crashes(newtrace, outpath))
  264. if __name__ == '__main__':
  265. if len(sys.argv) < 3:
  266. usage()
  267. if "-M1" in sys.argv:
  268. M1 = True
  269. if "-M2" in sys.argv:
  270. M2 = True
  271. QEMU_PATH = os.getenv("QEMU_PATH")
  272. QEMU_ARGS = os.getenv("QEMU_ARGS")
  273. if QEMU_PATH is None or QEMU_ARGS is None:
  274. usage()
  275. # if "accel" not in QEMU_ARGS:
  276. # QEMU_ARGS += " -accel qtest"
  277. CRASH_TOKEN = os.getenv("CRASH_TOKEN")
  278. QEMU_ARGS += " -qtest stdio -monitor none -serial none "
  279. minimize_trace(sys.argv[-2], sys.argv[-1])