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