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