xref: /openbmc/qemu/scripts/oss-fuzz/output_reproducer.py (revision 1abdde1ad42d0ebccc5e8bc574ebe805cd650102)
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