summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorPeter Maydell2021-01-11 16:15:35 +0100
committerPeter Maydell2021-01-11 16:15:35 +0100
commitb3f846c59d8405bb87c551187721fc92ff2f1b92 (patch)
tree8218e587659d3f163767d4b260d3256328f70163 /scripts
parentMerge remote-tracking branch 'remotes/pmaydell/tags/pull-target-arm-20210108'... (diff)
parentfuzz: map all BARs and enable PCI devices (diff)
downloadqemu-b3f846c59d8405bb87c551187721fc92ff2f1b92.tar.gz
qemu-b3f846c59d8405bb87c551187721fc92ff2f1b92.tar.xz
qemu-b3f846c59d8405bb87c551187721fc92ff2f1b92.zip
Merge remote-tracking branch 'remotes/huth-gitlab/tags/pull-request-2021-01-11v2' into staging
* Fuzzer improvements * Add OpenSUSE leap to the gitlab-CI * Some fixes to get our CI "green" again * Some initial patches to update bsd-user # gpg: Signature made Mon 11 Jan 2021 14:00:07 GMT # gpg: using RSA key 27B88847EEE0250118F3EAB92ED9D774FE702DB5 # gpg: issuer "thuth@redhat.com" # gpg: Good signature from "Thomas Huth <th.huth@gmx.de>" [full] # gpg: aka "Thomas Huth <thuth@redhat.com>" [full] # gpg: aka "Thomas Huth <huth@tuxfamily.org>" [full] # gpg: aka "Thomas Huth <th.huth@posteo.de>" [unknown] # Primary key fingerprint: 27B8 8847 EEE0 2501 18F3 EAB9 2ED9 D774 FE70 2DB5 * remotes/huth-gitlab/tags/pull-request-2021-01-11v2: fuzz: map all BARs and enable PCI devices tests/acceptance: Fix race conditions in s390x tests & skip fedora on gitlab-CI bsd-user: Update strace.list for FreeBSD's latest syscalls bsd-user: move strace OS/arch dependent code to host/arch dirs bsd-user: regenerate FreeBSD's system call numbers fuzz: heuristic split write based on past IOs fuzz: add minimization options fuzz: set bits in operand of write/out to zero fuzz: remove IO commands iteratively fuzz: split write operand using binary approach fuzz: double the IOs to remove for every loop fuzz: accelerate non-crash detection util/oslib-win32: Fix _aligned_malloc() arguments order qtest/libqtest: fix heap-buffer-overflow in qtest_cb_for_every_machine() gitlab-ci.yml: Add openSUSE Leap 15.2 for gitlab CI/CD Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/oss-fuzz/minimize_qtest_trace.py260
1 files changed, 213 insertions, 47 deletions
diff --git a/scripts/oss-fuzz/minimize_qtest_trace.py b/scripts/oss-fuzz/minimize_qtest_trace.py
index 5e405a0d5f..4cba96dee2 100755
--- a/scripts/oss-fuzz/minimize_qtest_trace.py
+++ b/scripts/oss-fuzz/minimize_qtest_trace.py
@@ -16,6 +16,10 @@ QEMU_PATH = None
TIMEOUT = 5
CRASH_TOKEN = None
+# Minimization levels
+M1 = False # try removing IO commands iteratively
+M2 = False # try setting bits in operand of write/out to zero
+
write_suffix_lookup = {"b": (1, "B"),
"w": (2, "H"),
"l": (4, "L"),
@@ -23,14 +27,30 @@ write_suffix_lookup = {"b": (1, "B"),
def usage():
sys.exit("""\
-Usage: QEMU_PATH="/path/to/qemu" QEMU_ARGS="args" {} input_trace output_trace
+Usage:
+
+QEMU_PATH="/path/to/qemu" QEMU_ARGS="args" {} [Options] input_trace output_trace
+
By default, will try to use the second-to-last line in the output to identify
whether the crash occred. Optionally, manually set a string that idenitifes the
crash by setting CRASH_TOKEN=
+
+Options:
+
+-M1: enable a loop around the remove minimizer, which may help decrease some
+ timing dependant instructions. Off by default.
+-M2: try setting bits in operand of write/out to zero. Off by default.
+
""".format((sys.argv[0])))
+deduplication_note = """\n\
+Note: While trimming the input, sometimes the mutated trace triggers a different
+type crash but indicates the same bug. Under this situation, our minimizer is
+incapable of recognizing and stopped from removing it. In the future, we may
+use a more sophisticated crash case deduplication method.
+\n"""
+
def check_if_trace_crashes(trace, path):
- global CRASH_TOKEN
with open(path, "w") as tracefile:
tracefile.write("".join(trace))
@@ -41,51 +61,99 @@ def check_if_trace_crashes(trace, path):
trace_path=path),
shell=True,
stdin=subprocess.PIPE,
- stdout=subprocess.PIPE)
- stdo = rc.communicate()[0]
- output = stdo.decode('unicode_escape')
- if rc.returncode == 137: # Timed Out
- return False
- if len(output.splitlines()) < 2:
- return False
-
+ stdout=subprocess.PIPE,
+ encoding="utf-8")
+ global CRASH_TOKEN
if CRASH_TOKEN is None:
- CRASH_TOKEN = output.splitlines()[-2]
+ try:
+ outs, _ = rc.communicate(timeout=5)
+ CRASH_TOKEN = " ".join(outs.splitlines()[-2].split()[0:3])
+ except subprocess.TimeoutExpired:
+ print("subprocess.TimeoutExpired")
+ return False
+ print("Identifying Crashes by this string: {}".format(CRASH_TOKEN))
+ global deduplication_note
+ print(deduplication_note)
+ return True
- return CRASH_TOKEN in output
+ for line in iter(rc.stdout.readline, ""):
+ if "CLOSED" in line:
+ return False
+ if CRASH_TOKEN in line:
+ return True
+ print("\nWarning:")
+ print(" There is no 'CLOSED'or CRASH_TOKEN in the stdout of subprocess.")
+ print(" Usually this indicates a different type of crash.\n")
+ return False
-def minimize_trace(inpath, outpath):
- global TIMEOUT
- with open(inpath) as f:
- trace = f.readlines()
- start = time.time()
- if not check_if_trace_crashes(trace, outpath):
- sys.exit("The input qtest trace didn't cause a crash...")
- end = time.time()
- print("Crashed in {} seconds".format(end-start))
- TIMEOUT = (end-start)*5
- print("Setting the timeout for {} seconds".format(TIMEOUT))
- print("Identifying Crashes by this string: {}".format(CRASH_TOKEN))
+# If previous write commands write the same length of data at the same
+# interval, we view it as a hint.
+def split_write_hint(newtrace, i):
+ HINT_LEN = 3 # > 2
+ if i <=(HINT_LEN-1):
+ return None
+
+ #find previous continuous write traces
+ k = 0
+ l = i-1
+ writes = []
+ while (k != HINT_LEN and l >= 0):
+ if newtrace[l].startswith("write "):
+ writes.append(newtrace[l])
+ k += 1
+ l -= 1
+ elif newtrace[l] == "":
+ l -= 1
+ else:
+ return None
+ if k != HINT_LEN:
+ return None
+
+ length = int(writes[0].split()[2], 16)
+ for j in range(1, HINT_LEN):
+ if length != int(writes[j].split()[2], 16):
+ return None
+
+ step = int(writes[0].split()[1], 16) - int(writes[1].split()[1], 16)
+ for j in range(1, HINT_LEN-1):
+ if step != int(writes[j].split()[1], 16) - \
+ int(writes[j+1].split()[1], 16):
+ return None
+
+ return (int(writes[0].split()[1], 16)+step, length)
+
+
+def remove_lines(newtrace, outpath):
+ remove_step = 1
i = 0
- newtrace = trace[:]
- # For each line
while i < len(newtrace):
- # 1.) Try to remove it completely and reproduce the crash. If it works,
- # we're done.
- prior = newtrace[i]
- print("Trying to remove {}".format(newtrace[i]))
- # Try to remove the line completely
- newtrace[i] = ""
+ # 1.) Try to remove lines completely and reproduce the crash.
+ # If it works, we're done.
+ if (i+remove_step) >= len(newtrace):
+ remove_step = 1
+ prior = newtrace[i:i+remove_step]
+ for j in range(i, i+remove_step):
+ newtrace[j] = ""
+ print("Removing {lines} ...\n".format(lines=prior))
if check_if_trace_crashes(newtrace, outpath):
- i += 1
+ i += remove_step
+ # Double the number of lines to remove for next round
+ remove_step *= 2
continue
- newtrace[i] = prior
+ # Failed to remove multiple IOs, fast recovery
+ if remove_step > 1:
+ for j in range(i, i+remove_step):
+ newtrace[j] = prior[j-i]
+ remove_step = 1
+ continue
+ newtrace[i] = prior[0] # remove_step = 1
# 2.) Try to replace write{bwlq} commands with a write addr, len
# command. Since this can require swapping endianness, try both LE and
# BE options. We do this, so we can "trim" the writes in (3)
+
if (newtrace[i].startswith("write") and not
newtrace[i].startswith("write ")):
suffix = newtrace[i].split()[0][-1]
@@ -103,22 +171,46 @@ def minimize_trace(inpath, outpath):
if(check_if_trace_crashes(newtrace, outpath)):
break
else:
- newtrace[i] = prior
+ newtrace[i] = prior[0]
# 3.) If it is a qtest write command: write addr len data, try to split
- # it into two separate write commands. If splitting the write down the
- # middle does not work, try to move the pivot "left" and retry, until
- # there is no space left. The idea is to prune unneccessary bytes from
- # long writes, while accommodating arbitrary MemoryRegion access sizes
- # and alignments.
+ # it into two separate write commands. If splitting the data operand
+ # from length/2^n bytes to the left does not work, try to move the pivot
+ # to the right side, then add one to n, until length/2^n == 0. The idea
+ # is to prune unneccessary bytes from long writes, while accommodating
+ # arbitrary MemoryRegion access sizes and alignments.
+
+ # This algorithm will fail under some rare situations.
+ # e.g., xxxxxxxxxuxxxxxx (u is the unnecessary byte)
+
if newtrace[i].startswith("write "):
addr = int(newtrace[i].split()[1], 16)
length = int(newtrace[i].split()[2], 16)
data = newtrace[i].split()[3][2:]
if length > 1:
+
+ # Can we get a hint from previous writes?
+ hint = split_write_hint(newtrace, i)
+ if hint is not None:
+ hint_addr = hint[0]
+ hint_len = hint[1]
+ if hint_addr >= addr and hint_addr+hint_len <= addr+length:
+ newtrace[i] = "write {addr} {size} 0x{data}\n".format(
+ addr=hex(hint_addr),
+ size=hex(hint_len),
+ data=data[(hint_addr-addr)*2:\
+ (hint_addr-addr)*2+hint_len*2])
+ if check_if_trace_crashes(newtrace, outpath):
+ # next round
+ i += 1
+ continue
+ newtrace[i] = prior[0]
+
+ # Try splitting it using a binary approach
leftlength = int(length/2)
rightlength = length - leftlength
newtrace.insert(i+1, "")
+ power = 1
while leftlength > 0:
newtrace[i] = "write {addr} {size} 0x{data}\n".format(
addr=hex(addr),
@@ -130,22 +222,96 @@ def minimize_trace(inpath, outpath):
data=data[leftlength*2:])
if check_if_trace_crashes(newtrace, outpath):
break
- else:
- leftlength -= 1
- rightlength += 1
+ # move the pivot to right side
+ if leftlength < rightlength:
+ rightlength, leftlength = leftlength, rightlength
+ continue
+ power += 1
+ leftlength = int(length/pow(2, power))
+ rightlength = length - leftlength
if check_if_trace_crashes(newtrace, outpath):
i -= 1
else:
- newtrace[i] = prior
+ newtrace[i] = prior[0]
del newtrace[i+1]
i += 1
- check_if_trace_crashes(newtrace, outpath)
+
+
+def clear_bits(newtrace, outpath):
+ # try setting bits in operands of out/write to zero
+ i = 0
+ while i < len(newtrace):
+ if (not newtrace[i].startswith("write ") and not
+ newtrace[i].startswith("out")):
+ i += 1
+ continue
+ # write ADDR SIZE DATA
+ # outx ADDR VALUE
+ print("\nzero setting bits: {}".format(newtrace[i]))
+
+ prefix = " ".join(newtrace[i].split()[:-1])
+ data = newtrace[i].split()[-1]
+ data_bin = bin(int(data, 16))
+ data_bin_list = list(data_bin)
+
+ for j in range(2, len(data_bin_list)):
+ prior = newtrace[i]
+ if (data_bin_list[j] == '1'):
+ data_bin_list[j] = '0'
+ data_try = hex(int("".join(data_bin_list), 2))
+ # It seems qtest only accepts padded hex-values.
+ if len(data_try) % 2 == 1:
+ data_try = data_try[:2] + "0" + data_try[2:-1]
+
+ newtrace[i] = "{prefix} {data_try}\n".format(
+ prefix=prefix,
+ data_try=data_try)
+
+ if not check_if_trace_crashes(newtrace, outpath):
+ data_bin_list[j] = '1'
+ newtrace[i] = prior
+ i += 1
+
+
+def minimize_trace(inpath, outpath):
+ global TIMEOUT
+ with open(inpath) as f:
+ trace = f.readlines()
+ start = time.time()
+ if not check_if_trace_crashes(trace, outpath):
+ sys.exit("The input qtest trace didn't cause a crash...")
+ end = time.time()
+ print("Crashed in {} seconds".format(end-start))
+ TIMEOUT = (end-start)*5
+ print("Setting the timeout for {} seconds".format(TIMEOUT))
+
+ newtrace = trace[:]
+ global M1, M2
+
+ # remove lines
+ old_len = len(newtrace) + 1
+ while(old_len > len(newtrace)):
+ old_len = len(newtrace)
+ print("trace lenth = ", old_len)
+ remove_lines(newtrace, outpath)
+ if not M1 and not M2:
+ break
+ newtrace = list(filter(lambda s: s != "", newtrace))
+ assert(check_if_trace_crashes(newtrace, outpath))
+
+ # set bits to zero
+ if M2:
+ clear_bits(newtrace, outpath)
+ assert(check_if_trace_crashes(newtrace, outpath))
if __name__ == '__main__':
if len(sys.argv) < 3:
usage()
-
+ if "-M1" in sys.argv:
+ M1 = True
+ if "-M2" in sys.argv:
+ M2 = True
QEMU_PATH = os.getenv("QEMU_PATH")
QEMU_ARGS = os.getenv("QEMU_ARGS")
if QEMU_PATH is None or QEMU_ARGS is None:
@@ -154,4 +320,4 @@ if __name__ == '__main__':
# QEMU_ARGS += " -accel qtest"
CRASH_TOKEN = os.getenv("CRASH_TOKEN")
QEMU_ARGS += " -qtest stdio -monitor none -serial none "
- minimize_trace(sys.argv[1], sys.argv[2])
+ minimize_trace(sys.argv[-2], sys.argv[-1])