• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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