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