blob: 0e25549f01ac30017a429088cf2cb5c66348b309 [file] [log] [blame]
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -08001# Copyright 2018 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Script for performing tasks that are useful for fuzzer development.
6
7Run "cros_fuzz" in the chroot for a list of command or "cros_fuzz $COMMAND
8--help" for their full details. Below is a summary of commands that the script
9can perform:
10
11coverage: Generate a coverage report for a given fuzzer (specified by "--fuzzer"
12 option). You almost certainly want to specify the package to build (using
13 the "--package" option) so that a coverage build is done, since a coverage
14 build is needed to generate a report. If your fuzz target is running on
15 ClusterFuzz already, you can use the "--download" option to download the
16 corpus from ClusterFuzz. Otherwise, you can use the "--corpus" option to
17 specify the path of the corpus to run the fuzzer on and generate a report.
18 The corpus will be copied to the sysroot so that the fuzzer can use it.
19 Note that "--download" and "--corpus" are mutually exclusive.
20
21reproduce: Runs the fuzzer specified by the "--fuzzer" option on a testcase
22 (path specified by the "--testcase" argument). Optionally does a build when
23 the "--package" option is used. The type of build can be specified using the
24 "--build_type" argument.
25
26download: Downloads the corpus from ClusterFuzz of the fuzzer specified by the
27 "--fuzzer" option. The path of the directory the corpus directory is
28 downloaded to can be specified using the "--directory" option.
29
30shell: Sets up the sysroot for fuzzing and then chroots into the sysroot giving
31 you a shell that is ready to fuzz.
32
33setup: Sets up the sysroot for fuzzing (done prior to doing "reproduce", "shell"
34 and "coverage" commands).
35
36cleanup: Undoes "setup".
37
38Note that cros_fuzz will print every shell command it runs if you set the
39log-level to debug ("--log-level debug"). Otherwise it will print commands that
40fail.
41"""
42
Chris McDonald59650c32021-07-20 15:29:28 -060043import logging
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -080044import os
45import shutil
46
Chris McDonald59650c32021-07-20 15:29:28 -060047from chromite.third_party import lddtree
48from chromite.third_party.pyelftools.elftools.elf.elffile import ELFFile
49
Mike Frysinger06a51c82021-04-06 11:39:17 -040050from chromite.lib import build_target_lib
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -080051from chromite.lib import commandline
52from chromite.lib import constants
53from chromite.lib import cros_build_lib
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -080054from chromite.lib import gs
55from chromite.lib import osutils
Manoj Guptae5e1e612019-10-21 12:39:57 -070056from chromite.lib import portage_util
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -080057
Mike Frysinger03b983f2020-02-21 02:31:49 -050058
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -080059# 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 """
Mike Frysinger06a51c82021-04-06 11:39:17 -0400142 cls.path_to_sysroot = build_target_lib.get_default_sysroot_path(board)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800143 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)
Manoj Gupta639d30f2019-10-28 16:24:25 -0700291 osutils.RmDir(dest_corpus_path.chroot, ignore_missing=True)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800292 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
Mike Frysinger45602c72019-09-22 02:15:11 -0400323def sudo_run(*args, **kwargs):
324 """Wrapper around cros_build_lib.sudo_run.
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800325
Mike Frysinger45602c72019-09-22 02:15:11 -0400326 Wrapper that calls cros_build_lib.sudo_run but sets debug_level by
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800327 default.
328
329 Args:
Mike Frysinger45602c72019-09-22 02:15:11 -0400330 *args: Positional arguments to pass to cros_build_lib.sudo_run.
331 *kwargs: Keyword arguments to pass to cros_build_lib.sudo_run.
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800332
333 Returns:
Mike Frysinger45602c72019-09-22 02:15:11 -0400334 The value returned by calling cros_build_lib.sudo_run.
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800335 """
336 kwargs.setdefault('debug_level', logging.DEBUG)
Mike Frysinger45602c72019-09-22 02:15:11 -0400337 return cros_build_lib.sudo_run(*args, **kwargs)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800338
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.
Manoj Gupta339b3562021-10-20 11:11:29 -0700409 # disable odr violation since many fuzzers hit it and it is also disabled on
410 # clusterfuzz.
411 options_dict = {'log_path': 'stderr', 'detect_odr_violation': '0'}
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800412 options_dict.update(extra_options)
413 sanitizer_options = ':'.join('%s=%s' % x for x in options_dict.items())
414 sanitizers = ('ASAN', 'MSAN', 'UBSAN')
415 return {x + '_OPTIONS': sanitizer_options for x in sanitizers}
416
417
Manoj Gupta5ca17652019-05-13 11:15:33 -0700418def RunFuzzer(fuzzer,
419 corpus_path=None,
420 fuzz_args='',
421 testcase_path=None,
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800422 crash_expected=False):
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800423 """Runs the fuzzer while chrooted into the sysroot.
424
425 Args:
426 fuzzer: The fuzzer to run.
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800427 corpus_path: A path to a corpus (not necessarily in the sysroot) to run the
428 fuzzer on.
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800429 fuzz_args: Additional arguments to pass to the fuzzer when running it.
430 testcase_path: A path to a testcase (not necessarily in the sysroot) to run
431 the fuzzer on.
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800432 crash_expected: Is it normal for the fuzzer to crash on this run?
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800433 """
434 logging.info('Running fuzzer: %s', fuzzer)
435 fuzzer_sysroot_path = GetFuzzerSysrootPath(fuzzer)
436 fuzz_command = [fuzzer_sysroot_path.sysroot]
437 fuzz_command += fuzz_args.split()
438
439 if testcase_path:
440 fuzz_command.append(testcase_path)
441 else:
442 LimitFuzzing(fuzz_command, corpus_path)
443
444 if corpus_path:
445 fuzz_command.append(corpus_path)
446
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800447 if crash_expected:
448 # Don't return nonzero when fuzzer OOMs, leaks, or timesout, since we don't
449 # want an exception in those cases. The user may be trying to reproduce
450 # those issues.
451 fuzz_command += ['-error_exitcode=0', '-timeout_exitcode=0']
452
453 # We must set exitcode=0 or else the fuzzer will return nonzero on
454 # successful reproduction.
455 sanitizer_options = {'exitcode': '0'}
456 else:
457 sanitizer_options = {}
458
459 extra_env = GetFuzzExtraEnv(sanitizer_options)
460 RunSysrootCommand(fuzz_command, extra_env=extra_env, debug_level=logging.INFO)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800461
462
463def MergeProfraw(fuzzer):
464 """Merges profraw file from a fuzzer and creates a profdata file.
465
466 Args:
467 fuzzer: The fuzzer to merge the profraw file from.
468 """
469 profdata_path = GetProfdataPath(fuzzer)
470 command = [
471 'llvm-profdata',
472 'merge',
473 '-sparse',
474 DEFAULT_PROFRAW_PATH,
475 '-o',
476 profdata_path.sysroot,
477 ]
478
479 RunSysrootCommand(command)
480 return profdata_path
481
482
483def GenerateCoverageReport(fuzzer, shared_libraries):
484 """Generates an HTML coverage report from a fuzzer run.
485
486 Args:
487 fuzzer: The fuzzer to generate the coverage report for.
488 shared_libraries: Libraries loaded dynamically by |fuzzer|.
489
490 Returns:
491 The path of the coverage report.
492 """
493 fuzzer_path = GetFuzzerSysrootPath(fuzzer).chroot
494 command = ['llvm-cov', 'show', '-object', fuzzer_path]
495 for library in shared_libraries:
496 command += ['-object', library]
497
498 coverage_directory = GetCoverageDirectory(fuzzer)
499 command += [
500 '-format=html',
501 '-instr-profile=%s' % GetProfdataPath(fuzzer).chroot,
502 '-output-dir=%s' % coverage_directory.chroot,
503 ]
504
505 # TODO(metzman): Investigate error messages printed by this command.
Mike Frysinger0282d222019-12-17 17:15:48 -0500506 cros_build_lib.run(command, stderr=True, debug_level=logging.DEBUG)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800507 return coverage_directory
508
509
510def GetSharedLibraries(binary_path):
511 """Gets the shared libraries used by a binary.
512
513 Gets the shared libraries used by the binary. Based on GetSharedLibraries from
514 src/tools/code_coverage/coverage_utils.py in Chromium.
515
516 Args:
517 binary_path: The path to the binary we want to find the shared libraries of.
518
519 Returns:
520 The shared libraries used by |binary_path|.
521 """
522 logging.info('Finding shared libraries for targets (if any).')
523 shared_libraries = []
524 elf_dict = lddtree.ParseELF(
525 binary_path.chroot, root=SysrootPath.path_to_sysroot)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400526 for shared_library in elf_dict['libs'].values():
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800527 shared_library_path = shared_library['path']
528
529 if shared_library_path in shared_libraries:
530 continue
531
532 assert os.path.exists(shared_library_path), ('Shared library "%s" used by '
533 'the given target(s) does not '
534 'exist.' % shared_library_path)
535
536 if IsInstrumentedWithClangCoverage(shared_library_path):
537 # Do not add non-instrumented libraries. Otherwise, llvm-cov errors out.
538 shared_libraries.append(shared_library_path)
539
540 logging.debug('Found shared libraries (%d): %s.', len(shared_libraries),
541 shared_libraries)
542 logging.info('Finished finding shared libraries for targets.')
543 return shared_libraries
544
545
546def IsInstrumentedWithClangCoverage(binary_path):
547 """Determines if a binary is instrumented with clang source based coverage.
548
549 Args:
550 binary_path: The path of the binary (executable or library) we are checking
551 is instrumented with clang source based coverage.
552
553 Returns:
554 True if the binary is instrumented with clang source based coverage.
555 """
556 with open(binary_path, 'rb') as file_handle:
557 elf_file = ELFFile(file_handle)
Manoj Gupta339b3562021-10-20 11:11:29 -0700558 return elf_file.get_section_by_name(b'__llvm_covmap') is not None
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800559
560
561def RunFuzzerAndGenerateCoverageReport(fuzzer, corpus, fuzz_args):
562 """Runs a fuzzer generates a coverage report and returns the report's path.
563
564 Gets a coverage report for a fuzzer.
565
566 Args:
567 fuzzer: The fuzzer to run and generate the coverage report for.
568 corpus: The path to a corpus to run the fuzzer on.
569 fuzz_args: Additional arguments to pass to the fuzzer.
570
571 Returns:
572 The path to the coverage report.
573 """
574 corpus_path = CopyCorpusToSysroot(corpus)
575 if corpus_path:
576 corpus_path = corpus_path.sysroot
577
578 RunFuzzer(fuzzer, corpus_path=corpus_path, fuzz_args=fuzz_args)
579 MergeProfraw(fuzzer)
580 fuzzer_sysroot_path = GetFuzzerSysrootPath(fuzzer)
581 shared_libraries = GetSharedLibraries(fuzzer_sysroot_path)
582 return GenerateCoverageReport(fuzzer, shared_libraries)
583
584
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800585def RunSysrootCommand(command, **kwargs):
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800586 """Runs command while chrooted into sysroot and returns the output.
587
588 Args:
589 command: A command to run in the sysroot.
Mike Frysinger45602c72019-09-22 02:15:11 -0400590 kwargs: Extra arguments to pass to cros_build_lib.sudo_run.
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800591
592 Returns:
Mike Frysinger45602c72019-09-22 02:15:11 -0400593 The result of a call to cros_build_lib.sudo_run.
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800594 """
595 command = ['chroot', SysrootPath.path_to_sysroot] + command
Mike Frysinger45602c72019-09-22 02:15:11 -0400596 return sudo_run(command, **kwargs)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800597
598
599def GetBuildExtraEnv(build_type):
600 """Gets the extra_env for building a package.
601
602 Args:
603 build_type: The type of build we want to do.
604
605 Returns:
606 The extra_env to use when building.
607 """
608 if build_type is None:
609 build_type = BuildType.ASAN
610
611 use_flags = os.environ.get('USE', '').split()
612 # Check that the user hasn't already set USE flags that we can set.
613 # No good way to iterate over an enum in python2.
614 for use_flag in BuildType.CHOICES:
615 if use_flag in use_flags:
Mike Frysinger968c1142020-05-09 00:37:56 -0400616 logging.warning('%s in USE flags. Please use --build_type instead.',
617 use_flag)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800618
619 # Set USE flags.
620 fuzzer_build_type = 'fuzzer'
621 use_flags += [fuzzer_build_type, build_type]
622 features_flags = os.environ.get('FEATURES', '').split()
623 if build_type == BuildType.COVERAGE:
624 # We must use ASan when doing coverage builds.
625 use_flags.append(BuildType.ASAN)
626 # Use noclean so that a coverage report can be generated based on the source
627 # code.
628 features_flags.append('noclean')
629
630 return {
631 'FEATURES': ' '.join(features_flags),
632 'USE': ' '.join(use_flags),
633 }
634
635
636def BuildPackage(package, board, build_type):
637 """Builds a package on a specified board.
638
639 Args:
640 package: The package to build. Nothing is built if None.
641 board: The board to build the package on.
Manoj Guptae207b562019-05-02 11:30:35 -0700642 build_type: The type of the build to do (e.g. asan, msan, ubsan, coverage).
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800643 """
644 if package is None:
645 return
646
647 logging.info('Building %s using %s.', package, build_type)
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800648 extra_env = GetBuildExtraEnv(build_type)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800649 command = [
Paul Moy2fe30722022-05-31 15:33:26 -0600650 'build_packages',
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800651 '--board',
652 board,
Paul Moy2fe30722022-05-31 15:33:26 -0600653 '--skip-chroot-upgrade',
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800654 package,
655 ]
Paul Moy2fe30722022-05-31 15:33:26 -0600656 # For msan builds, always use "--no-usepkg" since all package needs to be
Manoj Gupta5ca17652019-05-13 11:15:33 -0700657 # instrumented with msan.
658 if build_type == BuildType.MSAN:
Paul Moy2fe30722022-05-31 15:33:26 -0600659 command += ['--no-usepkg']
Manoj Gupta5ca17652019-05-13 11:15:33 -0700660
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800661 # Print the output of the build command. Do this because it is familiar to
662 # devs and we don't want to leave them not knowing about the build's progress
663 # for a long time.
Mike Frysinger45602c72019-09-22 02:15:11 -0400664 cros_build_lib.run(command, extra_env=extra_env)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800665
666
667def DownloadFuzzerCorpus(fuzzer, dest_directory=None):
668 """Downloads a corpus and returns its path.
669
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800670 Downloads a corpus to a subdirectory of dest_directory if specified and
671 returns path on the filesystem of the corpus. Asks users to authenticate
672 if permission to read from bucket is denied.
673
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800674 Args:
675 fuzzer: The name of the fuzzer who's corpus we want to download.
676 dest_directory: The directory to download the corpus to.
677
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800678 Returns:
679 The path to the downloaded corpus.
680
681 Raises:
682 gs.NoSuchKey: A corpus for the fuzzer doesn't exist.
683 gs.GSCommandError: The corpus failed to download for another reason.
684 """
685 if not fuzzer.startswith('chromeos_'):
686 # ClusterFuzz internally appends "chromeos_" to chromeos targets' names.
687 # Therefore we must do so in order to find the corpus.
688 fuzzer = 'chromeos_%s' % fuzzer
689
690 if dest_directory is None:
691 dest_directory = GetScriptStoragePath(CORPUS_DIRECTORY_NAME).chroot
692 osutils.SafeMakedirsNonRoot(dest_directory)
693
694 clusterfuzz_gcs_corpus_bucket = 'chromeos-corpus'
695 suburl = 'libfuzzer/%s' % fuzzer
696 gcs_path = gs.GetGsURL(
697 clusterfuzz_gcs_corpus_bucket,
698 for_gsutil=True,
699 public=False,
700 suburl=suburl)
701
702 dest_path = os.path.join(dest_directory, fuzzer)
703
704 try:
705 logging.info('Downloading corpus to %s.', dest_path)
706 ctx = gs.GSContext()
707 ctx.Copy(
708 gcs_path,
709 dest_directory,
710 recursive=True,
711 parallel=True,
712 debug_level=logging.DEBUG)
713 logging.info('Finished downloading corpus.')
714 except gs.GSNoSuchKey as exception:
715 logging.error('Corpus for fuzzer: %s does not exist.', fuzzer)
716 raise exception
717 # Try to authenticate if we were denied permission to access the corpus.
718 except gs.GSCommandError as exception:
719 logging.error(
720 'gsutil failed to download the corpus. You may need to log in. See:\n'
Mike Frysinger92dc6492021-02-17 16:01:09 -0500721 'https://chromium.googlesource.com/chromiumos/docs/+/HEAD/gsutil.md'
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800722 '#setup\n'
723 'for instructions on doing this.')
724 raise exception
725
726 return dest_path
727
728
729def Reproduce(fuzzer, testcase_path):
730 """Runs a fuzzer in the sysroot on a testcase.
731
732 Args:
733 fuzzer: The fuzzer to run.
734 testcase_path: The path (not necessarily in the sysroot) of the testcase to
735 run the fuzzer on.
736 """
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800737 testcase_sysroot_path = CopyTestcaseToSysroot(testcase_path).sysroot
738 RunFuzzer(fuzzer, testcase_path=testcase_sysroot_path, crash_expected=True)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800739
740
741def SetUpSysrootForFuzzing():
742 """Sets up the the sysroot for fuzzing
743
744 Prepares the sysroot for fuzzing. Idempotent.
745 """
746 logging.info('Setting up sysroot for fuzzing.')
747 # TODO(metzman): Don't create devices or mount /proc, use platform2_test.py
748 # instead.
749 # Mount /proc in sysroot and setup dev there because they are needed by
750 # sanitizers.
751 proc_manager = ProcManager()
752 proc_manager.Mount()
753
754 # Setup devices in /dev that are needed by libFuzzer.
755 device_manager = DeviceManager()
756 device_manager.SetUp()
757
758 # Set up asan_symbolize.py, llvm-symbolizer, and llvm-profdata in the
759 # sysroot so that fuzzer output (including stack traces) can be symbolized
760 # and so that coverage reports can be generated.
761 tool_manager = ToolManager()
762 tool_manager.Install()
763
764 osutils.SafeMakedirsNonRoot(GetSysrootPath(SCRIPT_STORAGE_PATH))
765
766
767def CleanUpSysroot():
768 """Cleans up the the sysroot from SetUpSysrootForFuzzing.
769
770 Undoes SetUpSysrootForFuzzing. Idempotent.
771 """
772 logging.info('Cleaning up the sysroot.')
773 proc_manager = ProcManager()
774 proc_manager.Unmount()
775
776 device_manager = DeviceManager()
777 device_manager.CleanUp()
778
779 tool_manager = ToolManager()
780 tool_manager.Uninstall()
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800781 osutils.RmDir(GetSysrootPath(SCRIPT_STORAGE_PATH), ignore_missing=True)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800782
783
784class ToolManager(object):
785 """Class that installs or uninstalls fuzzing tools to/from the sysroot.
786
787 Install and Uninstall methods are idempotent. Both are safe to call at any
788 point.
789 """
790
791 # Path to asan_symbolize.py.
792 ASAN_SYMBOLIZE_PATH = os.path.join('/', 'usr', 'bin', 'asan_symbolize.py')
793
794 # List of LLVM binaries we must install in sysroot.
Manoj Guptafeb1b7a2019-02-20 11:04:05 -0800795 LLVM_BINARY_NAMES = ['gdbserver', 'llvm-symbolizer', 'llvm-profdata']
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800796
797 def __init__(self):
798 self.asan_symbolize_sysroot_path = GetSysrootPath(self.ASAN_SYMBOLIZE_PATH)
799
800 def Install(self):
801 """Installs tools to the sysroot."""
802 # Install asan_symbolize.py.
Mike Frysinger45602c72019-09-22 02:15:11 -0400803 sudo_run(['cp', self.ASAN_SYMBOLIZE_PATH, self.asan_symbolize_sysroot_path])
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800804 # Install the LLVM binaries.
805 # TODO(metzman): Build these tools so that we don't mess up when board is
806 # for a different ISA.
807 for llvm_binary in self._GetLLVMBinaries():
808 llvm_binary.Install()
809
810 def Uninstall(self):
811 """Uninstalls tools from the sysroot. Undoes Install."""
812 # Uninstall asan_symbolize.py.
813 osutils.SafeUnlink(self.asan_symbolize_sysroot_path, sudo=True)
814 # Uninstall the LLVM binaries.
815 for llvm_binary in self._GetLLVMBinaries():
816 llvm_binary.Uninstall()
817
818 def _GetLLVMBinaries(self):
819 """Creates LllvmBinary objects for each binary name in LLVM_BINARY_NAMES."""
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800820 return [LlvmBinary(x) for x in self.LLVM_BINARY_NAMES]
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800821
822
823class LlvmBinary(object):
824 """Class for representing installing/uninstalling an LLVM binary in sysroot.
825
826 Install and Uninstall methods are idempotent. Both are safe to call at any
827 time.
828 """
829
830 # Path to the lddtree chromite script.
831 LDDTREE_SCRIPT_PATH = os.path.join(constants.CHROMITE_BIN_DIR, 'lddtree')
832
833 def __init__(self, binary):
834 self.binary = binary
835 self.install_dir = GetSysrootPath(
836 os.path.join('/', 'usr', 'libexec', binary))
837 self.binary_dir_path = GetSysrootPath(os.path.join('/', 'usr', 'bin'))
838 self.binary_chroot_dest_path = os.path.join(self.binary_dir_path, binary)
839
840 def Uninstall(self):
841 """Removes an LLVM binary from sysroot. Undoes Install."""
842 osutils.RmDir(self.install_dir, ignore_missing=True, sudo=True)
843 osutils.SafeUnlink(self.binary_chroot_dest_path, sudo=True)
844
845 def Install(self):
846 """Installs (sets up) an LLVM binary in the sysroot.
847
848 Sets up an llvm binary in the sysroot so that it can be run there.
849 """
850 # Create a directory for installing |binary| and all of its dependencies in
851 # the sysroot.
852 binary_rel_path = ['usr', 'bin', self.binary]
853 binary_chroot_path = os.path.join('/', *binary_rel_path)
Manoj Guptafeb1b7a2019-02-20 11:04:05 -0800854 if not os.path.exists(binary_chroot_path):
855 logging.warning('Cannot copy %s, file does not exist in chroot.',
856 binary_chroot_path)
857 logging.warning('Functionality provided by %s will be missing.',
858 binary_chroot_path)
859 return
860
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800861 osutils.SafeMakedirsNonRoot(self.install_dir)
862
863 # Copy the binary and everything needed to run it into the sysroot.
864 cmd = [
865 self.LDDTREE_SCRIPT_PATH,
866 '-v',
867 '--generate-wrappers',
868 '--root',
869 '/',
870 '--copy-to-tree',
871 self.install_dir,
872 binary_chroot_path,
873 ]
Mike Frysinger45602c72019-09-22 02:15:11 -0400874 sudo_run(cmd)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800875
876 # Create a symlink to the copy of the binary (we can't do lddtree in
877 # self.binary_dir_path). Note that symlink should be relative so that it
878 # will be valid when chrooted into the sysroot.
879 rel_path = os.path.relpath(self.install_dir, self.binary_dir_path)
880 link_path = os.path.join(rel_path, *binary_rel_path)
881 osutils.SafeSymlink(link_path, self.binary_chroot_dest_path, sudo=True)
882
883
884class DeviceManager(object):
885 """Class that creates or removes devices from /dev in sysroot.
886
887 SetUp and CleanUp methods are idempotent. Both are safe to call at any point.
888 """
889
890 DEVICE_MKNOD_PARAMS = {
891 'null': (666, 3),
892 'random': (444, 8),
893 'urandom': (444, 9),
894 }
895
896 MKNOD_MAJOR = '1'
897
898 def __init__(self):
899 self.dev_path_chroot = GetSysrootPath('/dev')
900
901 def _GetDevicePath(self, device_name):
902 """Returns the path of |device_name| in sysroot's /dev."""
903 return os.path.join(self.dev_path_chroot, device_name)
904
905 def SetUp(self):
906 """Sets up devices in the sysroot's /dev.
907
908 Creates /dev/null, /dev/random, and /dev/urandom. If they already exist then
909 recreates them.
910 """
911 self.CleanUp()
912 osutils.SafeMakedirsNonRoot(self.dev_path_chroot)
Mike Frysinger0bdbc102019-06-13 15:27:29 -0400913 for device, mknod_params in self.DEVICE_MKNOD_PARAMS.items():
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800914 device_path = self._GetDevicePath(device)
915 self._MakeCharDevice(device_path, *mknod_params)
916
917 def CleanUp(self):
918 """Cleans up devices in the sysroot's /dev. Undoes SetUp.
919
920 Removes /dev/null, /dev/random, and /dev/urandom if they exist.
921 """
922 for device in self.DEVICE_MKNOD_PARAMS:
923 device_path = self._GetDevicePath(device)
924 if os.path.exists(device_path):
925 # Use -r since dev/null is sometimes a directory.
Mike Frysinger45602c72019-09-22 02:15:11 -0400926 sudo_run(['rm', '-r', device_path])
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800927
928 def _MakeCharDevice(self, path, mode, minor):
929 """Make a character device."""
930 mode = str(mode)
931 minor = str(minor)
932 command = ['mknod', '-m', mode, path, 'c', self.MKNOD_MAJOR, minor]
Mike Frysinger45602c72019-09-22 02:15:11 -0400933 sudo_run(command)
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800934
935
936class ProcManager(object):
937 """Class that mounts or unmounts /proc in sysroot.
938
939 Mount and Unmount are idempotent. Both are safe to call at any point.
940 """
941
942 PROC_PATH = '/proc'
943
944 def __init__(self):
945 self.proc_path_chroot = GetSysrootPath(self.PROC_PATH)
946 self.is_mounted = osutils.IsMounted(self.proc_path_chroot)
947
948 def Unmount(self):
949 """Unmounts /proc in chroot. Undoes Mount."""
950 if not self.is_mounted:
951 return
952 osutils.UmountDir(self.proc_path_chroot, cleanup=False)
953
954 def Mount(self):
955 """Mounts /proc in chroot. Remounts it if already mounted."""
956 self.Unmount()
957 osutils.MountDir(
958 self.PROC_PATH,
959 self.proc_path_chroot,
960 'proc',
961 debug_level=logging.DEBUG)
962
963
964def EnterSysrootShell():
965 """Spawns and gives user access to a bash shell in the sysroot."""
966 command = ['/bin/bash', '-i']
967 return RunSysrootCommand(
Jonathan Metzmanb2c33732018-11-08 11:33:35 -0800968 command,
969 extra_env=GetFuzzExtraEnv(),
970 debug_level=logging.INFO,
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -0500971 check=False).returncode
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -0800972
973
974def StripFuzzerPrefixes(fuzzer_name):
975 """Strip the prefix ClusterFuzz uses in case they are specified.
976
977 Strip the prefixes used by ClusterFuzz if the users has included them by
978 accident.
979
980 Args:
981 fuzzer_name: The fuzzer who's name may contain prefixes.
982
983 Returns:
984 The name of the fuzz target without prefixes.
985 """
986 initial_name = fuzzer_name
987
988 def StripPrefix(prefix):
989 if fuzzer_name.startswith(prefix):
990 return fuzzer_name[len(prefix):]
991 return fuzzer_name
992
993 clusterfuzz_prefixes = ['libFuzzer_', 'chromeos_']
994
995 for prefix in clusterfuzz_prefixes:
996 fuzzer_name = StripPrefix(prefix)
997
998 if initial_name != fuzzer_name:
Mike Frysinger968c1142020-05-09 00:37:56 -0400999 logging.warning(
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -08001000 '%s contains a prefix from ClusterFuzz (one or more of %s) that is not '
Mike Frysinger80de5012019-08-01 14:10:53 -04001001 "part of the fuzzer's name. Interpreting --fuzzer as %s.",
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -08001002 initial_name, clusterfuzz_prefixes, fuzzer_name)
1003
1004 return fuzzer_name
1005
1006
1007def ExecuteShellCommand():
1008 """Executes the "shell" command.
1009
1010 Sets up the sysroot for fuzzing and gives user access to a bash shell it
1011 spawns in the sysroot.
1012
1013 Returns:
1014 The exit code of the shell command.
1015 """
1016 SetUpSysrootForFuzzing()
1017 return EnterSysrootShell()
1018
1019
1020def ExecuteSetupCommand():
1021 """Executes the "setup" command. Wrapper for SetUpSysrootForFuzzing.
1022
1023 Sets up the sysroot for fuzzing.
1024 """
1025 SetUpSysrootForFuzzing()
1026
1027
1028def ExecuteCleanupCommand():
1029 """Executes the "cleanup" command. Wrapper for CleanUpSysroot.
1030
1031 Undoes pre-fuzzing setup.
1032 """
1033 CleanUpSysroot()
1034
1035
1036def ExecuteCoverageCommand(options):
1037 """Executes the "coverage" command.
1038
1039 Executes the "coverage" command by optionally doing a coverage build of a
1040 package, optionally downloading the fuzzer's corpus, optionally copying it
1041 into the sysroot, running the fuzzer and then generating a coverage report
1042 for the user to view. Causes program to exit if fuzzer is not instrumented
1043 with source based coverage.
1044
1045 Args:
1046 options: The parsed arguments passed to this program.
1047 """
1048 BuildPackage(options.package, options.board, BuildType.COVERAGE)
1049
1050 fuzzer = StripFuzzerPrefixes(options.fuzzer)
1051 fuzzer_sysroot_path = GetFuzzerSysrootPath(fuzzer)
1052 if not IsInstrumentedWithClangCoverage(fuzzer_sysroot_path.chroot):
1053 # Don't run the fuzzer if it isn't instrumented with source based coverage.
1054 # Quit and let the user know how to build the fuzzer properly.
1055 cros_build_lib.Die(
1056 '%s is not instrumented with source based coverage.\nSpecify --package '
1057 'to do a coverage build or build with USE flag: "coverage".', fuzzer)
1058
1059 corpus = options.corpus
1060 if options.download:
1061 corpus = DownloadFuzzerCorpus(options.fuzzer)
1062
1063 # Set up sysroot for fuzzing.
1064 SetUpSysrootForFuzzing()
1065
1066 coverage_report_path = RunFuzzerAndGenerateCoverageReport(
1067 fuzzer, corpus, options.fuzz_args)
1068
1069 # Get path on host so user can access it with their browser.
1070 # TODO(metzman): Add the ability to convert to host paths to path_util.
1071 external_trunk_path = os.getenv('EXTERNAL_TRUNK_PATH')
1072 coverage_report_host_path = os.path.join(external_trunk_path, 'chroot',
1073 coverage_report_path.chroot[1:])
1074 print('Coverage report written to file://%s/index.html' %
1075 coverage_report_host_path)
1076
1077
1078def ExecuteDownloadCommand(options):
1079 """Executes the "download" command. Wrapper around DownloadFuzzerCorpus."""
1080 DownloadFuzzerCorpus(StripFuzzerPrefixes(options.fuzzer), options.directory)
1081
1082
1083def ExecuteReproduceCommand(options):
1084 """Executes the "reproduce" command.
1085
1086 Executes the "reproduce" command by Running a fuzzer on a testcase.
1087 May build the fuzzer before running.
1088
1089 Args:
1090 options: The parsed arguments passed to this program.
1091 """
1092 if options.build_type and not options.package:
1093 raise Exception('Cannot specify --build_type without specifying --package.')
1094
Manoj Gupta5ca17652019-05-13 11:15:33 -07001095 # Verify that "msan-fuzzer" profile is being used with msan.
1096 # Check presence of "-fsanitize=memory" in CFLAGS.
1097 if options.build_type == BuildType.MSAN:
1098 cmd = ['portageq-%s' % options.board, 'envvar', 'CFLAGS']
Manoj Guptabc6fc7f2020-04-17 19:24:24 -07001099 cflags = cros_build_lib.run(
1100 cmd, capture_output=True, encoding='utf-8').stdout.splitlines()
Manoj Gupta5ca17652019-05-13 11:15:33 -07001101 check_string = '-fsanitize=memory'
1102 if not any(check_string in s for s in cflags):
1103 logging.error(
1104 '-fsanitize=memory not found in CFLAGS. '
1105 'Use "setup_board --board=amd64-generic --profile=msan-fuzzer" '
1106 'for MSan Fuzzing Builds.')
1107 raise Exception('Incompatible profile used for msan fuzzing.')
1108
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -08001109 BuildPackage(options.package, options.board, options.build_type)
1110 SetUpSysrootForFuzzing()
1111 Reproduce(StripFuzzerPrefixes(options.fuzzer), options.testcase)
1112
Manoj Guptae5e1e612019-10-21 12:39:57 -07001113
Manoj Guptaec08b812019-10-10 14:21:16 -07001114def InstallBaseDependencies(options):
Mike Frysingerdf1d0b02019-11-12 17:44:12 -05001115 """Installs the base packages needed to chroot in board sysroot.
Manoj Guptaec08b812019-10-10 14:21:16 -07001116
1117 Args:
1118 options: The parsed arguments passed to this program.
1119 """
Manoj Guptae5e1e612019-10-21 12:39:57 -07001120 package = 'virtual/implicit-system'
1121 if not portage_util.IsPackageInstalled(
1122 package, sysroot=SysrootPath.path_to_sysroot):
1123 build_type = getattr(options, 'build_type', None)
1124 BuildPackage(package, options.board, build_type)
1125
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -08001126
1127def ParseArgs(argv):
1128 """Parses program arguments.
1129
1130 Args:
1131 argv: The program arguments we want to parse.
1132
1133 Returns:
1134 An options object which will tell us which command to run and which options
1135 to use for that command.
1136 """
1137 parser = commandline.ArgumentParser(description=__doc__)
1138
1139 parser.add_argument(
1140 '--board',
1141 default=cros_build_lib.GetDefaultBoard(),
1142 help='Board on which to run test.')
1143
1144 subparsers = parser.add_subparsers(dest='command')
1145
1146 subparsers.add_parser('cleanup', help='Undo setup command.')
1147 coverage_parser = subparsers.add_parser(
1148 'coverage', help='Get a coverage report for a fuzzer.')
1149
1150 coverage_parser.add_argument('--package', help='Package to build.')
1151
1152 corpus_parser = coverage_parser.add_mutually_exclusive_group()
1153 corpus_parser.add_argument('--corpus', help='Corpus to run fuzzer on.')
1154
1155 corpus_parser.add_argument(
1156 '--download',
1157 action='store_true',
1158 help='Generate coverage report based on corpus from ClusterFuzz.')
1159
1160 coverage_parser.add_argument(
1161 '--fuzzer',
1162 required=True,
1163 help='The fuzz target to generate a coverage report for.')
1164
1165 coverage_parser.add_argument(
1166 '--fuzz-args',
1167 default='',
1168 help='Arguments to pass libFuzzer. '
1169 'Please use an equals sign or parsing will fail '
1170 '(i.e. --fuzzer_args="-rss_limit_mb=2048 -print_funcs=1").')
1171
1172 download_parser = subparsers.add_parser('download', help='Download a corpus.')
1173
1174 download_parser.add_argument(
1175 '--directory', help='Path to directory to download the corpus to.')
1176
1177 download_parser.add_argument(
1178 '--fuzzer', required=True, help='Fuzzer to download the corpus for.')
1179
1180 reproduce_parser = subparsers.add_parser(
1181 'reproduce', help='Run a fuzzer on a testcase.')
1182
1183 reproduce_parser.add_argument(
1184 '--testcase', required=True, help='Path of testcase to run fuzzer on.')
1185
1186 reproduce_parser.add_argument(
1187 '--fuzzer', required=True, help='Fuzzer to reproduce the crash on.')
1188
1189 reproduce_parser.add_argument('--package', help='Package to build.')
1190
1191 reproduce_parser.add_argument(
1192 '--build-type',
1193 choices=BuildType.CHOICES,
1194 help='Type of build.',
1195 type=str.lower) # Ignore sanitizer case.
1196
1197 subparsers.add_parser('setup', help='Set up the sysroot to test fuzzing.')
1198
1199 subparsers.add_parser(
1200 'shell',
1201 help='Set up sysroot for fuzzing and get a shell in the sysroot.')
1202
1203 opts = parser.parse_args(argv)
1204 opts.Freeze()
1205 return opts
1206
1207
1208def main(argv):
1209 """Parses arguments and executes a command.
1210
1211 Args:
1212 argv: The prorgram arguments.
1213
1214 Returns:
1215 0 on success. Non-zero on failure.
1216 """
1217 cros_build_lib.AssertInsideChroot()
1218 options = ParseArgs(argv)
1219 if options.board is None:
1220 logging.error('Please specify "--board" or set ".default_board".')
1221 return 1
1222
1223 SysrootPath.SetPathToSysroot(options.board)
1224
Manoj Guptaec08b812019-10-10 14:21:16 -07001225 InstallBaseDependencies(options)
1226
Jonathan Metzmand5ee1c62018-11-05 10:33:08 -08001227 if options.command == 'cleanup':
1228 ExecuteCleanupCommand()
1229 elif options.command == 'coverage':
1230 ExecuteCoverageCommand(options)
1231 elif options.command == 'setup':
1232 ExecuteSetupCommand()
1233 elif options.command == 'download':
1234 ExecuteDownloadCommand(options)
1235 elif options.command == 'reproduce':
1236 ExecuteReproduceCommand(options)
1237 elif options.command == 'shell':
1238 return ExecuteShellCommand()
1239
1240 return 0