1#!/usr/bin/env python 2# Copyright 2016 Google Inc. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15# 16################################################################################ 17"""Helper script for OSS-Fuzz users. Can do common tasks like building 18projects/fuzzers, running them etc.""" 19 20from __future__ import print_function 21from multiprocessing.dummy import Pool as ThreadPool 22import argparse 23import datetime 24import errno 25import multiprocessing 26import os 27import pipes 28import re 29import subprocess 30import sys 31import templates 32 33OSSFUZZ_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) 34BUILD_DIR = os.path.join(OSSFUZZ_DIR, 'build') 35 36BASE_IMAGES = [ 37 'gcr.io/oss-fuzz-base/base-image', 38 'gcr.io/oss-fuzz-base/base-clang', 39 'gcr.io/oss-fuzz-base/base-builder', 40 'gcr.io/oss-fuzz-base/base-runner', 41 'gcr.io/oss-fuzz-base/base-runner-debug', 42 'gcr.io/oss-fuzz-base/base-msan-builder', 43 'gcr.io/oss-fuzz-base/msan-builder', 44] 45 46VALID_PROJECT_NAME_REGEX = re.compile(r'^[a-zA-Z0-9_-]+$') 47MAX_PROJECT_NAME_LENGTH = 26 48 49if sys.version_info[0] >= 3: 50 raw_input = input # pylint: disable=invalid-name 51 52CORPUS_URL_FORMAT = ( 53 'gs://{project_name}-corpus.clusterfuzz-external.appspot.com/libFuzzer/' 54 '{fuzz_target}/') 55CORPUS_BACKUP_URL_FORMAT = ( 56 'gs://{project_name}-backup.clusterfuzz-external.appspot.com/corpus/' 57 'libFuzzer/{fuzz_target}/') 58 59 60def main(): # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements 61 """Get subcommand from program arguments and do it.""" 62 os.chdir(OSSFUZZ_DIR) 63 if not os.path.exists(BUILD_DIR): 64 os.mkdir(BUILD_DIR) 65 66 parser = argparse.ArgumentParser('helper.py', description='oss-fuzz helpers') 67 subparsers = parser.add_subparsers(dest='command') 68 69 generate_parser = subparsers.add_parser( 70 'generate', help='Generate files for new project.') 71 generate_parser.add_argument('project_name') 72 73 build_image_parser = subparsers.add_parser('build_image', 74 help='Build an image.') 75 build_image_parser.add_argument('project_name') 76 build_image_parser.add_argument('--pull', 77 action='store_true', 78 help='Pull latest base image.') 79 build_image_parser.add_argument('--no-pull', 80 action='store_true', 81 help='Do not pull latest base image.') 82 83 build_fuzzers_parser = subparsers.add_parser( 84 'build_fuzzers', help='Build fuzzers for a project.') 85 _add_architecture_args(build_fuzzers_parser) 86 _add_engine_args(build_fuzzers_parser) 87 _add_sanitizer_args(build_fuzzers_parser) 88 _add_environment_args(build_fuzzers_parser) 89 build_fuzzers_parser.add_argument('project_name') 90 build_fuzzers_parser.add_argument('source_path', 91 help='path of local source', 92 nargs='?') 93 build_fuzzers_parser.add_argument('--clean', 94 dest='clean', 95 action='store_true', 96 help='clean existing artifacts.') 97 build_fuzzers_parser.add_argument('--no-clean', 98 dest='clean', 99 action='store_false', 100 help='do not clean existing artifacts ' 101 '(default).') 102 build_fuzzers_parser.set_defaults(clean=False) 103 104 check_build_parser = subparsers.add_parser( 105 'check_build', help='Checks that fuzzers execute without errors.') 106 _add_architecture_args(check_build_parser) 107 _add_engine_args(check_build_parser, choices=['libfuzzer', 'afl', 'dataflow']) 108 _add_sanitizer_args(check_build_parser, 109 choices=['address', 'memory', 'undefined', 'dataflow']) 110 _add_environment_args(check_build_parser) 111 check_build_parser.add_argument('project_name', help='name of the project') 112 check_build_parser.add_argument('fuzzer_name', 113 help='name of the fuzzer', 114 nargs='?') 115 116 run_fuzzer_parser = subparsers.add_parser( 117 'run_fuzzer', help='Run a fuzzer in the emulated fuzzing environment.') 118 _add_engine_args(run_fuzzer_parser) 119 _add_sanitizer_args(run_fuzzer_parser) 120 _add_environment_args(run_fuzzer_parser) 121 run_fuzzer_parser.add_argument('project_name', help='name of the project') 122 run_fuzzer_parser.add_argument('fuzzer_name', help='name of the fuzzer') 123 run_fuzzer_parser.add_argument('fuzzer_args', 124 help='arguments to pass to the fuzzer', 125 nargs=argparse.REMAINDER) 126 127 coverage_parser = subparsers.add_parser( 128 'coverage', help='Generate code coverage report for the project.') 129 coverage_parser.add_argument('--no-corpus-download', 130 action='store_true', 131 help='do not download corpus backup from ' 132 'OSS-Fuzz; use corpus located in ' 133 'build/corpus/<project>/<fuzz_target>/') 134 coverage_parser.add_argument('--port', 135 default='8008', 136 help='specify port for' 137 ' a local HTTP server rendering coverage report') 138 coverage_parser.add_argument('--fuzz-target', 139 help='specify name of a fuzz ' 140 'target to be run for generating coverage ' 141 'report') 142 coverage_parser.add_argument('--corpus-dir', 143 help='specify location of corpus' 144 ' to be used (requires --fuzz-target argument)') 145 coverage_parser.add_argument('project_name', help='name of the project') 146 coverage_parser.add_argument('extra_args', 147 help='additional arguments to ' 148 'pass to llvm-cov utility.', 149 nargs='*') 150 151 download_corpora_parser = subparsers.add_parser( 152 'download_corpora', help='Download all corpora for a project.') 153 download_corpora_parser.add_argument('--fuzz-target', 154 help='specify name of a fuzz target') 155 download_corpora_parser.add_argument('project_name', 156 help='name of the project') 157 158 reproduce_parser = subparsers.add_parser('reproduce', 159 help='Reproduce a crash.') 160 reproduce_parser.add_argument('--valgrind', 161 action='store_true', 162 help='run with valgrind') 163 reproduce_parser.add_argument('project_name', help='name of the project') 164 reproduce_parser.add_argument('fuzzer_name', help='name of the fuzzer') 165 reproduce_parser.add_argument('testcase_path', help='path of local testcase') 166 reproduce_parser.add_argument('fuzzer_args', 167 help='arguments to pass to the fuzzer', 168 nargs=argparse.REMAINDER) 169 _add_environment_args(reproduce_parser) 170 171 shell_parser = subparsers.add_parser( 172 'shell', help='Run /bin/bash within the builder container.') 173 shell_parser.add_argument('project_name', help='name of the project') 174 _add_architecture_args(shell_parser) 175 _add_engine_args(shell_parser) 176 _add_sanitizer_args(shell_parser) 177 _add_environment_args(shell_parser) 178 179 subparsers.add_parser('pull_images', help='Pull base images.') 180 181 args = parser.parse_args() 182 183 # We have different default values for `sanitizer` depending on the `engine`. 184 # Some commands do not have `sanitizer` argument, so `hasattr` is necessary. 185 if hasattr(args, 'sanitizer') and not args.sanitizer: 186 if args.engine == 'dataflow': 187 args.sanitizer = 'dataflow' 188 else: 189 args.sanitizer = 'address' 190 191 if args.command == 'generate': 192 return generate(args) 193 if args.command == 'build_image': 194 return build_image(args) 195 if args.command == 'build_fuzzers': 196 return build_fuzzers(args) 197 if args.command == 'check_build': 198 return check_build(args) 199 if args.command == 'download_corpora': 200 return download_corpora(args) 201 if args.command == 'run_fuzzer': 202 return run_fuzzer(args) 203 if args.command == 'coverage': 204 return coverage(args) 205 if args.command == 'reproduce': 206 return reproduce(args) 207 if args.command == 'shell': 208 return shell(args) 209 if args.command == 'pull_images': 210 return pull_images(args) 211 212 return 0 213 214 215def is_base_image(image_name): 216 """Checks if the image name is a base image.""" 217 return os.path.exists(os.path.join('infra', 'base-images', image_name)) 218 219 220def check_project_exists(project_name): 221 """Checks if a project exists.""" 222 if not os.path.exists(_get_project_dir(project_name)): 223 print(project_name, 'does not exist', file=sys.stderr) 224 return False 225 226 return True 227 228 229def _check_fuzzer_exists(project_name, fuzzer_name): 230 """Checks if a fuzzer exists.""" 231 command = ['docker', 'run', '--rm'] 232 command.extend(['-v', '%s:/out' % _get_output_dir(project_name)]) 233 command.append('ubuntu:16.04') 234 235 command.extend(['/bin/bash', '-c', 'test -f /out/%s' % fuzzer_name]) 236 237 try: 238 subprocess.check_call(command) 239 except subprocess.CalledProcessError: 240 print(fuzzer_name, 241 'does not seem to exist. Please run build_fuzzers first.', 242 file=sys.stderr) 243 return False 244 245 return True 246 247 248def _get_absolute_path(path): 249 """Returns absolute path with user expansion.""" 250 return os.path.abspath(os.path.expanduser(path)) 251 252 253def _get_command_string(command): 254 """Returns a shell escaped command string.""" 255 return ' '.join(pipes.quote(part) for part in command) 256 257 258def _get_project_dir(project_name): 259 """Returns path to the project.""" 260 return os.path.join(OSSFUZZ_DIR, 'projects', project_name) 261 262 263def get_dockerfile_path(project_name): 264 """Returns path to the project Dockerfile.""" 265 return os.path.join(_get_project_dir(project_name), 'Dockerfile') 266 267 268def _get_corpus_dir(project_name=''): 269 """Returns path to /corpus directory for the given project (if specified).""" 270 return os.path.join(BUILD_DIR, 'corpus', project_name) 271 272 273def _get_output_dir(project_name=''): 274 """Returns path to /out directory for the given project (if specified).""" 275 return os.path.join(BUILD_DIR, 'out', project_name) 276 277 278def _get_work_dir(project_name=''): 279 """Returns path to /work directory for the given project (if specified).""" 280 return os.path.join(BUILD_DIR, 'work', project_name) 281 282 283def _add_architecture_args(parser, choices=('x86_64', 'i386')): 284 """Add common architecture args.""" 285 parser.add_argument('--architecture', default='x86_64', choices=choices) 286 287 288def _add_engine_args(parser, 289 choices=('libfuzzer', 'afl', 'honggfuzz', 'dataflow', 290 'none')): 291 """Add common engine args.""" 292 parser.add_argument('--engine', default='libfuzzer', choices=choices) 293 294 295def _add_sanitizer_args(parser, 296 choices=('address', 'memory', 'undefined', 'coverage', 297 'dataflow')): 298 """Add common sanitizer args.""" 299 parser.add_argument( 300 '--sanitizer', 301 default=None, 302 choices=choices, 303 help='the default is "address"; "dataflow" for "dataflow" engine') 304 305 306def _add_environment_args(parser): 307 """Add common environment args.""" 308 parser.add_argument('-e', 309 action='append', 310 help="set environment variable e.g. VAR=value") 311 312 313def build_image_impl(image_name, no_cache=False, pull=False): 314 """Build image.""" 315 316 proj_is_base_image = is_base_image(image_name) 317 if proj_is_base_image: 318 image_project = 'oss-fuzz-base' 319 dockerfile_dir = os.path.join('infra', 'base-images', image_name) 320 else: 321 image_project = 'oss-fuzz' 322 if not check_project_exists(image_name): 323 return False 324 325 dockerfile_dir = os.path.join('projects', image_name) 326 327 build_args = [] 328 if no_cache: 329 build_args.append('--no-cache') 330 331 build_args += [ 332 '-t', 'gcr.io/%s/%s' % (image_project, image_name), dockerfile_dir 333 ] 334 335 return docker_build(build_args, pull=pull) 336 337 338def _env_to_docker_args(env_list): 339 """Turn envirnoment variable list into docker arguments.""" 340 return sum([['-e', v] for v in env_list], []) 341 342 343WORKDIR_REGEX = re.compile(r'\s*WORKDIR\s*([^\s]+)') 344 345 346def _workdir_from_dockerfile(project_name): 347 """Parse WORKDIR from the Dockerfile for the given project.""" 348 dockerfile_path = get_dockerfile_path(project_name) 349 350 with open(dockerfile_path) as file_handle: 351 lines = file_handle.readlines() 352 353 for line in reversed(lines): # reversed to get last WORKDIR. 354 match = re.match(WORKDIR_REGEX, line) 355 if match: 356 workdir = match.group(1) 357 workdir = workdir.replace('$SRC', '/src') 358 359 if not os.path.isabs(workdir): 360 workdir = os.path.join('/src', workdir) 361 362 return os.path.normpath(workdir) 363 364 return os.path.join('/src', project_name) 365 366 367def docker_run(run_args, print_output=True): 368 """Call `docker run`.""" 369 command = ['docker', 'run', '--rm', '--privileged'] 370 371 # Support environments with a TTY. 372 if sys.stdin.isatty(): 373 command.append('-i') 374 375 command.extend(run_args) 376 377 print('Running:', _get_command_string(command)) 378 stdout = None 379 if not print_output: 380 stdout = open(os.devnull, 'w') 381 382 try: 383 subprocess.check_call(command, stdout=stdout, stderr=subprocess.STDOUT) 384 except subprocess.CalledProcessError as error: 385 return error.returncode 386 387 return 0 388 389 390def docker_build(build_args, pull=False): 391 """Call `docker build`.""" 392 command = ['docker', 'build'] 393 if pull: 394 command.append('--pull') 395 396 command.extend(build_args) 397 print('Running:', _get_command_string(command)) 398 399 try: 400 subprocess.check_call(command) 401 except subprocess.CalledProcessError: 402 print('docker build failed.', file=sys.stderr) 403 return False 404 405 return True 406 407 408def docker_pull(image): 409 """Call `docker pull`.""" 410 command = ['docker', 'pull', image] 411 print('Running:', _get_command_string(command)) 412 413 try: 414 subprocess.check_call(command) 415 except subprocess.CalledProcessError: 416 print('docker pull failed.', file=sys.stderr) 417 return False 418 419 return True 420 421 422def build_image(args): 423 """Build docker image.""" 424 if args.pull and args.no_pull: 425 print('Incompatible arguments --pull and --no-pull.') 426 return 1 427 428 if args.pull: 429 pull = True 430 elif args.no_pull: 431 pull = False 432 else: 433 y_or_n = raw_input('Pull latest base images (compiler/runtime)? (y/N): ') 434 pull = y_or_n.lower() == 'y' 435 436 if pull: 437 print('Pulling latest base images...') 438 else: 439 print('Using cached base images...') 440 441 # If build_image is called explicitly, don't use cache. 442 if build_image_impl(args.project_name, no_cache=True, pull=pull): 443 return 0 444 445 return 1 446 447 448def build_fuzzers_impl( # pylint: disable=too-many-arguments 449 project_name, 450 clean, 451 engine, 452 sanitizer, 453 architecture, 454 env_to_add, 455 source_path, 456 no_cache=False, 457 mount_location=None): 458 """Build fuzzers.""" 459 if not build_image_impl(project_name, no_cache=no_cache): 460 return 1 461 462 project_out_dir = _get_output_dir(project_name) 463 if clean: 464 print('Cleaning existing build artifacts.') 465 466 # Clean old and possibly conflicting artifacts in project's out directory. 467 docker_run([ 468 '-v', 469 '%s:/out' % project_out_dir, '-t', 470 'gcr.io/oss-fuzz/%s' % project_name, '/bin/bash', '-c', 'rm -rf /out/*' 471 ]) 472 473 else: 474 print('Keeping existing build artifacts as-is (if any).') 475 env = [ 476 'FUZZING_ENGINE=' + engine, 477 'SANITIZER=' + sanitizer, 478 'ARCHITECTURE=' + architecture, 479 ] 480 if env_to_add: 481 env += env_to_add 482 483 project_work_dir = _get_work_dir(project_name) 484 485 # Copy instrumented libraries. 486 if sanitizer == 'memory': 487 docker_run([ 488 '-v', 489 '%s:/work' % project_work_dir, 'gcr.io/oss-fuzz-base/msan-builder', 490 'bash', '-c', 'cp -r /msan /work' 491 ]) 492 env.append('MSAN_LIBS_PATH=' + '/work/msan') 493 494 command = ['--cap-add', 'SYS_PTRACE'] + _env_to_docker_args(env) 495 if source_path: 496 workdir = _workdir_from_dockerfile(project_name) 497 if workdir == '/src': 498 print('Cannot use local checkout with "WORKDIR: /src".', file=sys.stderr) 499 return 1 500 if not mount_location: 501 command += [ 502 '-v', 503 '%s:%s' % (_get_absolute_path(source_path), workdir), 504 ] 505 else: 506 command += [ 507 '-v', 508 '%s:%s' % (_get_absolute_path(source_path), mount_location), 509 ] 510 511 command += [ 512 '-v', 513 '%s:/out' % project_out_dir, '-v', 514 '%s:/work' % project_work_dir, '-t', 515 'gcr.io/oss-fuzz/%s' % project_name 516 ] 517 518 result_code = docker_run(command) 519 if result_code: 520 print('Building fuzzers failed.', file=sys.stderr) 521 return result_code 522 523 # Patch MSan builds to use instrumented shared libraries. 524 if sanitizer == 'memory': 525 docker_run( 526 [ 527 '-v', 528 '%s:/out' % project_out_dir, '-v', 529 '%s:/work' % project_work_dir 530 ] + _env_to_docker_args(env) + 531 ['gcr.io/oss-fuzz-base/base-msan-builder', 'patch_build.py', '/out']) 532 533 return 0 534 535 536def build_fuzzers(args): 537 """Build fuzzers.""" 538 return build_fuzzers_impl(args.project_name, args.clean, args.engine, 539 args.sanitizer, args.architecture, args.e, 540 args.source_path) 541 542 543def check_build(args): 544 """Checks that fuzzers in the container execute without errors.""" 545 if not check_project_exists(args.project_name): 546 return 1 547 548 if (args.fuzzer_name and 549 not _check_fuzzer_exists(args.project_name, args.fuzzer_name)): 550 return 1 551 552 env = [ 553 'FUZZING_ENGINE=' + args.engine, 554 'SANITIZER=' + args.sanitizer, 555 'ARCHITECTURE=' + args.architecture, 556 ] 557 if args.e: 558 env += args.e 559 560 run_args = _env_to_docker_args(env) + [ 561 '-v', 562 '%s:/out' % _get_output_dir(args.project_name), '-t', 563 'gcr.io/oss-fuzz-base/base-runner' 564 ] 565 566 if args.fuzzer_name: 567 run_args += ['test_one', os.path.join('/out', args.fuzzer_name)] 568 else: 569 run_args.append('test_all') 570 571 exit_code = docker_run(run_args) 572 if exit_code == 0: 573 print('Check build passed.') 574 else: 575 print('Check build failed.') 576 577 return exit_code 578 579 580def _get_fuzz_targets(project_name): 581 """Return names of fuzz targest build in the project's /out directory.""" 582 fuzz_targets = [] 583 for name in os.listdir(_get_output_dir(project_name)): 584 if name.startswith('afl-'): 585 continue 586 587 path = os.path.join(_get_output_dir(project_name), name) 588 if os.path.isfile(path) and os.access(path, os.X_OK): 589 fuzz_targets.append(name) 590 591 return fuzz_targets 592 593 594def _get_latest_corpus(project_name, fuzz_target, base_corpus_dir): 595 """Download the latest corpus for the given fuzz target.""" 596 corpus_dir = os.path.join(base_corpus_dir, fuzz_target) 597 if not os.path.exists(corpus_dir): 598 os.makedirs(corpus_dir) 599 600 if not fuzz_target.startswith(project_name): 601 fuzz_target = '%s_%s' % (project_name, fuzz_target) 602 603 corpus_backup_url = CORPUS_BACKUP_URL_FORMAT.format(project_name=project_name, 604 fuzz_target=fuzz_target) 605 command = ['gsutil', 'ls', corpus_backup_url] 606 607 corpus_listing = subprocess.Popen(command, 608 stdout=subprocess.PIPE, 609 stderr=subprocess.PIPE) 610 output, error = corpus_listing.communicate() 611 612 # Some fuzz targets (e.g. new ones) may not have corpus yet, just skip those. 613 if corpus_listing.returncode: 614 print('WARNING: corpus for {0} not found:\n{1}'.format(fuzz_target, error), 615 file=sys.stderr) 616 return 617 618 if output: 619 latest_backup_url = output.splitlines()[-1] 620 archive_path = corpus_dir + '.zip' 621 command = ['gsutil', '-q', 'cp', latest_backup_url, archive_path] 622 subprocess.check_call(command) 623 624 command = ['unzip', '-q', '-o', archive_path, '-d', corpus_dir] 625 subprocess.check_call(command) 626 os.remove(archive_path) 627 else: 628 # Sync the working corpus copy if a minimized backup is not available. 629 corpus_url = CORPUS_URL_FORMAT.format(project_name=project_name, 630 fuzz_target=fuzz_target) 631 command = ['gsutil', '-m', '-q', 'rsync', '-R', corpus_url, corpus_dir] 632 subprocess.check_call(command) 633 634 635def download_corpora(args): 636 """Download most recent corpora from GCS for the given project.""" 637 if not check_project_exists(args.project_name): 638 return 1 639 640 try: 641 with open(os.devnull, 'w') as stdout: 642 subprocess.check_call(['gsutil', '--version'], stdout=stdout) 643 except OSError: 644 print( 645 'ERROR: gsutil not found. Please install it from ' 646 'https://cloud.google.com/storage/docs/gsutil_install', 647 file=sys.stderr) 648 return False 649 650 if args.fuzz_target: 651 fuzz_targets = [args.fuzz_target] 652 else: 653 fuzz_targets = _get_fuzz_targets(args.project_name) 654 655 corpus_dir = _get_corpus_dir(args.project_name) 656 if not os.path.exists(corpus_dir): 657 os.makedirs(corpus_dir) 658 659 def _download_for_single_target(fuzz_target): 660 try: 661 _get_latest_corpus(args.project_name, fuzz_target, corpus_dir) 662 return True 663 except Exception as error: # pylint:disable=broad-except 664 print('ERROR: corpus download for %s failed: %s' % 665 (fuzz_target, str(error)), 666 file=sys.stderr) 667 return False 668 669 print('Downloading corpora for %s project to %s' % 670 (args.project_name, corpus_dir)) 671 thread_pool = ThreadPool(multiprocessing.cpu_count()) 672 return all(thread_pool.map(_download_for_single_target, fuzz_targets)) 673 674 675def coverage(args): 676 """Generate code coverage using clang source based code coverage.""" 677 if args.corpus_dir and not args.fuzz_target: 678 print( 679 'ERROR: --corpus-dir requires specifying a particular fuzz target ' 680 'using --fuzz-target', 681 file=sys.stderr) 682 return 1 683 684 if not check_project_exists(args.project_name): 685 return 1 686 687 if not args.no_corpus_download and not args.corpus_dir: 688 if not download_corpora(args): 689 return 1 690 691 env = [ 692 'FUZZING_ENGINE=libfuzzer', 693 'PROJECT=%s' % args.project_name, 694 'SANITIZER=coverage', 695 'HTTP_PORT=%s' % args.port, 696 'COVERAGE_EXTRA_ARGS=%s' % ' '.join(args.extra_args), 697 ] 698 699 run_args = _env_to_docker_args(env) 700 701 if args.corpus_dir: 702 if not os.path.exists(args.corpus_dir): 703 print('ERROR: the path provided in --corpus-dir argument does not exist', 704 file=sys.stderr) 705 return 1 706 corpus_dir = os.path.realpath(args.corpus_dir) 707 run_args.extend(['-v', '%s:/corpus/%s' % (corpus_dir, args.fuzz_target)]) 708 else: 709 run_args.extend(['-v', '%s:/corpus' % _get_corpus_dir(args.project_name)]) 710 711 run_args.extend([ 712 '-v', 713 '%s:/out' % _get_output_dir(args.project_name), 714 '-p', 715 '%s:%s' % (args.port, args.port), 716 '-t', 717 'gcr.io/oss-fuzz-base/base-runner', 718 ]) 719 720 run_args.append('coverage') 721 if args.fuzz_target: 722 run_args.append(args.fuzz_target) 723 724 exit_code = docker_run(run_args) 725 if exit_code == 0: 726 print('Successfully generated clang code coverage report.') 727 else: 728 print('Failed to generate clang code coverage report.') 729 730 return exit_code 731 732 733def run_fuzzer(args): 734 """Runs a fuzzer in the container.""" 735 if not check_project_exists(args.project_name): 736 return 1 737 738 if not _check_fuzzer_exists(args.project_name, args.fuzzer_name): 739 return 1 740 741 env = [ 742 'FUZZING_ENGINE=' + args.engine, 743 'SANITIZER=' + args.sanitizer, 744 'RUN_FUZZER_MODE=interactive', 745 ] 746 747 if args.e: 748 env += args.e 749 750 run_args = _env_to_docker_args(env) + [ 751 '-v', 752 '%s:/out' % _get_output_dir(args.project_name), 753 '-t', 754 'gcr.io/oss-fuzz-base/base-runner', 755 'run_fuzzer', 756 args.fuzzer_name, 757 ] + args.fuzzer_args 758 759 return docker_run(run_args) 760 761 762def reproduce(args): 763 """Reproduce a specific test case from a specific project.""" 764 return reproduce_impl(args.project_name, args.fuzzer_name, args.valgrind, 765 args.e, args.fuzzer_args, args.testcase_path) 766 767 768def reproduce_impl( # pylint: disable=too-many-arguments 769 project_name, fuzzer_name, valgrind, env_to_add, fuzzer_args, 770 testcase_path): 771 """Reproduces a testcase in the container.""" 772 if not check_project_exists(project_name): 773 return 1 774 775 if not _check_fuzzer_exists(project_name, fuzzer_name): 776 return 1 777 778 debugger = '' 779 env = [] 780 image_name = 'base-runner' 781 782 if valgrind: 783 debugger = 'valgrind --tool=memcheck --track-origins=yes --leak-check=full' 784 785 if debugger: 786 image_name = 'base-runner-debug' 787 env += ['DEBUGGER=' + debugger] 788 789 if env_to_add: 790 env += env_to_add 791 792 run_args = _env_to_docker_args(env) + [ 793 '-v', 794 '%s:/out' % _get_output_dir(project_name), 795 '-v', 796 '%s:/testcase' % _get_absolute_path(testcase_path), 797 '-t', 798 'gcr.io/oss-fuzz-base/%s' % image_name, 799 'reproduce', 800 fuzzer_name, 801 '-runs=100', 802 ] + fuzzer_args 803 804 return docker_run(run_args) 805 806 807def generate(args): 808 """Generate empty project files.""" 809 if len(args.project_name) > MAX_PROJECT_NAME_LENGTH: 810 print('Project name needs to be less than or equal to %d characters.' % 811 MAX_PROJECT_NAME_LENGTH, 812 file=sys.stderr) 813 return 1 814 815 if not VALID_PROJECT_NAME_REGEX.match(args.project_name): 816 print('Invalid project name.', file=sys.stderr) 817 return 1 818 819 directory = os.path.join('projects', args.project_name) 820 821 try: 822 os.mkdir(directory) 823 except OSError as error: 824 if error.errno != errno.EEXIST: 825 raise 826 print(directory, 'already exists.', file=sys.stderr) 827 return 1 828 829 print('Writing new files to', directory) 830 831 template_args = { 832 'project_name': args.project_name, 833 'year': datetime.datetime.now().year 834 } 835 with open(os.path.join(directory, 'project.yaml'), 'w') as file_handle: 836 file_handle.write(templates.PROJECT_YAML_TEMPLATE % template_args) 837 838 with open(os.path.join(directory, 'Dockerfile'), 'w') as file_handle: 839 file_handle.write(templates.DOCKER_TEMPLATE % template_args) 840 841 build_sh_path = os.path.join(directory, 'build.sh') 842 with open(build_sh_path, 'w') as file_handle: 843 file_handle.write(templates.BUILD_TEMPLATE % template_args) 844 845 os.chmod(build_sh_path, 0o755) 846 return 0 847 848 849def shell(args): 850 """Runs a shell within a docker image.""" 851 if not build_image_impl(args.project_name): 852 return 1 853 854 env = [ 855 'FUZZING_ENGINE=' + args.engine, 856 'SANITIZER=' + args.sanitizer, 857 'ARCHITECTURE=' + args.architecture, 858 ] 859 860 if args.e: 861 env += args.e 862 863 if is_base_image(args.project_name): 864 image_project = 'oss-fuzz-base' 865 out_dir = _get_output_dir() 866 else: 867 image_project = 'oss-fuzz' 868 out_dir = _get_output_dir(args.project_name) 869 870 run_args = _env_to_docker_args(env) + [ 871 '-v', 872 '%s:/out' % out_dir, '-v', 873 '%s:/work' % _get_work_dir(args.project_name), '-t', 874 'gcr.io/%s/%s' % (image_project, args.project_name), '/bin/bash' 875 ] 876 877 docker_run(run_args) 878 return 0 879 880 881def pull_images(_): 882 """Pull base images.""" 883 for base_image in BASE_IMAGES: 884 if not docker_pull(base_image): 885 return 1 886 887 return 0 888 889 890if __name__ == '__main__': 891 sys.exit(main()) 892