1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4"""
5Use this to convert qtest log info from a generic fuzzer input into a qtest
6trace that you can feed into a standard qemu-system process. Example usage:
7
8QEMU_FUZZ_ARGS="-machine q35,accel=qtest" QEMU_FUZZ_OBJECTS="*" \
9        ./i386-softmmu/qemu-fuzz-i386 --fuzz-target=generic-pci-fuzz
10# .. Finds some crash
11QTEST_LOG=1 FUZZ_SERIALIZE_QTEST=1 \
12QEMU_FUZZ_ARGS="-machine q35,accel=qtest" QEMU_FUZZ_OBJECTS="*" \
13        ./i386-softmmu/qemu-fuzz-i386 --fuzz-target=generic-pci-fuzz
14        /path/to/crash 2> qtest_log_output
15scripts/oss-fuzz/reorder_fuzzer_qtest_trace.py qtest_log_output > qtest_trace
16./i386-softmmu/qemu-fuzz-i386 -machine q35,accel=qtest \
17        -qtest stdio < qtest_trace
18
19### Details ###
20
21Some fuzzer make use of hooks that allow us to populate some memory range, just
22before a DMA read from that range. This means that the fuzzer can produce
23activity that looks like:
24    [start] read from mmio addr
25    [end]   read from mmio addr
26    [start] write to pio addr
27        [start] fill a DMA buffer just in time
28        [end]   fill a DMA buffer just in time
29        [start] fill a DMA buffer just in time
30        [end]   fill a DMA buffer just in time
31    [end]   write to pio addr
32    [start] read from mmio addr
33    [end]   read from mmio addr
34
35We annotate these "nested" DMA writes, so with QTEST_LOG=1 the QTest trace
36might look something like:
37[R +0.028431] readw 0x10000
38[R +0.028434] outl 0xc000 0xbeef  # Triggers a DMA read from 0xbeef and 0xbf00
39[DMA][R +0.034639] write 0xbeef 0x2 0xAAAA
40[DMA][R +0.034639] write 0xbf00 0x2 0xBBBB
41[R +0.028431] readw 0xfc000
42
43This script would reorder the above trace so it becomes:
44readw 0x10000
45write 0xbeef 0x2 0xAAAA
46write 0xbf00 0x2 0xBBBB
47outl 0xc000 0xbeef
48readw 0xfc000
49
50I.e. by the time, 0xc000 tries to read from DMA, those DMA buffers have already
51been set up, removing the need for the DMA hooks. We can simply provide this
52reordered trace via -qtest stdio to reproduce the input
53
54Note: this won't work for traces where the device tries to read from the same
55DMA region twice in between MMIO/PIO commands. E.g:
56    [R +0.028434] outl 0xc000 0xbeef
57    [DMA][R +0.034639] write 0xbeef 0x2 0xAAAA
58    [DMA][R +0.034639] write 0xbeef 0x2 0xBBBB
59
60The fuzzer will annotate suspected double-fetches with [DOUBLE-FETCH]. This
61script looks for these tags and warns the users that the resulting trace might
62not reproduce the bug.
63"""
64
65import sys
66
67__author__     = "Alexander Bulekov <alxndr@bu.edu>"
68__copyright__  = "Copyright (C) 2020, Red Hat, Inc."
69__license__    = "GPL version 2 or (at your option) any later version"
70
71__maintainer__ = "Alexander Bulekov"
72__email__      = "alxndr@bu.edu"
73
74
75def usage():
76    sys.exit("Usage: {} /path/to/qtest_log_output".format((sys.argv[0])))
77
78
79def main(filename):
80    with open(filename, "r") as f:
81        trace = f.readlines()
82
83    # Leave only lines that look like logged qtest commands
84    trace[:] = [x.strip() for x in trace if "[R +" in x
85                or "[S +" in x and "CLOSED" not in x]
86
87    for i in range(len(trace)):
88        if i+1 < len(trace):
89            if "[DMA]" in trace[i+1]:
90                if "[DOUBLE-FETCH]" in trace[i+1]:
91                    sys.stderr.write("Warning: Likely double fetch on line"
92                                     "{}.\n There will likely be problems "
93                                     "reproducing behavior with the "
94                                     "resulting qtest trace\n\n".format(i+1))
95                trace[i], trace[i+1] = trace[i+1], trace[i]
96    for line in trace:
97        print(line.split("]")[-1].strip())
98
99
100if __name__ == '__main__':
101    if len(sys.argv) == 1:
102        usage()
103    main(sys.argv[1])
104