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