• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Module for running fuzzers."""
15import enum
16import logging
17import os
18import shutil
19import sys
20import time
21
22import clusterfuzz_deployment
23import fuzz_target
24import generate_coverage_report
25import stack_parser
26import workspace_utils
27
28# pylint: disable=wrong-import-position,import-error
29sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30
31import utils
32
33
34class RunFuzzersResult(enum.Enum):
35  """Enum result from running fuzzers."""
36  ERROR = 0
37  BUG_FOUND = 1
38  NO_BUG_FOUND = 2
39
40
41class BaseFuzzTargetRunner:
42  """Base class for fuzzer runners."""
43
44  def __init__(self, config):
45    self.config = config
46    self.workspace = workspace_utils.Workspace(config)
47    self.clusterfuzz_deployment = (
48        clusterfuzz_deployment.get_clusterfuzz_deployment(
49            self.config, self.workspace))
50
51    # Set by the initialize method.
52    self.fuzz_target_paths = None
53
54  def get_fuzz_targets(self):
55    """Returns fuzz targets in out directory."""
56    return utils.get_fuzz_targets(self.workspace.out)
57
58  def initialize(self):
59    """Initialization method. Must be called before calling run_fuzz_targets.
60    Returns True on success."""
61    # Use a seperate initialization function so we can return False on failure
62    # instead of exceptioning like we need to do if this were done in the
63    # __init__ method.
64
65    logging.info('Using %s sanitizer.', self.config.sanitizer)
66
67    # TODO(metzman) Add a check to ensure we aren't over time limit.
68    if not self.config.fuzz_seconds or self.config.fuzz_seconds < 1:
69      logging.error(
70          'Fuzz_seconds argument must be greater than 1, but was: %s.',
71          self.config.fuzz_seconds)
72      return False
73
74    if not os.path.exists(self.workspace.out):
75      logging.error('Out directory: %s does not exist.', self.workspace.out)
76      return False
77
78    if not os.path.exists(self.workspace.artifacts):
79      os.makedirs(self.workspace.artifacts)
80    elif (not os.path.isdir(self.workspace.artifacts) or
81          os.listdir(self.workspace.artifacts)):
82      logging.error('Artifacts path: %s exists and is not an empty directory.',
83                    self.workspace.artifacts)
84      return False
85
86    self.fuzz_target_paths = self.get_fuzz_targets()
87    logging.info('Fuzz targets: %s', self.fuzz_target_paths)
88    if not self.fuzz_target_paths:
89      logging.error('No fuzz targets were found in out directory: %s.',
90                    self.workspace.out)
91      return False
92
93    return True
94
95  def cleanup_after_fuzz_target_run(self, fuzz_target_obj):  # pylint: disable=no-self-use
96    """Cleans up after running |fuzz_target_obj|."""
97    raise NotImplementedError('Child class must implement method.')
98
99  def run_fuzz_target(self, fuzz_target_obj):  # pylint: disable=no-self-use
100    """Fuzzes with |fuzz_target_obj| and returns the result."""
101    raise NotImplementedError('Child class must implement method.')
102
103  @property
104  def quit_on_bug_found(self):
105    """Property that is checked to determine if fuzzing should quit after first
106    bug is found."""
107    raise NotImplementedError('Child class must implement method.')
108
109  def get_fuzz_target_artifact(self, target, artifact_name):
110    """Returns the path of a fuzzing artifact named |artifact_name| for
111    |fuzz_target|."""
112    artifact_name = (f'{target.target_name}-{self.config.sanitizer}-'
113                     f'{artifact_name}')
114    return os.path.join(self.workspace.artifacts, artifact_name)
115
116  def create_fuzz_target_obj(self, target_path, run_seconds):
117    """Returns a fuzz target object."""
118    return fuzz_target.FuzzTarget(target_path, run_seconds, self.workspace,
119                                  self.clusterfuzz_deployment, self.config)
120
121  def run_fuzz_targets(self):
122    """Runs fuzz targets. Returns True if a bug was found."""
123    fuzzers_left_to_run = len(self.fuzz_target_paths)
124
125    # Make a copy since we will mutate it.
126    fuzz_seconds = self.config.fuzz_seconds
127
128    min_seconds_per_fuzzer = fuzz_seconds // fuzzers_left_to_run
129    bug_found = False
130    for target_path in self.fuzz_target_paths:
131      # By doing this, we can ensure that every fuzz target runs for at least
132      # min_seconds_per_fuzzer, but that other fuzzers will have longer to run
133      # if one ends early.
134      run_seconds = max(fuzz_seconds // fuzzers_left_to_run,
135                        min_seconds_per_fuzzer)
136
137      target = self.create_fuzz_target_obj(target_path, run_seconds)
138      start_time = time.time()
139      result = self.run_fuzz_target(target)
140      self.cleanup_after_fuzz_target_run(target)
141
142      # It's OK if this goes negative since we take max when determining
143      # run_seconds.
144      fuzz_seconds -= time.time() - start_time
145
146      fuzzers_left_to_run -= 1
147      if not result.testcase or not result.stacktrace:
148        logging.info('Fuzzer %s finished running without crashes.',
149                     target.target_name)
150        continue
151
152      # TODO(metzman): Do this with filestore.
153      testcase_artifact_path = self.get_fuzz_target_artifact(
154          target, os.path.basename(result.testcase))
155      shutil.move(result.testcase, testcase_artifact_path)
156      bug_summary_artifact_path = self.get_fuzz_target_artifact(
157          target, 'bug-summary.txt')
158      stack_parser.parse_fuzzer_output(result.stacktrace,
159                                       bug_summary_artifact_path)
160
161      bug_found = True
162      if self.quit_on_bug_found:
163        logging.info('Bug found. Stopping fuzzing.')
164        return bug_found
165
166    return bug_found
167
168
169class PruneTargetRunner(BaseFuzzTargetRunner):
170  """Runner that prunes corpora."""
171
172  @property
173  def quit_on_bug_found(self):
174    return False
175
176  def run_fuzz_target(self, fuzz_target_obj):
177    """Prunes with |fuzz_target_obj| and returns the result."""
178    result = fuzz_target_obj.prune()
179    logging.debug('Corpus path contents: %s.', os.listdir(result.corpus_path))
180    self.clusterfuzz_deployment.upload_corpus(fuzz_target_obj.target_name,
181                                              result.corpus_path,
182                                              replace=True)
183    return result
184
185  def cleanup_after_fuzz_target_run(self, fuzz_target_obj):  # pylint: disable=no-self-use
186    """Cleans up after pruning with |fuzz_target_obj|."""
187    fuzz_target_obj.free_disk_if_needed()
188
189
190class CoverageTargetRunner(BaseFuzzTargetRunner):
191  """Runner that runs the 'coverage' command."""
192
193  @property
194  def quit_on_bug_found(self):
195    raise NotImplementedError('Not implemented for CoverageTargetRunner.')
196
197  def get_fuzz_targets(self):
198    """Returns fuzz targets in out directory."""
199    # We only want fuzz targets from the root because during the coverage build,
200    # a lot of the image's filesystem is copied into /out for the purpose of
201    # generating coverage reports.
202    # TOOD(metzman): Figure out if top_level_only should be the only behavior
203    # for this function.
204    return utils.get_fuzz_targets(self.workspace.out, top_level_only=True)
205
206  def run_fuzz_targets(self):
207    """Generates a coverage report. Always returns False since it never finds
208    any bugs."""
209    generate_coverage_report.generate_coverage_report(
210        self.fuzz_target_paths, self.workspace, self.clusterfuzz_deployment,
211        self.config)
212    return False
213
214  def run_fuzz_target(self, fuzz_target_obj):  # pylint: disable=no-self-use
215    """Fuzzes with |fuzz_target_obj| and returns the result."""
216    raise NotImplementedError('Not implemented for CoverageTargetRunner.')
217
218  def cleanup_after_fuzz_target_run(self, fuzz_target_obj):  # pylint: disable=no-self-use
219    """Cleans up after running |fuzz_target_obj|."""
220    raise NotImplementedError('Not implemented for CoverageTargetRunner.')
221
222
223class CiFuzzTargetRunner(BaseFuzzTargetRunner):
224  """Runner for fuzz targets used in CI (patch-fuzzing) context."""
225
226  @property
227  def quit_on_bug_found(self):
228    return True
229
230  def cleanup_after_fuzz_target_run(self, fuzz_target_obj):  # pylint: disable=no-self-use
231    """Cleans up after running |fuzz_target_obj|."""
232    fuzz_target_obj.free_disk_if_needed()
233
234  def run_fuzz_target(self, fuzz_target_obj):  # pylint: disable=no-self-use
235    return fuzz_target_obj.fuzz()
236
237
238class BatchFuzzTargetRunner(BaseFuzzTargetRunner):
239  """Runner for fuzz targets used in batch fuzzing context."""
240
241  @property
242  def quit_on_bug_found(self):
243    return False
244
245  def run_fuzz_target(self, fuzz_target_obj):
246    """Fuzzes with |fuzz_target_obj| and returns the result."""
247    result = fuzz_target_obj.fuzz()
248    logging.debug('Corpus path contents: %s.', os.listdir(result.corpus_path))
249    self.clusterfuzz_deployment.upload_corpus(fuzz_target_obj.target_name,
250                                              result.corpus_path)
251    return result
252
253  def cleanup_after_fuzz_target_run(self, fuzz_target_obj):
254    """Cleans up after running |fuzz_target_obj|."""
255    # This must be done after we upload the corpus, otherwise it will be deleted
256    # before we get a chance to upload it. We can't delete the fuzz target
257    # because it is needed when we upload the build.
258    fuzz_target_obj.free_disk_if_needed(delete_fuzz_target=False)
259
260  def run_fuzz_targets(self):
261    result = super().run_fuzz_targets()
262    self.clusterfuzz_deployment.upload_crashes()
263    return result
264
265
266_RUN_FUZZERS_MODE_RUNNER_MAPPING = {
267    'batch': BatchFuzzTargetRunner,
268    'coverage': CoverageTargetRunner,
269    'prune': PruneTargetRunner,
270    'ci': CiFuzzTargetRunner,
271}
272
273
274def get_fuzz_target_runner(config):
275  """Returns a fuzz target runner object based on the run_fuzzers_mode of
276  |config|."""
277  runner = _RUN_FUZZERS_MODE_RUNNER_MAPPING[config.run_fuzzers_mode](config)
278  logging.info('RUN_FUZZERS_MODE is: %s. Runner: %s.', config.run_fuzzers_mode,
279               runner)
280  return runner
281
282
283def run_fuzzers(config):  # pylint: disable=too-many-locals
284  """Runs fuzzers for a specific OSS-Fuzz project.
285
286  Args:
287    config: A RunFuzzTargetsConfig.
288
289  Returns:
290    A RunFuzzersResult enum value indicating what happened during fuzzing.
291  """
292  fuzz_target_runner = get_fuzz_target_runner(config)
293  if not fuzz_target_runner.initialize():
294    # We didn't fuzz at all because of internal (CIFuzz) errors. And we didn't
295    # find any bugs.
296    return RunFuzzersResult.ERROR
297
298  if not fuzz_target_runner.run_fuzz_targets():
299    # We fuzzed successfully, but didn't find any bugs (in the fuzz target).
300    return RunFuzzersResult.NO_BUG_FOUND
301
302  # We fuzzed successfully and found bug(s) in the fuzz targets.
303  return RunFuzzersResult.BUG_FOUND
304