diff options
| author | Peter Maydell | 2021-01-11 16:15:35 +0100 |
|---|---|---|
| committer | Peter Maydell | 2021-01-11 16:15:35 +0100 |
| commit | b3f846c59d8405bb87c551187721fc92ff2f1b92 (patch) | |
| tree | 8218e587659d3f163767d4b260d3256328f70163 /scripts | |
| parent | Merge remote-tracking branch 'remotes/pmaydell/tags/pull-target-arm-20210108'... (diff) | |
| parent | fuzz: map all BARs and enable PCI devices (diff) | |
| download | qemu-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-x | scripts/oss-fuzz/minimize_qtest_trace.py | 260 |
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]) |
