#!/usr/bin/env python3 import argparse import os import re import shlex import sys from subprocess import run, SubprocessError, DEVNULL, PIPE from tempfile import NamedTemporaryFile DESC = """ A `csmith` fuzzing driver for `bindgen`. Generates random C source files with `csmith` and then passes them to `bindgen` (via `predicate.py`). If `bindgen` can't emit bindings, `rustc` can't compile those bindings, or the compiled bindings' layout tests fail, then the driver has found a bug, and will report the problematic test case to you. """ parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description=DESC.strip()) parser.add_argument( "--keep-going", action="store_true", help="Do not stop after finding a test case that exhibits a bug in `bindgen`. Instead, keep going.") CSMITH_ARGS="\ --no-checksum \ --nomain \ --max-block-size 1 \ --max-block-depth 1" parser.add_argument( "--csmith-args", type=str, default=CSMITH_ARGS, help="Pass this argument string to `csmith`. By default, very small functions are generated.") BINDGEN_ARGS = "--with-derive-partialeq \ --with-derive-eq \ --with-derive-partialord \ --with-derive-ord \ --with-derive-hash \ --with-derive-default" parser.add_argument( "--bindgen-args", type=str, default=BINDGEN_ARGS, help="Pass this argument string to `bindgen`. By default, all traits are derived.") parser.add_argument( "--no-creduce", action="store_false", dest="creduce", help="Do not run `creduce` on any buggy test case(s) discovered.") ################################################################################ def cat(path, title=None): if not title: title = path print("-------------------- {} --------------------".format(title)) print() print() run(["cat", path]) def decode(f): return f.decode(encoding="utf-8", errors="ignore") def run_logged(cmd): result = run(cmd, stdin=DEVNULL, stdout=PIPE, stderr=PIPE) result.stdout = decode(result.stdout) result.stderr = decode(result.stderr) if result.returncode != 0: print() print() print("Error: {} exited with code {}".format(cmd, result.returncode)) print() print() for line in result.stdout.splitlines(): sys.stdout.write("+") sys.stdout.write(line) sys.stdout.write("\n") for line in result.stderr.splitlines(): sys.stderr.write("+") sys.stderr.write(line) sys.stderr.write("\n") return result def main(): os.environ["RUST_BACKTRACE"] = "full" args = parser.parse_args() bindgen_args = args.bindgen_args if bindgen_args.find(" -- ") == -1: bindgen_args = bindgen_args + " -- " bindgen_args = bindgen_args + " -I{}".format(os.path.abspath(os.path.dirname(sys.argv[0]))) args.bindgen_args = bindgen_args print() print() print("Fuzzing `bindgen` with C-Smith...") print() print() iterations = 0 while True: print("\rIteration: {}".format(iterations), end="", flush=True) iterations += 1 input = NamedTemporaryFile(delete=False, prefix="input-", suffix=".h") input.close() result = run_logged(["csmith", "-o", input.name] + shlex.split(args.csmith_args)) if result.returncode != 0: exit(1) predicate_command = [ "./predicate.py", "--bindgen-args", args.bindgen_args, input.name ] result = run_logged(predicate_command) if result.returncode != 0: print() print() cat(input.name, title="Failing test case: {}".format(input.name)) print() print() if args.creduce: creduce(args, input.name, result) print_issue_template(args, input.name, predicate_command, result) if args.keep_going: continue exit(1) os.remove(input.name) RUSTC_ERROR_REGEX = re.compile(r".*(error\[.*].*)") LAYOUT_TEST_FAILURE = re.compile(r".*(test bindgen_test_layout_.* \.\.\. FAILED)") def creduce(args, failing_test_case, result): print() print() print("Reducing failing test case with `creduce`...") match = re.search(RUSTC_ERROR_REGEX, result.stderr) if match: error_msg = match.group(1) print("...searching for \"{}\".".format(error_msg)) return creduce_with_predicate_flags( args, failing_test_case, "--bindgen-args '{}' --expect-compile-fail --rustc-grep '{}'".format( args.bindgen_args, re.escape(error_msg) ) ) match = re.search(LAYOUT_TEST_FAILURE, result.stdout) if match: layout_failure = match.group(1) struct_name = layout_failure[len("test bindgen_test_layout_"):layout_failure.rindex(" ... FAILED")] print("...searching for \"{}\".".format(layout_failure)) return creduce_with_predicate_flags( args, failing_test_case, "--bindgen-args '{}' --expect-layout-tests-fail --bindings-grep '{}' --layout-tests-grep '{}'".format( args.bindgen_args, re.escape(struct_name), re.escape(layout_failure) ) ) print("...nevermind, don't know how to `creduce` this bug. Skipping.") def creduce_with_predicate_flags(args, failing_test_case, predicate_flags): predicate = """ #!/usr/bin/env bash set -eu {} {} {} """.format( os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "predicate.py")), predicate_flags, os.path.basename(failing_test_case) ) print("...and reducing with this script:") print() print() print(predicate) print() print() predicate_path = failing_test_case + ".predicate.sh" with open(predicate_path, "w") as p: p.write(predicate) os.chmod(predicate_path, 0o755) creduce_command = ["creduce", "--n", str(os.cpu_count()), predicate_path, failing_test_case] print("Running:", creduce_command) result = run(creduce_command) if result.returncode == 0: print() print() print("`creduce` reduced the failing test case to:") print() print() cat(failing_test_case) print() print() else: print() print() print("`creduce` failed!") if not args.keep_going: sys.exit(1) def print_issue_template(args, failing_test_case, predicate_command, result): test_case_contents = None with open(failing_test_case, "r") as f: test_case_contents = f.read() print(""" ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! File this issue at https://github.com/rust-lang/rust-bindgen/issues/new ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! --------------- 8< --------------- 8< --------------- 8< --------------- This bug was found with `csmith` and `driver.py`. ### Input Header ```c {} ``` ### `bindgen` Invocation ``` $ {} ``` ### Actual Results
``` {} ```
### Expected Results `bindgen` emits bindings OK, then `rustc` compiles those bindings OK, then the compiled bindings' layout tests pass OK. --------------- 8< --------------- 8< --------------- 8< --------------- <3 <3 <3 Thank you! <3 <3 <3 """.format( test_case_contents, " ".join(map(lambda s: "'{}'".format(s), predicate_command)), result.stdout + result.stderr )) if __name__ == "__main__": try: os.chdir(os.path.abspath(os.path.dirname(sys.argv[0]))) main() except KeyboardInterrupt: exit()