The original crash detection method is to fork a process to test our new trace input. If the child process exits in time and the second-to-last line is the same as the first crash, we think it is a crash triggered by the same bug. However, in some situations, it doesn't work since it is a hardcoded-offset string comparison.
For example, suppose an assertion failure makes the crash. In that case, the second-to-last line will be 'timeout: the monitored command dumped core', which doesn't contain any information about the assertion failure like where it happened or the assertion statement. This may lead to a minimized input triggers assertion failure but may indicate another bug. As for some sanitizers' crashes, the direct string comparison may stop us from getting a smaller input, since they may have a different leaf stack frame. Perhaps we can detect crashes using both precise output string comparison and rough pattern string match and info the user when the trace input triggers different but a seminar output. Tested: Assertion failure, https://bugs.launchpad.net/qemu/+bug/1908062 AddressSanitizer, https://bugs.launchpad.net/qemu/+bug/1907497 Trace input that doesn't crash Trace input that crashes Qtest Signed-off-by: Qiuhao Li <qiuhao...@outlook.com> --- scripts/oss-fuzz/minimize_qtest_trace.py | 59 ++++++++++++++++++------ 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/scripts/oss-fuzz/minimize_qtest_trace.py b/scripts/oss-fuzz/minimize_qtest_trace.py index 5e405a0d5f..d3b09e6567 100755 --- a/scripts/oss-fuzz/minimize_qtest_trace.py +++ b/scripts/oss-fuzz/minimize_qtest_trace.py @@ -10,11 +10,16 @@ import os import subprocess import time import struct +import re QEMU_ARGS = None QEMU_PATH = None TIMEOUT = 5 -CRASH_TOKEN = None + +crash_patterns = ("Assertion.+failed", + "SUMMARY.+Sanitizer") +crash_pattern = None +crash_string = None write_suffix_lookup = {"b": (1, "B"), "w": (2, "H"), @@ -24,13 +29,12 @@ write_suffix_lookup = {"b": (1, "B"), def usage(): sys.exit("""\ Usage: QEMU_PATH="/path/to/qemu" QEMU_ARGS="args" {} 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= +By default, we will try to search predefined crash patterns through the +tracing output to see whether the crash occred. Optionally, manually set a +string that idenitifes the crash by setting CRASH_PATTERN= """.format((sys.argv[0]))) def check_if_trace_crashes(trace, path): - global CRASH_TOKEN with open(path, "w") as tracefile: tracefile.write("".join(trace)) @@ -42,17 +46,47 @@ def check_if_trace_crashes(trace, path): shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + if rc.returncode == 137: # Timed Out + return False + stdo = rc.communicate()[0] output = stdo.decode('unicode_escape') - if rc.returncode == 137: # Timed Out - return False - if len(output.splitlines()) < 2: + output_lines = output.splitlines() + # Usually we care about the summary info in the last few lines, reverse. + output_lines.reverse() + + global crash_pattern, crash_patterns, crash_string + if crash_pattern is None: # Initialization + for line in output_lines: + for c in crash_patterns: + if re.search(c, line) is not None: + crash_pattern = c + crash_string = line + print("Identifying crash pattern by this string: ",\ + crash_string) + print("Using regex pattern: ", crash_pattern) + return True + print("Failed to initialize crash pattern: no match.") return False - if CRASH_TOKEN is None: - CRASH_TOKEN = output.splitlines()[-2] + # First, we search exactly the previous crash string. + for line in output_lines: + if crash_string == line: + return True + + # Then we decide whether a similar (same pattern) crash happened. + # Slower now :( + for line in output_lines: + if re.search(crash_pattern, line) is not None: + print("\nINFO: The crash string changed during our minimization process.") + print("Before: ", crash_string) + print("After: ", line) + print("The original regex pattern can still match, updated the crash string.") + crash_string = line + return True - return CRASH_TOKEN in output + # The input did not trigger (the same type) bug. + return False def minimize_trace(inpath, outpath): @@ -66,7 +100,6 @@ def minimize_trace(inpath, outpath): 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)) i = 0 newtrace = trace[:] @@ -152,6 +185,6 @@ if __name__ == '__main__': usage() # if "accel" not in QEMU_ARGS: # QEMU_ARGS += " -accel qtest" - CRASH_TOKEN = os.getenv("CRASH_TOKEN") + crash_pattern = os.getenv("CRASH_PATTERN") QEMU_ARGS += " -qtest stdio -monitor none -serial none " minimize_trace(sys.argv[1], sys.argv[2]) -- 2.25.1