• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2
3import argparse
4import os
5import re
6import shlex
7import sys
8from subprocess import run, SubprocessError, DEVNULL, PIPE
9from tempfile import NamedTemporaryFile
10
11DESC = """
12
13A `csmith` fuzzing driver for `bindgen`.
14
15Generates random C source files with `csmith` and then passes them to `bindgen`
16(via `predicate.py`). If `bindgen` can't emit bindings, `rustc` can't compile
17those bindings, or the compiled bindings' layout tests fail, then the driver has
18found a bug, and will report the problematic test case to you.
19
20"""
21
22parser = argparse.ArgumentParser(
23    formatter_class=argparse.RawDescriptionHelpFormatter,
24    description=DESC.strip())
25
26parser.add_argument(
27    "--keep-going",
28    action="store_true",
29    help="Do not stop after finding a test case that exhibits a bug in `bindgen`. Instead, keep going.")
30
31CSMITH_ARGS="\
32--no-checksum \
33--nomain \
34--max-block-size 1 \
35--max-block-depth 1"
36
37parser.add_argument(
38    "--csmith-args",
39    type=str,
40    default=CSMITH_ARGS,
41    help="Pass this argument string to `csmith`. By default, very small functions are generated.")
42
43BINDGEN_ARGS = "--with-derive-partialeq \
44--with-derive-eq \
45--with-derive-partialord \
46--with-derive-ord \
47--with-derive-hash \
48--with-derive-default"
49
50parser.add_argument(
51    "--bindgen-args",
52    type=str,
53    default=BINDGEN_ARGS,
54    help="Pass this argument string to `bindgen`. By default, all traits are derived.")
55
56parser.add_argument(
57    "--no-creduce",
58    action="store_false",
59    dest="creduce",
60    help="Do not run `creduce` on any buggy test case(s) discovered.")
61
62################################################################################
63
64def cat(path, title=None):
65    if not title:
66        title = path
67    print("-------------------- {} --------------------".format(title))
68    print()
69    print()
70    run(["cat", path])
71
72def decode(f):
73    return f.decode(encoding="utf-8", errors="ignore")
74
75def run_logged(cmd):
76    result = run(cmd, stdin=DEVNULL, stdout=PIPE, stderr=PIPE)
77    result.stdout = decode(result.stdout)
78    result.stderr = decode(result.stderr)
79    if result.returncode != 0:
80        print()
81        print()
82        print("Error: {} exited with code {}".format(cmd, result.returncode))
83        print()
84        print()
85        for line in result.stdout.splitlines():
86            sys.stdout.write("+")
87            sys.stdout.write(line)
88            sys.stdout.write("\n")
89        for line in result.stderr.splitlines():
90            sys.stderr.write("+")
91            sys.stderr.write(line)
92            sys.stderr.write("\n")
93    return result
94
95def main():
96    os.environ["RUST_BACKTRACE"] = "full"
97    args = parser.parse_args()
98
99    bindgen_args = args.bindgen_args
100    if bindgen_args.find(" -- ") == -1:
101        bindgen_args = bindgen_args + " -- "
102    bindgen_args = bindgen_args + " -I{}".format(os.path.abspath(os.path.dirname(sys.argv[0])))
103    args.bindgen_args = bindgen_args
104
105    print()
106    print()
107    print("Fuzzing `bindgen` with C-Smith...")
108    print()
109    print()
110
111    iterations = 0
112    while True:
113        print("\rIteration: {}".format(iterations), end="", flush=True)
114        iterations += 1
115
116        input = NamedTemporaryFile(delete=False, prefix="input-", suffix=".h")
117        input.close()
118        result = run_logged(["csmith", "-o", input.name] + shlex.split(args.csmith_args))
119        if result.returncode != 0:
120            exit(1)
121
122        predicate_command = [
123            "./predicate.py",
124            "--bindgen-args",
125            args.bindgen_args,
126            input.name
127        ]
128        result = run_logged(predicate_command)
129
130        if result.returncode != 0:
131            print()
132            print()
133            cat(input.name, title="Failing test case: {}".format(input.name))
134            print()
135            print()
136
137            if args.creduce:
138                creduce(args, input.name, result)
139
140            print_issue_template(args, input.name, predicate_command, result)
141
142            if args.keep_going:
143                continue
144            exit(1)
145
146        os.remove(input.name)
147
148RUSTC_ERROR_REGEX = re.compile(r".*(error\[.*].*)")
149LAYOUT_TEST_FAILURE = re.compile(r".*(test bindgen_test_layout_.* \.\.\. FAILED)")
150
151def creduce(args, failing_test_case, result):
152    print()
153    print()
154    print("Reducing failing test case with `creduce`...")
155
156    match = re.search(RUSTC_ERROR_REGEX, result.stderr)
157    if match:
158        error_msg = match.group(1)
159        print("...searching for \"{}\".".format(error_msg))
160        return creduce_with_predicate_flags(
161            args,
162            failing_test_case,
163            "--bindgen-args '{}' --expect-compile-fail --rustc-grep '{}'".format(
164                args.bindgen_args,
165                re.escape(error_msg)
166            )
167        )
168
169    match = re.search(LAYOUT_TEST_FAILURE, result.stdout)
170    if match:
171        layout_failure = match.group(1)
172        struct_name = layout_failure[len("test bindgen_test_layout_"):layout_failure.rindex(" ... FAILED")]
173        print("...searching for \"{}\".".format(layout_failure))
174        return creduce_with_predicate_flags(
175            args,
176            failing_test_case,
177            "--bindgen-args '{}' --expect-layout-tests-fail --bindings-grep '{}' --layout-tests-grep '{}'".format(
178                args.bindgen_args,
179                re.escape(struct_name),
180                re.escape(layout_failure)
181            )
182        )
183
184    print("...nevermind, don't know how to `creduce` this bug. Skipping.")
185
186def creduce_with_predicate_flags(args, failing_test_case, predicate_flags):
187    predicate = """
188#!/usr/bin/env bash
189set -eu
190{} {} {}
191    """.format(
192        os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "predicate.py")),
193        predicate_flags,
194        os.path.basename(failing_test_case)
195    )
196
197    print("...and reducing with this script:")
198    print()
199    print()
200    print(predicate)
201    print()
202    print()
203
204    predicate_path = failing_test_case + ".predicate.sh"
205    with open(predicate_path, "w") as p:
206        p.write(predicate)
207    os.chmod(predicate_path, 0o755)
208
209    creduce_command = ["creduce", "--n", str(os.cpu_count()), predicate_path, failing_test_case]
210    print("Running:", creduce_command)
211    result = run(creduce_command)
212    if result.returncode == 0:
213        print()
214        print()
215        print("`creduce` reduced the failing test case to:")
216        print()
217        print()
218        cat(failing_test_case)
219        print()
220        print()
221    else:
222        print()
223        print()
224        print("`creduce` failed!")
225        if not args.keep_going:
226            sys.exit(1)
227
228def print_issue_template(args, failing_test_case, predicate_command, result):
229    test_case_contents = None
230    with open(failing_test_case, "r") as f:
231        test_case_contents = f.read()
232
233    print("""
234
235! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
236! File this issue at https://github.com/rust-lang/rust-bindgen/issues/new !
237! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
238
239   --------------- 8< --------------- 8< --------------- 8< ---------------
240
241This bug was found with `csmith` and `driver.py`.
242
243### Input Header
244
245```c
246{}
247```
248
249### `bindgen` Invocation
250
251```
252$ {}
253```
254
255### Actual Results
256
257<details>
258
259```
260{}
261```
262
263</details>
264
265### Expected Results
266
267`bindgen` emits bindings OK, then `rustc` compiles those bindings OK, then the
268compiled bindings' layout tests pass OK.
269
270   --------------- 8< --------------- 8< --------------- 8< ---------------
271
272                         <3 <3 <3 Thank you! <3 <3 <3
273    """.format(
274        test_case_contents,
275        " ".join(map(lambda s: "'{}'".format(s), predicate_command)),
276        result.stdout + result.stderr
277    ))
278
279if __name__ == "__main__":
280    try:
281        os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
282        main()
283    except KeyboardInterrupt:
284        exit()
285