1#!/usr/bin/env python3 2 3""" 4Convert plain qtest traces to C or Bash reproducers 5 6Use this to help build bug-reports or create in-tree reproducers for bugs. 7Note: This will not format C code for you. Pipe the output through 8clang-format -style="{BasedOnStyle: llvm, IndentWidth: 4, ColumnLimit: 90}" 9or similar 10""" 11 12import sys 13import os 14import argparse 15import textwrap 16from datetime import date 17 18__author__ = "Alexander Bulekov <alxndr@bu.edu>" 19__copyright__ = "Copyright (C) 2021, Red Hat, Inc." 20__license__ = "GPL version 2 or (at your option) any later version" 21 22__maintainer__ = "Alexander Bulekov" 23__email__ = "alxndr@bu.edu" 24 25 26def c_header(owner): 27 return """/* 28 * Autogenerated Fuzzer Test Case 29 * 30 * Copyright (c) {date} {owner} 31 * 32 * This work is licensed under the terms of the GNU GPL, version 2 or later. 33 * See the COPYING file in the top-level directory. 34 */ 35 36#include "qemu/osdep.h" 37 38#include "libqtest.h" 39 40 """.format(date=date.today().year, owner=owner) 41 42def c_comment(s): 43 """ Return a multi-line C comment. Assume the text is already wrapped """ 44 return "/*\n * " + "\n * ".join(s.splitlines()) + "\n*/" 45 46def print_c_function(s): 47 print("/* ") 48 for l in s.splitlines(): 49 print(" * {}".format(l)) 50 51def bash_reproducer(path, args, trace): 52 result = '\\\n'.join(textwrap.wrap("cat << EOF | {} {}".format(path, args), 53 72, break_on_hyphens=False, 54 drop_whitespace=False)) 55 for l in trace.splitlines(): 56 result += "\n" + '\\\n'.join(textwrap.wrap(l,72,drop_whitespace=False)) 57 result += "\nEOF" 58 return result 59 60def c_reproducer(name, args, trace): 61 result = [] 62 result.append("""static void {}(void)\n{{""".format(name)) 63 64 # libqtest will add its own qtest args, so get rid of them 65 args = args.replace("-accel qtest","") 66 args = args.replace(",accel=qtest","") 67 args = args.replace("-machine accel=qtest","") 68 args = args.replace("-qtest stdio","") 69 result.append("""QTestState *s = qtest_init("{}");""".format(args)) 70 for l in trace.splitlines(): 71 param = l.split() 72 cmd = param[0] 73 if cmd == "write": 74 buf = param[3][2:] #Get the 0x... buffer and trim the "0x" 75 assert len(buf)%2 == 0 76 bufbytes = [buf[i:i+2] for i in range(0, len(buf), 2)] 77 bufstring = '\\x'+'\\x'.join(bufbytes) 78 addr = param[1] 79 size = param[2] 80 result.append("""qtest_bufwrite(s, {}, "{}", {});""".format( 81 addr, bufstring, size)) 82 elif cmd.startswith("in") or cmd.startswith("read"): 83 result.append("qtest_{}(s, {});".format( 84 cmd, param[1])) 85 elif cmd.startswith("out") or cmd.startswith("write"): 86 result.append("qtest_{}(s, {}, {});".format( 87 cmd, param[1], param[2])) 88 elif cmd == "clock_step": 89 if len(param) ==1: 90 result.append("qtest_clock_step_next(s);") 91 else: 92 result.append("qtest_clock_step(s, {});".format(param[1])) 93 result.append("qtest_quit(s);\n}") 94 return "\n".join(result) 95 96def c_main(name, arch): 97 return """int main(int argc, char **argv) 98{{ 99 const char *arch = qtest_get_arch(); 100 101 g_test_init(&argc, &argv, NULL); 102 103 if (strcmp(arch, "{arch}") == 0) {{ 104 qtest_add_func("fuzz/{name}",{name}); 105 }} 106 107 return g_test_run(); 108}}""".format(name=name, arch=arch) 109 110def main(): 111 parser = argparse.ArgumentParser() 112 group = parser.add_mutually_exclusive_group() 113 group.add_argument("-bash", help="Only output a copy-pastable bash command", 114 action="store_true") 115 group.add_argument("-c", help="Only output a c function", 116 action="store_true") 117 parser.add_argument('-owner', help="If generating complete C source code, \ 118 this specifies the Copyright owner", 119 nargs='?', default="<name of author>") 120 parser.add_argument("-no_comment", help="Don't include a bash reproducer \ 121 as a comment in the C reproducers", 122 action="store_true") 123 parser.add_argument('-name', help="The name of the c function", 124 nargs='?', default="test_fuzz") 125 parser.add_argument('input_trace', help="input QTest command sequence \ 126 (stdin by default)", 127 nargs='?', type=argparse.FileType('r'), 128 default=sys.stdin) 129 args = parser.parse_args() 130 131 qemu_path = os.getenv("QEMU_PATH") 132 qemu_args = os.getenv("QEMU_ARGS") 133 if not qemu_args or not qemu_path: 134 print("Please set QEMU_PATH and QEMU_ARGS environment variables") 135 sys.exit(1) 136 137 bash_args = qemu_args 138 if " -qtest stdio" not in qemu_args: 139 bash_args += " -qtest stdio" 140 141 arch = qemu_path.split("-")[-1] 142 trace = args.input_trace.read().strip() 143 144 if args.bash : 145 print(bash_reproducer(qemu_path, bash_args, trace)) 146 else: 147 output = "" 148 if not args.c: 149 output += c_header(args.owner) + "\n" 150 if not args.no_comment: 151 output += c_comment(bash_reproducer(qemu_path, bash_args, trace)) 152 output += c_reproducer(args.name, qemu_args, trace) 153 if not args.c: 154 output += c_main(args.name, arch) 155 print(output) 156 157 158if __name__ == '__main__': 159 main() 160