summaryrefslogblamecommitdiffstats
path: root/scripts/oss-fuzz/output_reproducer.py
blob: 3608b0600e00d737c1eea025e71a2b8fde5a5762 (plain) (tree)






























































































































































                                                                                
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Convert plain qtest traces to C or Bash reproducers

Use this to help build bug-reports or create in-tree reproducers for bugs.
Note: This will not format C code for you. Pipe the output through
clang-format -style="{BasedOnStyle: llvm, IndentWidth: 4, ColumnLimit: 90}"
or similar
"""

import sys
import os
import argparse
import textwrap
from datetime import date

__author__     = "Alexander Bulekov <alxndr@bu.edu>"
__copyright__  = "Copyright (C) 2021, Red Hat, Inc."
__license__    = "GPL version 2 or (at your option) any later version"

__maintainer__ = "Alexander Bulekov"
__email__      = "alxndr@bu.edu"


def c_header(owner):
    return """/*
 * Autogenerated Fuzzer Test Case
 *
 * Copyright (c) {date} {owner}
 *
 * This work is licensed under the terms of the GNU GPL, version 2 or later.
 * See the COPYING file in the top-level directory.
 */

#include "qemu/osdep.h"

#include "libqos/libqtest.h"

    """.format(date=date.today().year, owner=owner)

def c_comment(s):
    """ Return a multi-line C comment. Assume the text is already wrapped """
    return "/*\n * " + "\n * ".join(s.splitlines()) + "\n*/"

def print_c_function(s):
    print("/* ")
    for l in s.splitlines():
        print(" * {}".format(l))

def bash_reproducer(path, args, trace):
    result = '\\\n'.join(textwrap.wrap("cat << EOF | {} {}".format(path, args),
                                       72, break_on_hyphens=False,
                                       drop_whitespace=False))
    for l in trace.splitlines():
        result += "\n" + '\\\n'.join(textwrap.wrap(l,72,drop_whitespace=False))
    result += "\nEOF"
    return result

def c_reproducer(name, args, trace):
    result = []
    result.append("""static void {}(void)\n{{""".format(name))

    # libqtest will add its own qtest args, so get rid of them
    args = args.replace("-accel qtest","")
    args = args.replace(",accel=qtest","")
    args = args.replace("-machine accel=qtest","")
    args = args.replace("-qtest stdio","")
    result.append("""QTestState *s = qtest_init("{}");""".format(args))
    for l in trace.splitlines():
        param = l.split()
        cmd = param[0]
        if cmd == "write":
            buf = param[3][2:] #Get the 0x... buffer and trim the "0x"
            assert len(buf)%2 == 0
            bufbytes = [buf[i:i+2] for i in range(0, len(buf), 2)]
            bufstring = '\\x'+'\\x'.join(bufbytes)
            addr = param[1]
            size = param[2]
            result.append("""qtest_bufwrite(s, {}, "{}", {});""".format(
                          addr, bufstring, size))
        elif cmd.startswith("in") or cmd.startswith("read"):
            result.append("qtest_{}(s, {});".format(
                          cmd, param[1]))
        elif cmd.startswith("out") or cmd.startswith("write"):
            result.append("qtest_{}(s, {}, {});".format(
                          cmd, param[1], param[2]))
        elif cmd == "clock_step":
            if len(param) ==1:
                result.append("qtest_clock_step_next(s);")
            else:
                result.append("qtest_clock_step(s, {});".format(param[1]))
    result.append("qtest_quit(s);\n}")
    return "\n".join(result)

def c_main(name, arch):
    return """int main(int argc, char **argv)
{{
    const char *arch = qtest_get_arch();

    g_test_init(&argc, &argv, NULL);

   if (strcmp(arch, "{arch}") == 0) {{
        qtest_add_func("fuzz/{name}",{name});
   }}

   return g_test_run();
}}""".format(name=name, arch=arch)

def main():
    parser = argparse.ArgumentParser()
    group = parser.add_mutually_exclusive_group()
    group.add_argument("-bash", help="Only output a copy-pastable bash command",
                        action="store_true")
    group.add_argument("-c", help="Only output a c function",
                        action="store_true")
    parser.add_argument('-owner', help="If generating complete C source code, \
                        this specifies the Copyright owner",
                        nargs='?', default="<name of author>")
    parser.add_argument("-no_comment", help="Don't include a bash reproducer \
                        as a comment in the C reproducers",
                        action="store_true")
    parser.add_argument('-name', help="The name of the c function",
                        nargs='?', default="test_fuzz")
    parser.add_argument('input_trace', help="input QTest command sequence \
                        (stdin by default)",
                        nargs='?', type=argparse.FileType('r'),
                        default=sys.stdin)
    args = parser.parse_args()

    qemu_path = os.getenv("QEMU_PATH")
    qemu_args = os.getenv("QEMU_ARGS")
    if not qemu_args or not qemu_path:
        print("Please set QEMU_PATH and QEMU_ARGS environment variables")
        sys.exit(1)

    bash_args = qemu_args
    if " -qtest stdio" not in  qemu_args:
        bash_args += " -qtest stdio"

    arch = qemu_path.split("-")[-1]
    trace = args.input_trace.read().strip()

    if args.bash :
        print(bash_reproducer(qemu_path, bash_args, trace))
    else:
        output = ""
        if not args.c:
            output += c_header(args.owner) + "\n"
        if not args.no_comment:
            output += c_comment(bash_reproducer(qemu_path, bash_args, trace))
        output += c_reproducer(args.name, qemu_args, trace)
        if not args.c:
            output += c_main(args.name, arch)
        print(output)


if __name__ == '__main__':
    main()