blob: 9cca138dbe09b28dd4ed4f5b8c3d58103fdc38a5 [file] [log] [blame]
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -08001# -*- coding: utf-8 -*-
2# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script for performing tasks that are useful for fuzzer development.
7
8Run "cros_fuzz" in the chroot for a list of command or "cros_fuzz $COMMAND
9--help" for their full details. Below is a summary of commands that the script
10can perform:
11
12coverage: Generate a coverage report for a given fuzzer (specified by "--fuzzer"
13 option). You almost certainly want to specify the package to build (using
14 the "--package" option) so that a coverage build is done, since a coverage
15 build is needed to generate a report. If your fuzz target is running on
16 ClusterFuzz already, you can use the "--download" option to download the
17 corpus from ClusterFuzz. Otherwise, you can use the "--corpus" option to
18 specify the path of the corpus to run the fuzzer on and generate a report.
19 The corpus will be copied to the sysroot so that the fuzzer can use it.
20 Note that "--download" and "--corpus" are mutually exclusive.
21
22reproduce: Runs the fuzzer specified by the "--fuzzer" option on a testcase
23 (path specified by the "--testcase" argument). Optionally does a build when
24 the "--package" option is used. The type of build can be specified using the
25 "--build_type" argument.
26
27download: Downloads the corpus from ClusterFuzz of the fuzzer specified by the
28 "--fuzzer" option. The path of the directory the corpus directory is
29 downloaded to can be specified using the "--directory" option.
30
31shell: Sets up the sysroot for fuzzing and then chroots into the sysroot giving
32 you a shell that is ready to fuzz.
33
34setup: Sets up the sysroot for fuzzing (done prior to doing "reproduce", "shell"
35 and "coverage" commands).
36
37cleanup: Undoes "setup".
38
39Note that cros_fuzz will print every shell command it runs if you set the
40log-level to debug ("--log-level debug"). Otherwise it will print commands that
41fail.
42"""
43
44from __future__ import print_function
45
46import os
47import shutil
48
49from elftools.elf.elffile import ELFFile
50import lddtree
51
52from chromite.lib import commandline
53from chromite.lib import constants
54from chromite.lib import cros_build_lib
55from chromite.lib import cros_logging as logging
56from chromite.lib import gs
57from chromite.lib import osutils
58
59# Directory in sysroot's /tmp directory that this script will use for files it
60# needs to write. We need a directory to write files to because this script uses
61# external programs that must write and read to/from files and because these
62# must be run inside the sysroot and thus are usually unable to read or write
63# from directories in the chroot environment this script is executed in.
64SCRIPT_STORAGE_DIRECTORY = 'fuzz'
65SCRIPT_STORAGE_PATH = os.path.join('/', 'tmp', SCRIPT_STORAGE_DIRECTORY)
66
67# Names of subdirectories in "fuzz" directory used by this script to store
68# things.
69CORPUS_DIRECTORY_NAME = 'corpus'
70TESTCASE_DIRECTORY_NAME = 'testcase'
71COVERAGE_REPORT_DIRECTORY_NAME = 'coverage-report'
72
73# Constants needed for fuzzing.
74# The environment we need to use when fuzzing. ASAN_OPTIONS must contain
75# log_path=stderr otherwise ASAN errors will be hidden from users.
76FUZZ_ENV = {'ASAN_OPTIONS': 'log_path=stderr'}
77
78# Constants for names of libFuzzer command line options.
79RUNS_OPTION_NAME = 'runs'
80MAX_TOTAL_TIME_OPTION_NAME = 'max_total_time'
81
82# The default path a profraw file written by a clang coverage instrumented
83# binary when run by this script (default is current working directory).
84DEFAULT_PROFRAW_PATH = '/default.profraw'
85
86# Constants for libFuzzer command line values.
87# 0 runs means execute everything in the corpus and do no mutations.
88RUNS_DEFAULT_VALUE = 0
89# An arbitrary but short amount of time to run a fuzzer to get some coverage
90# data (when a corpus hasn't been provided and we aren't told to download one.
91MAX_TOTAL_TIME_DEFAULT_VALUE = 30
92
93
94class BuildType(object):
95 """Class to hold the different kinds of build types."""
96
97 ASAN = 'asan'
98 UBSAN = 'ubsan'
99 COVERAGE = 'coverage'
100 STANDARD = ''
101
102 # Build types that users can specify.
103 CHOICES = (ASAN, UBSAN, COVERAGE)
104
105
106class SysrootPath(object):
107 """Class for representing a path that is in the sysroot.
108
109 Useful for dealing with paths that we must interact with when chrooted into
110 the sysroot and outside of it.
111
112 For example, if we need to interact with the "/tmp" directory of the sysroot,
113 SysrootPath('/tmp').sysroot returns the path of the directory if we are in
114 chrooted into the sysroot, i.e. "/tmp".
115
116 SysrootPath('/tmp').chroot returns the path of the directory when in the
117 cros_sdk i.e. SYSROOT_DIRECTORY + "/tmp" (this will probably be
118 "/build/amd64-generic/tmp" in most cases).
119 """
120
121 # The actual path to the sysroot (from within the chroot).
122 path_to_sysroot = None
123
124 def __init__(self, path):
125 """Constructor.
126
127 Args:
128 path: An absolute path representing something in the sysroot.
129 """
130
131 assert path.startswith('/')
132 if self.IsPathInSysroot(path):
133 path = self.FromChrootPathInSysroot(os.path.abspath(path))
134 self.path_list = path.split(os.sep)[1:]
135
136 @classmethod
137 def SetPathToSysroot(cls, board):
138 """Sets path_to_sysroot
139
140 Args:
141 board: The board we will use for our sysroot.
142
143 Returns:
144 The path to the sysroot (the value of path_to_sysroot).
145 """
146 cls.path_to_sysroot = cros_build_lib.GetSysroot(board)
147 return cls.path_to_sysroot
148
149 @property
150 def chroot(self):
151 """Get the path of the object in the Chrome OS SDK chroot.
152
153 Returns:
154 The path this object represents when chrooted into the sysroot.
155 """
156 assert self.path_to_sysroot is not None, "set SysrootPath.path_to_sysroot"
157 return os.path.join(self.path_to_sysroot, *self.path_list)
158
159 @property
160 def sysroot(self):
161 """Get the path of the object when in the sysroot.
162
163 Returns:
164 The path this object represents when in the Chrome OS SDK .
165 """
166 return os.path.join('/', *self.path_list)
167
168 @classmethod
169 def IsPathInSysroot(cls, path):
170 """Is a path in the sysroot.
171
172 Args:
173 path: The path we are checking is in the sysroot.
174
175 Returns:
176 True if path is within the sysroot's path in the chroot.
177 """
178 assert cls.path_to_sysroot
179 return path.startswith(cls.path_to_sysroot)
180
181 @classmethod
182 def FromChrootPathInSysroot(cls, path):
183 """Converts a chroot-relative path that is in sysroot into sysroot-relative.
184
185 Args:
186 path: The chroot-relative path we are converting to sysroot relative.
187
188 Returns:
189 The sysroot relative version of |path|.
190 """
191 assert cls.IsPathInSysroot(path)
192 common_prefix = os.path.commonprefix([cls.path_to_sysroot, path])
193 return path[len(common_prefix):]
194
195
196def GetScriptStoragePath(relative_path):
197 """Get the SysrootPath representing a script storage path.
198
199 Get a path of a directory this script will store things in.
200
201 Args:
202 relative_path: The path relative to the root of the script storage
203 directory.
204
205 Returns:
206 The SysrootPath representing absolute path of |relative_path| in the script
207 storage directory.
208 """
209 path = os.path.join(SCRIPT_STORAGE_PATH, relative_path)
210 return SysrootPath(path)
211
212
213def GetSysrootPath(path):
214 """Get the chroot-relative path of a path in the sysroot.
215
216 Args:
217 path: An absolute path in the sysroot that we will get the path in the
218 chroot for.
219
220 Returns:
221 The chroot-relative path of |path| in the sysroot.
222 """
223 return SysrootPath(path).chroot
224
225
226def GetCoverageDirectory(fuzzer):
227 """Get a coverage report directory for a fuzzer
228
229 Args:
230 fuzzer: The fuzzer to get the coverage report directory for.
231
232 Returns:
233 The location of the coverage report directory for the |fuzzer|.
234 """
235 relative_path = os.path.join(COVERAGE_REPORT_DIRECTORY_NAME, fuzzer)
236 return GetScriptStoragePath(relative_path)
237
238
239def GetFuzzerSysrootPath(fuzzer):
240 """Get the path in the sysroot of a fuzzer.
241
242 Args:
243 fuzzer: The fuzzer to get the path of.
244
245 Returns:
246 The path of |fuzzer| in the sysroot.
247 """
248 return SysrootPath(os.path.join('/', 'usr', 'libexec', 'fuzzers', fuzzer))
249
250
251def GetProfdataPath(fuzzer):
252 """Get the profdata file of a fuzzer.
253
254 Args:
255 fuzzer: The fuzzer to get the profdata file of.
256
257 Returns:
258 The path of the profdata file that should be used by |fuzzer|.
259 """
260 return GetScriptStoragePath('%s.profdata' % fuzzer)
261
262
263def GetPathForCopy(parent_directory, chroot_path):
264 """Returns a path in the script storage directory to copy chroot_path.
265
266 Returns a SysrootPath representing the location where |chroot_path| should
267 copied. This path will be in the parent_directory which will be in the script
268 storage directory.
269 """
270 basename = os.path.basename(chroot_path)
271 return GetScriptStoragePath(os.path.join(parent_directory, basename))
272
273
274def CopyCorpusToSysroot(src_corpus_path):
275 """Copies corpus into the sysroot.
276
277 Copies corpus into the sysroot. Doesn't copy if corpus is already in sysroot.
278
279 Args:
280 src_corpus_path: A path (in the chroot) to a corpus that will be copied into
281 sysroot.
282
283 Returns:
284 The path in the sysroot that the corpus was copied to.
285 """
286 if src_corpus_path is None:
287 return None
288
289 if SysrootPath.IsPathInSysroot(src_corpus_path):
290 # Don't copy if |src_testcase_path| is already in sysroot. Just return it in
291 # the format expected by the caller.
292 return SysrootPath(src_corpus_path)
293
294 dest_corpus_path = GetPathForCopy(CORPUS_DIRECTORY_NAME, src_corpus_path)
295 osutils.RmDir(dest_corpus_path.chroot)
296 shutil.copytree(src_corpus_path, dest_corpus_path.chroot)
297 return dest_corpus_path
298
299
300def CopyTestcaseToSysroot(src_testcase_path):
301 """Copies a testcase into the sysroot.
302
303 Copies a testcase into the sysroot. Doesn't copy if testcase is already in
304 sysroot.
305
306 Args:
307 src_testcase_path: A path (in the chroot) to a testcase that will be copied
308 into sysroot.
309
310 Returns:
311 The path in the sysroot that the testcase was copied to.
312 """
313 if SysrootPath.IsPathInSysroot(src_testcase_path):
314 # Don't copy if |src_testcase_path| is already in sysroot. Just return it in
315 # the format expected by the caller.
316 return SysrootPath(src_testcase_path)
317
318 dest_testcase_path = GetPathForCopy(TESTCASE_DIRECTORY_NAME,
319 src_testcase_path)
320 osutils.SafeMakedirsNonRoot(os.path.dirname(dest_testcase_path.chroot))
321 osutils.SafeUnlink(dest_testcase_path.chroot)
322
323 shutil.copy(src_testcase_path, dest_testcase_path.chroot)
324 return dest_testcase_path
325
326
327def SudoRunCommand(*args, **kwargs):
328 """Wrapper around cros_build_lib.SudoRunCommand.
329
330 Wrapper that calls cros_build_lib.SudoRunCommand but sets debug_level by
331 default.
332
333 Args:
334 *args: Positional arguments to pass to cros_build_lib.SudoRunCommand.
335 *kwargs: Keyword arguments to pass to cros_build_lib.SudoRunCommand.
336
337 Returns:
338 The value returned by calling cros_build_lib.SudoRunCommand.
339 """
340 kwargs.setdefault('debug_level', logging.DEBUG)
341 return cros_build_lib.SudoRunCommand(*args, **kwargs)
342
343
344def GetLibFuzzerOption(option_name, option_value):
345 """Gets the libFuzzer command line option with the specified name and value.
346
347 Args:
348 option_name: The name of the libFuzzer option.
349 option_value: The value of the libFuzzer option.
350
351 Returns:
352 The libFuzzer option composed of |option_name| and |option_value|.
353 """
354 return '-%s=%s' % (option_name, option_value)
355
356
357def IsOptionLimit(option):
358 """Determines if fuzzer option limits fuzzing time."""
359 for limit_name in [MAX_TOTAL_TIME_OPTION_NAME, RUNS_OPTION_NAME]:
360 if option.startswith('-%s' % limit_name):
361 return True
362
363 return False
364
365
366def LimitFuzzing(fuzz_command, corpus):
367 """Limits how long fuzzing will go if unspecified.
368
369 Adds a reasonable limit on how much fuzzing will be done unless there already
370 is some kind of limit. Mutates fuzz_command.
371
372 Args:
373 fuzz_command: A command to run a fuzzer. Used to determine if a limit needs
374 to be set. Mutated if it is needed to specify a limit.
375 corpus: The corpus that will be passed to the fuzzer. If not None then
376 fuzzing is limited by running everything in the corpus once.
377 """
378 if any(IsOptionLimit(option) for option in fuzz_command[1:]):
379 # Don't do anything if there is already a limit.
380 return
381
382 if corpus:
383 # If there is a corpus, just run everything in the corpus once.
384 fuzz_command.append(
385 GetLibFuzzerOption(RUNS_OPTION_NAME, RUNS_DEFAULT_VALUE))
386 return
387
388 # Since there is no corpus, just fuzz for 30 seconds.
389 logging.info('Limiting fuzzing to %s seconds.', MAX_TOTAL_TIME_DEFAULT_VALUE)
390 max_total_time_option = GetLibFuzzerOption(MAX_TOTAL_TIME_OPTION_NAME,
391 MAX_TOTAL_TIME_DEFAULT_VALUE)
392 fuzz_command.append(max_total_time_option)
393
394
395def RunFuzzer(fuzzer, corpus_path=None, fuzz_args='', testcase_path=None):
396 """Runs the fuzzer while chrooted into the sysroot.
397
398 Args:
399 fuzzer: The fuzzer to run.
400 corpus_path: A path to a corpus (not necessarily in the sysroot) to
401 run the fuzzer on.
402 fuzz_args: Additional arguments to pass to the fuzzer when running it.
403 testcase_path: A path to a testcase (not necessarily in the sysroot) to run
404 the fuzzer on.
405 """
406 logging.info('Running fuzzer: %s', fuzzer)
407 fuzzer_sysroot_path = GetFuzzerSysrootPath(fuzzer)
408 fuzz_command = [fuzzer_sysroot_path.sysroot]
409 fuzz_command += fuzz_args.split()
410
411 if testcase_path:
412 fuzz_command.append(testcase_path)
413 else:
414 LimitFuzzing(fuzz_command, corpus_path)
415
416 if corpus_path:
417 fuzz_command.append(corpus_path)
418
419 RunSysrootCommand(fuzz_command, env=FUZZ_ENV, debug_level=logging.INFO)
420
421
422def MergeProfraw(fuzzer):
423 """Merges profraw file from a fuzzer and creates a profdata file.
424
425 Args:
426 fuzzer: The fuzzer to merge the profraw file from.
427 """
428 profdata_path = GetProfdataPath(fuzzer)
429 command = [
430 'llvm-profdata',
431 'merge',
432 '-sparse',
433 DEFAULT_PROFRAW_PATH,
434 '-o',
435 profdata_path.sysroot,
436 ]
437
438 RunSysrootCommand(command)
439 return profdata_path
440
441
442def GenerateCoverageReport(fuzzer, shared_libraries):
443 """Generates an HTML coverage report from a fuzzer run.
444
445 Args:
446 fuzzer: The fuzzer to generate the coverage report for.
447 shared_libraries: Libraries loaded dynamically by |fuzzer|.
448
449 Returns:
450 The path of the coverage report.
451 """
452 fuzzer_path = GetFuzzerSysrootPath(fuzzer).chroot
453 command = ['llvm-cov', 'show', '-object', fuzzer_path]
454 for library in shared_libraries:
455 command += ['-object', library]
456
457 coverage_directory = GetCoverageDirectory(fuzzer)
458 command += [
459 '-format=html',
460 '-instr-profile=%s' % GetProfdataPath(fuzzer).chroot,
461 '-output-dir=%s' % coverage_directory.chroot,
462 ]
463
464 # TODO(metzman): Investigate error messages printed by this command.
465 cros_build_lib.RunCommand(
466 command, redirect_stderr=True, debug_level=logging.DEBUG)
467 return coverage_directory
468
469
470def GetSharedLibraries(binary_path):
471 """Gets the shared libraries used by a binary.
472
473 Gets the shared libraries used by the binary. Based on GetSharedLibraries from
474 src/tools/code_coverage/coverage_utils.py in Chromium.
475
476 Args:
477 binary_path: The path to the binary we want to find the shared libraries of.
478
479 Returns:
480 The shared libraries used by |binary_path|.
481 """
482 logging.info('Finding shared libraries for targets (if any).')
483 shared_libraries = []
484 elf_dict = lddtree.ParseELF(
485 binary_path.chroot, root=SysrootPath.path_to_sysroot)
486 for shared_library in elf_dict['libs'].itervalues():
487 shared_library_path = shared_library['path']
488
489 if shared_library_path in shared_libraries:
490 continue
491
492 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
493 'the given target(s) does not '
494 'exist.' % shared_library_path)
495
496 if IsInstrumentedWithClangCoverage(shared_library_path):
497 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors out.
498 shared_libraries.append(shared_library_path)
499
500 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
501 shared_libraries)
502 logging.info('Finished finding shared libraries for targets.')
503 return shared_libraries
504
505
506def IsInstrumentedWithClangCoverage(binary_path):
507 """Determines if a binary is instrumented with clang source based coverage.
508
509 Args:
510 binary_path: The path of the binary (executable or library) we are checking
511 is instrumented with clang source based coverage.
512
513 Returns:
514 True if the binary is instrumented with clang source based coverage.
515 """
516 with open(binary_path, 'rb') as file_handle:
517 elf_file = ELFFile(file_handle)
518 return elf_file.get_section_by_name('__llvm_covmap') is not None
519
520
521def RunFuzzerAndGenerateCoverageReport(fuzzer, corpus, fuzz_args):
522 """Runs a fuzzer generates a coverage report and returns the report's path.
523
524 Gets a coverage report for a fuzzer.
525
526 Args:
527 fuzzer: The fuzzer to run and generate the coverage report for.
528 corpus: The path to a corpus to run the fuzzer on.
529 fuzz_args: Additional arguments to pass to the fuzzer.
530
531 Returns:
532 The path to the coverage report.
533 """
534 corpus_path = CopyCorpusToSysroot(corpus)
535 if corpus_path:
536 corpus_path = corpus_path.sysroot
537
538 RunFuzzer(fuzzer, corpus_path=corpus_path, fuzz_args=fuzz_args)
539 MergeProfraw(fuzzer)
540 fuzzer_sysroot_path = GetFuzzerSysrootPath(fuzzer)
541 shared_libraries = GetSharedLibraries(fuzzer_sysroot_path)
542 return GenerateCoverageReport(fuzzer, shared_libraries)
543
544
545def RunSysrootCommand(command, env=None, **kwargs):
546 """Runs command while chrooted into sysroot and returns the output.
547
548 Args:
549 command: A command to run in the sysroot.
550 env: Environment variables that can be used by |command| when executing in
551 the sysroot.
552 kwargs: Extra arguments to pass to cros_build_lib.SudoRunCommand.
553
554 Returns:
555 The result of a call to cros_build_lib.SudoRunCommand.
556 """
557 command = ['chroot', SysrootPath.path_to_sysroot] + command
558 return SudoRunCommand(command, extra_env=env, **kwargs)
559
560
561def GetBuildExtraEnv(build_type):
562 """Gets the extra_env for building a package.
563
564 Args:
565 build_type: The type of build we want to do.
566
567 Returns:
568 The extra_env to use when building.
569 """
570 if build_type is None:
571 build_type = BuildType.ASAN
572
573 use_flags = os.environ.get('USE', '').split()
574 # Check that the user hasn't already set USE flags that we can set.
575 # No good way to iterate over an enum in python2.
576 for use_flag in BuildType.CHOICES:
577 if use_flag in use_flags:
578 logging.warn('%s in USE flags. Please use --build_type instead.',
579 use_flag)
580
581 # Set USE flags.
582 fuzzer_build_type = 'fuzzer'
583 use_flags += [fuzzer_build_type, build_type]
584 features_flags = os.environ.get('FEATURES', '').split()
585 if build_type == BuildType.COVERAGE:
586 # We must use ASan when doing coverage builds.
587 use_flags.append(BuildType.ASAN)
588 # Use noclean so that a coverage report can be generated based on the source
589 # code.
590 features_flags.append('noclean')
591
592 return {
593 'FEATURES': ' '.join(features_flags),
594 'USE': ' '.join(use_flags),
595 }
596
597
598def BuildPackage(package, board, build_type):
599 """Builds a package on a specified board.
600
601 Args:
602 package: The package to build. Nothing is built if None.
603 board: The board to build the package on.
604 build_type: The type of the build to do (e.g. asan, ubsan or coverage).
605 """
606 if package is None:
607 return
608
609 logging.info('Building %s using %s.', package, build_type)
610 env = GetBuildExtraEnv(build_type)
611 build_packages_path = os.path.join(constants.SOURCE_ROOT, 'src', 'scripts',
612 'build_packages')
613 command = [
614 build_packages_path,
615 '--board',
616 board,
617 '--skip_chroot_upgrade',
618 package,
619 ]
620 # Print the output of the build command. Do this because it is familiar to
621 # devs and we don't want to leave them not knowing about the build's progress
622 # for a long time.
623 cros_build_lib.RunCommand(command, extra_env=env)
624
625
626def DownloadFuzzerCorpus(fuzzer, dest_directory=None):
627 """Downloads a corpus and returns its path.
628
629 Args:
630 fuzzer: The name of the fuzzer who's corpus we want to download.
631 dest_directory: The directory to download the corpus to.
632
633 Downloads the corpus of |fuzzer| to a subdirectory of |dest_directory|.
634 Returns the path of this subdirectory. If we are denied access to the file,
635 asks the user to authenticate to gsutil.
636
637 Returns:
638 The path to the downloaded corpus.
639
640 Raises:
641 gs.NoSuchKey: A corpus for the fuzzer doesn't exist.
642 gs.GSCommandError: The corpus failed to download for another reason.
643 """
644 if not fuzzer.startswith('chromeos_'):
645 # ClusterFuzz internally appends "chromeos_" to chromeos targets' names.
646 # Therefore we must do so in order to find the corpus.
647 fuzzer = 'chromeos_%s' % fuzzer
648
649 if dest_directory is None:
650 dest_directory = GetScriptStoragePath(CORPUS_DIRECTORY_NAME).chroot
651 osutils.SafeMakedirsNonRoot(dest_directory)
652
653 clusterfuzz_gcs_corpus_bucket = 'chromeos-corpus'
654 suburl = 'libfuzzer/%s' % fuzzer
655 gcs_path = gs.GetGsURL(
656 clusterfuzz_gcs_corpus_bucket,
657 for_gsutil=True,
658 public=False,
659 suburl=suburl)
660
661 dest_path = os.path.join(dest_directory, fuzzer)
662
663 try:
664 logging.info('Downloading corpus to %s.', dest_path)
665 ctx = gs.GSContext()
666 ctx.Copy(
667 gcs_path,
668 dest_directory,
669 recursive=True,
670 parallel=True,
671 debug_level=logging.DEBUG)
672 logging.info('Finished downloading corpus.')
673 except gs.GSNoSuchKey as exception:
674 logging.error('Corpus for fuzzer: %s does not exist.', fuzzer)
675 raise exception
676 # Try to authenticate if we were denied permission to access the corpus.
677 except gs.GSCommandError as exception:
678 logging.error(
679 'gsutil failed to download the corpus. You may need to log in. See:\n'
680 'https://chromium.googlesource.com/chromiumos/docs/+/master/gsutil.md'
681 '#setup\n'
682 'for instructions on doing this.')
683 raise exception
684
685 return dest_path
686
687
688def Reproduce(fuzzer, testcase_path):
689 """Runs a fuzzer in the sysroot on a testcase.
690
691 Args:
692 fuzzer: The fuzzer to run.
693 testcase_path: The path (not necessarily in the sysroot) of the testcase to
694 run the fuzzer on.
695 """
696 testcase_sysroot_path = CopyTestcaseToSysroot(testcase_path)
697 RunFuzzer(fuzzer, testcase_path=testcase_sysroot_path.sysroot)
698
699
700def SetUpSysrootForFuzzing():
701 """Sets up the the sysroot for fuzzing
702
703 Prepares the sysroot for fuzzing. Idempotent.
704 """
705 logging.info('Setting up sysroot for fuzzing.')
706 # TODO(metzman): Don't create devices or mount /proc, use platform2_test.py
707 # instead.
708 # Mount /proc in sysroot and setup dev there because they are needed by
709 # sanitizers.
710 proc_manager = ProcManager()
711 proc_manager.Mount()
712
713 # Setup devices in /dev that are needed by libFuzzer.
714 device_manager = DeviceManager()
715 device_manager.SetUp()
716
717 # Set up asan_symbolize.py, llvm-symbolizer, and llvm-profdata in the
718 # sysroot so that fuzzer output (including stack traces) can be symbolized
719 # and so that coverage reports can be generated.
720 tool_manager = ToolManager()
721 tool_manager.Install()
722
723 osutils.SafeMakedirsNonRoot(GetSysrootPath(SCRIPT_STORAGE_PATH))
724
725
726def CleanUpSysroot():
727 """Cleans up the the sysroot from SetUpSysrootForFuzzing.
728
729 Undoes SetUpSysrootForFuzzing. Idempotent.
730 """
731 logging.info('Cleaning up the sysroot.')
732 proc_manager = ProcManager()
733 proc_manager.Unmount()
734
735 device_manager = DeviceManager()
736 device_manager.CleanUp()
737
738 tool_manager = ToolManager()
739 tool_manager.Uninstall()
740 osutils.RmDir(GetSysrootPath(SCRIPT_STORAGE_PATH))
741
742
743class ToolManager(object):
744 """Class that installs or uninstalls fuzzing tools to/from the sysroot.
745
746 Install and Uninstall methods are idempotent. Both are safe to call at any
747 point.
748 """
749
750 # Path to asan_symbolize.py.
751 ASAN_SYMBOLIZE_PATH = os.path.join('/', 'usr', 'bin', 'asan_symbolize.py')
752
753 # List of LLVM binaries we must install in sysroot.
754 LLVM_BINARY_NAMES = ['llvm-symbolizer', 'llvm-profdata']
755
756 def __init__(self):
757 self.asan_symbolize_sysroot_path = GetSysrootPath(self.ASAN_SYMBOLIZE_PATH)
758
759 def Install(self):
760 """Installs tools to the sysroot."""
761 # Install asan_symbolize.py.
762 SudoRunCommand(
763 ['cp', self.ASAN_SYMBOLIZE_PATH, self.asan_symbolize_sysroot_path])
764 # Install the LLVM binaries.
765 # TODO(metzman): Build these tools so that we don't mess up when board is
766 # for a different ISA.
767 for llvm_binary in self._GetLLVMBinaries():
768 llvm_binary.Install()
769
770 def Uninstall(self):
771 """Uninstalls tools from the sysroot. Undoes Install."""
772 # Uninstall asan_symbolize.py.
773 osutils.SafeUnlink(self.asan_symbolize_sysroot_path, sudo=True)
774 # Uninstall the LLVM binaries.
775 for llvm_binary in self._GetLLVMBinaries():
776 llvm_binary.Uninstall()
777
778 def _GetLLVMBinaries(self):
779 """Creates LllvmBinary objects for each binary name in LLVM_BINARY_NAMES."""
780 return [LlvmBinary(binary_name) for binary_name in self.LLVM_BINARY_NAMES]
781
782
783class LlvmBinary(object):
784 """Class for representing installing/uninstalling an LLVM binary in sysroot.
785
786 Install and Uninstall methods are idempotent. Both are safe to call at any
787 time.
788 """
789
790 # Path to the lddtree chromite script.
791 LDDTREE_SCRIPT_PATH = os.path.join(constants.CHROMITE_BIN_DIR, 'lddtree')
792
793 def __init__(self, binary):
794 self.binary = binary
795 self.install_dir = GetSysrootPath(
796 os.path.join('/', 'usr', 'libexec', binary))
797 self.binary_dir_path = GetSysrootPath(os.path.join('/', 'usr', 'bin'))
798 self.binary_chroot_dest_path = os.path.join(self.binary_dir_path, binary)
799
800 def Uninstall(self):
801 """Removes an LLVM binary from sysroot. Undoes Install."""
802 osutils.RmDir(self.install_dir, ignore_missing=True, sudo=True)
803 osutils.SafeUnlink(self.binary_chroot_dest_path, sudo=True)
804
805 def Install(self):
806 """Installs (sets up) an LLVM binary in the sysroot.
807
808 Sets up an llvm binary in the sysroot so that it can be run there.
809 """
810 # Create a directory for installing |binary| and all of its dependencies in
811 # the sysroot.
812 binary_rel_path = ['usr', 'bin', self.binary]
813 binary_chroot_path = os.path.join('/', *binary_rel_path)
814 osutils.SafeMakedirsNonRoot(self.install_dir)
815
816 # Copy the binary and everything needed to run it into the sysroot.
817 cmd = [
818 self.LDDTREE_SCRIPT_PATH,
819 '-v',
820 '--generate-wrappers',
821 '--root',
822 '/',
823 '--copy-to-tree',
824 self.install_dir,
825 binary_chroot_path,
826 ]
827 SudoRunCommand(cmd)
828
829 # Create a symlink to the copy of the binary (we can't do lddtree in
830 # self.binary_dir_path). Note that symlink should be relative so that it
831 # will be valid when chrooted into the sysroot.
832 rel_path = os.path.relpath(self.install_dir, self.binary_dir_path)
833 link_path = os.path.join(rel_path, *binary_rel_path)
834 osutils.SafeSymlink(link_path, self.binary_chroot_dest_path, sudo=True)
835
836
837class DeviceManager(object):
838 """Class that creates or removes devices from /dev in sysroot.
839
840 SetUp and CleanUp methods are idempotent. Both are safe to call at any point.
841 """
842
843 DEVICE_MKNOD_PARAMS = {
844 'null': (666, 3),
845 'random': (444, 8),
846 'urandom': (444, 9),
847 }
848
849 MKNOD_MAJOR = '1'
850
851 def __init__(self):
852 self.dev_path_chroot = GetSysrootPath('/dev')
853
854 def _GetDevicePath(self, device_name):
855 """Returns the path of |device_name| in sysroot's /dev."""
856 return os.path.join(self.dev_path_chroot, device_name)
857
858 def SetUp(self):
859 """Sets up devices in the sysroot's /dev.
860
861 Creates /dev/null, /dev/random, and /dev/urandom. If they already exist then
862 recreates them.
863 """
864 self.CleanUp()
865 osutils.SafeMakedirsNonRoot(self.dev_path_chroot)
866 for device, mknod_params in self.DEVICE_MKNOD_PARAMS.iteritems():
867 device_path = self._GetDevicePath(device)
868 self._MakeCharDevice(device_path, *mknod_params)
869
870 def CleanUp(self):
871 """Cleans up devices in the sysroot's /dev. Undoes SetUp.
872
873 Removes /dev/null, /dev/random, and /dev/urandom if they exist.
874 """
875 for device in self.DEVICE_MKNOD_PARAMS:
876 device_path = self._GetDevicePath(device)
877 if os.path.exists(device_path):
878 # Use -r since dev/null is sometimes a directory.
879 SudoRunCommand(['rm', '-r', device_path])
880
881 def _MakeCharDevice(self, path, mode, minor):
882 """Make a character device."""
883 mode = str(mode)
884 minor = str(minor)
885 command = ['mknod', '-m', mode, path, 'c', self.MKNOD_MAJOR, minor]
886 SudoRunCommand(command)
887
888
889class ProcManager(object):
890 """Class that mounts or unmounts /proc in sysroot.
891
892 Mount and Unmount are idempotent. Both are safe to call at any point.
893 """
894
895 PROC_PATH = '/proc'
896
897 def __init__(self):
898 self.proc_path_chroot = GetSysrootPath(self.PROC_PATH)
899 self.is_mounted = osutils.IsMounted(self.proc_path_chroot)
900
901 def Unmount(self):
902 """Unmounts /proc in chroot. Undoes Mount."""
903 if not self.is_mounted:
904 return
905 osutils.UmountDir(self.proc_path_chroot, cleanup=False)
906
907 def Mount(self):
908 """Mounts /proc in chroot. Remounts it if already mounted."""
909 self.Unmount()
910 osutils.MountDir(
911 self.PROC_PATH,
912 self.proc_path_chroot,
913 'proc',
914 debug_level=logging.DEBUG)
915
916
917def EnterSysrootShell():
918 """Spawns and gives user access to a bash shell in the sysroot."""
919 command = ['/bin/bash', '-i']
920 return RunSysrootCommand(
921 command, env=FUZZ_ENV, debug_level=logging.INFO,
922 error_code_ok=True).returncode
923
924
925def StripFuzzerPrefixes(fuzzer_name):
926 """Strip the prefix ClusterFuzz uses in case they are specified.
927
928 Strip the prefixes used by ClusterFuzz if the users has included them by
929 accident.
930
931 Args:
932 fuzzer_name: The fuzzer who's name may contain prefixes.
933
934 Returns:
935 The name of the fuzz target without prefixes.
936 """
937 initial_name = fuzzer_name
938
939 def StripPrefix(prefix):
940 if fuzzer_name.startswith(prefix):
941 return fuzzer_name[len(prefix):]
942 return fuzzer_name
943
944 clusterfuzz_prefixes = ['libFuzzer_', 'chromeos_']
945
946 for prefix in clusterfuzz_prefixes:
947 fuzzer_name = StripPrefix(prefix)
948
949 if initial_name != fuzzer_name:
950 logging.warn(
951 '%s contains a prefix from ClusterFuzz (one or more of %s) that is not '
952 'part of the fuzzer\'s name. Interpreting --fuzzer as %s.',
953 initial_name, clusterfuzz_prefixes, fuzzer_name)
954
955 return fuzzer_name
956
957
958def ExecuteShellCommand():
959 """Executes the "shell" command.
960
961 Sets up the sysroot for fuzzing and gives user access to a bash shell it
962 spawns in the sysroot.
963
964 Returns:
965 The exit code of the shell command.
966 """
967 SetUpSysrootForFuzzing()
968 return EnterSysrootShell()
969
970
971def ExecuteSetupCommand():
972 """Executes the "setup" command. Wrapper for SetUpSysrootForFuzzing.
973
974 Sets up the sysroot for fuzzing.
975 """
976 SetUpSysrootForFuzzing()
977
978
979def ExecuteCleanupCommand():
980 """Executes the "cleanup" command. Wrapper for CleanUpSysroot.
981
982 Undoes pre-fuzzing setup.
983 """
984 CleanUpSysroot()
985
986
987def ExecuteCoverageCommand(options):
988 """Executes the "coverage" command.
989
990 Executes the "coverage" command by optionally doing a coverage build of a
991 package, optionally downloading the fuzzer's corpus, optionally copying it
992 into the sysroot, running the fuzzer and then generating a coverage report
993 for the user to view. Causes program to exit if fuzzer is not instrumented
994 with source based coverage.
995
996 Args:
997 options: The parsed arguments passed to this program.
998 """
999 BuildPackage(options.package, options.board, BuildType.COVERAGE)
1000
1001 fuzzer = StripFuzzerPrefixes(options.fuzzer)
1002 fuzzer_sysroot_path = GetFuzzerSysrootPath(fuzzer)
1003 if not IsInstrumentedWithClangCoverage(fuzzer_sysroot_path.chroot):
1004 # Don't run the fuzzer if it isn't instrumented with source based coverage.
1005 # Quit and let the user know how to build the fuzzer properly.
1006 cros_build_lib.Die(
1007 '%s is not instrumented with source based coverage.\nSpecify --package '
1008 'to do a coverage build or build with USE flag: "coverage".', fuzzer)
1009
1010 corpus = options.corpus
1011 if options.download:
1012 corpus = DownloadFuzzerCorpus(options.fuzzer)
1013
1014 # Set up sysroot for fuzzing.
1015 SetUpSysrootForFuzzing()
1016
1017 coverage_report_path = RunFuzzerAndGenerateCoverageReport(
1018 fuzzer, corpus, options.fuzz_args)
1019
1020 # Get path on host so user can access it with their browser.
1021 # TODO(metzman): Add the ability to convert to host paths to path_util.
1022 external_trunk_path = os.getenv('EXTERNAL_TRUNK_PATH')
1023 coverage_report_host_path = os.path.join(external_trunk_path, 'chroot',
1024 coverage_report_path.chroot[1:])
1025 print('Coverage report written to file://%s/index.html' %
1026 coverage_report_host_path)
1027
1028
1029def ExecuteDownloadCommand(options):
1030 """Executes the "download" command. Wrapper around DownloadFuzzerCorpus."""
1031 DownloadFuzzerCorpus(StripFuzzerPrefixes(options.fuzzer), options.directory)
1032
1033
1034def ExecuteReproduceCommand(options):
1035 """Executes the "reproduce" command.
1036
1037 Executes the "reproduce" command by Running a fuzzer on a testcase.
1038 May build the fuzzer before running.
1039
1040 Args:
1041 options: The parsed arguments passed to this program.
1042 """
1043 if options.build_type and not options.package:
1044 raise Exception('Cannot specify --build_type without specifying --package.')
1045
1046 BuildPackage(options.package, options.board, options.build_type)
1047 SetUpSysrootForFuzzing()
1048 Reproduce(StripFuzzerPrefixes(options.fuzzer), options.testcase)
1049
1050
1051def ParseArgs(argv):
1052 """Parses program arguments.
1053
1054 Args:
1055 argv: The program arguments we want to parse.
1056
1057 Returns:
1058 An options object which will tell us which command to run and which options
1059 to use for that command.
1060 """
1061 parser = commandline.ArgumentParser(description=__doc__)
1062
1063 parser.add_argument(
1064 '--board',
1065 default=cros_build_lib.GetDefaultBoard(),
1066 help='Board on which to run test.')
1067
1068 subparsers = parser.add_subparsers(dest='command')
1069
1070 subparsers.add_parser('cleanup', help='Undo setup command.')
1071 coverage_parser = subparsers.add_parser(
1072 'coverage', help='Get a coverage report for a fuzzer.')
1073
1074 coverage_parser.add_argument('--package', help='Package to build.')
1075
1076 corpus_parser = coverage_parser.add_mutually_exclusive_group()
1077 corpus_parser.add_argument('--corpus', help='Corpus to run fuzzer on.')
1078
1079 corpus_parser.add_argument(
1080 '--download',
1081 action='store_true',
1082 help='Generate coverage report based on corpus from ClusterFuzz.')
1083
1084 coverage_parser.add_argument(
1085 '--fuzzer',
1086 required=True,
1087 help='The fuzz target to generate a coverage report for.')
1088
1089 coverage_parser.add_argument(
1090 '--fuzz-args',
1091 default='',
1092 help='Arguments to pass libFuzzer. '
1093 'Please use an equals sign or parsing will fail '
1094 '(i.e. --fuzzer_args="-rss_limit_mb=2048 -print_funcs=1").')
1095
1096 download_parser = subparsers.add_parser('download', help='Download a corpus.')
1097
1098 download_parser.add_argument(
1099 '--directory', help='Path to directory to download the corpus to.')
1100
1101 download_parser.add_argument(
1102 '--fuzzer', required=True, help='Fuzzer to download the corpus for.')
1103
1104 reproduce_parser = subparsers.add_parser(
1105 'reproduce', help='Run a fuzzer on a testcase.')
1106
1107 reproduce_parser.add_argument(
1108 '--testcase', required=True, help='Path of testcase to run fuzzer on.')
1109
1110 reproduce_parser.add_argument(
1111 '--fuzzer', required=True, help='Fuzzer to reproduce the crash on.')
1112
1113 reproduce_parser.add_argument('--package', help='Package to build.')
1114
1115 reproduce_parser.add_argument(
1116 '--build-type',
1117 choices=BuildType.CHOICES,
1118 help='Type of build.',
1119 type=str.lower) # Ignore sanitizer case.
1120
1121 subparsers.add_parser('setup', help='Set up the sysroot to test fuzzing.')
1122
1123 subparsers.add_parser(
1124 'shell',
1125 help='Set up sysroot for fuzzing and get a shell in the sysroot.')
1126
1127 opts = parser.parse_args(argv)
1128 opts.Freeze()
1129 return opts
1130
1131
1132def main(argv):
1133 """Parses arguments and executes a command.
1134
1135 Args:
1136 argv: The prorgram arguments.
1137
1138 Returns:
1139 0 on success. Non-zero on failure.
1140 """
1141 cros_build_lib.AssertInsideChroot()
1142 options = ParseArgs(argv)
1143 if options.board is None:
1144 logging.error('Please specify "--board" or set ".default_board".')
1145 return 1
1146
1147 SysrootPath.SetPathToSysroot(options.board)
1148
1149 if options.command == 'cleanup':
1150 ExecuteCleanupCommand()
1151 elif options.command == 'coverage':
1152 ExecuteCoverageCommand(options)
1153 elif options.command == 'setup':
1154 ExecuteSetupCommand()
1155 elif options.command == 'download':
1156 ExecuteDownloadCommand(options)
1157 elif options.command == 'reproduce':
1158 ExecuteReproduceCommand(options)
1159 elif options.command == 'shell':
1160 return ExecuteShellCommand()
1161
1162 return 0