| #!/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 | |
| <details> | |
| ``` | |
| {} | |
| ``` | |
| </details> | |
| ### 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() |