• 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 stack_parser
25
26# pylint: disable=wrong-import-position,import-error
27sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
28
29import utils
30
31
32class RunFuzzersResult(enum.Enum):
33  """Enum result from running fuzzers."""
34  ERROR = 0
35  BUG_FOUND = 1
36  NO_BUG_FOUND = 2
37
38
39class BaseFuzzTargetRunner:
40  """Base class for fuzzer runners."""
41
42  def __init__(self, config):
43    self.config = config
44    self.clusterfuzz_deployment = (
45        clusterfuzz_deployment.get_clusterfuzz_deployment(self.config))
46    # Set by the initialize method.
47    self.out_dir = None
48    self.fuzz_target_paths = None
49    self.artifacts_dir = None
50
51  def initialize(self):
52    """Initialization method. Must be called before calling run_fuzz_targets.
53    Returns True on success."""
54    # Use a seperate initialization function so we can return False on failure
55    # instead of exceptioning like we need to do if this were done in the
56    # __init__ method.
57
58    logging.info('Using %s sanitizer.', self.config.sanitizer)
59
60    # TODO(metzman) Add a check to ensure we aren't over time limit.
61    if not self.config.fuzz_seconds or self.config.fuzz_seconds < 1:
62      logging.error(
63          'Fuzz_seconds argument must be greater than 1, but was: %s.',
64          self.config.fuzz_seconds)
65      return False
66
67    self.out_dir = os.path.join(self.config.workspace, 'out')
68    if not os.path.exists(self.out_dir):
69      logging.error('Out directory: %s does not exist.', self.out_dir)
70      return False
71
72    self.artifacts_dir = os.path.join(self.out_dir, 'artifacts')
73    if not os.path.exists(self.artifacts_dir):
74      os.mkdir(self.artifacts_dir)
75    elif (not os.path.isdir(self.artifacts_dir) or
76          os.listdir(self.artifacts_dir)):
77      logging.error('Artifacts path: %s exists and is not an empty directory.',
78                    self.artifacts_dir)
79      return False
80
81    self.fuzz_target_paths = utils.get_fuzz_targets(self.out_dir)
82    logging.info('Fuzz targets: %s', self.fuzz_target_paths)
83    if not self.fuzz_target_paths:
84      logging.error('No fuzz targets were found in out directory: %s.',
85                    self.out_dir)
86      return False
87
88    return True
89
90  def run_fuzz_target(self, fuzz_target_obj):  # pylint: disable=no-self-use
91    """Fuzzes with |fuzz_target_obj| and returns the result."""
92    # TODO(metzman): Make children implement this so that the batch runner can
93    # do things differently.
94    result = fuzz_target_obj.fuzz()
95    fuzz_target_obj.free_disk_if_needed()
96    return result
97
98  @property
99  def quit_on_bug_found(self):
100    """Property that is checked to determine if fuzzing should quit after first
101    bug is found."""
102    raise NotImplementedError('Child class must implement method')
103
104  def get_fuzz_target_artifact(self, target, artifact_name):
105    """Returns the path of a fuzzing artifact named |artifact_name| for
106    |fuzz_target|."""
107    artifact_name = '{target_name}-{sanitizer}-{artifact_name}'.format(
108        target_name=target.target_name,
109        sanitizer=self.config.sanitizer,
110        artifact_name=artifact_name)
111    return os.path.join(self.artifacts_dir, artifact_name)
112
113  def create_fuzz_target_obj(self, target_path, run_seconds):
114    """Returns a fuzz target object."""
115    return fuzz_target.FuzzTarget(target_path, run_seconds, self.out_dir,
116                                  self.clusterfuzz_deployment, self.config)
117
118  def run_fuzz_targets(self):
119    """Runs fuzz targets. Returns True if a bug was found."""
120    fuzzers_left_to_run = len(self.fuzz_target_paths)
121
122    # Make a copy since we will mutate it.
123    fuzz_seconds = self.config.fuzz_seconds
124
125    min_seconds_per_fuzzer = fuzz_seconds // fuzzers_left_to_run
126    bug_found = False
127    for target_path in self.fuzz_target_paths:
128      # By doing this, we can ensure that every fuzz target runs for at least
129      # min_seconds_per_fuzzer, but that other fuzzers will have longer to run
130      # if one ends early.
131      run_seconds = max(fuzz_seconds // fuzzers_left_to_run,
132                        min_seconds_per_fuzzer)
133
134      target = self.create_fuzz_target_obj(target_path, run_seconds)
135      start_time = time.time()
136      result = self.run_fuzz_target(target)
137
138      # It's OK if this goes negative since we take max when determining
139      # run_seconds.
140      fuzz_seconds -= time.time() - start_time
141
142      fuzzers_left_to_run -= 1
143      if not result.testcase or not result.stacktrace:
144        logging.info('Fuzzer %s finished running without crashes.',
145                     target.target_name)
146        continue
147
148      # TODO(metzman): Do this with filestore.
149      testcase_artifact_path = self.get_fuzz_target_artifact(
150          target, os.path.basename(result.testcase))
151      shutil.move(result.testcase, testcase_artifact_path)
152      bug_summary_artifact_path = self.get_fuzz_target_artifact(
153          target, 'bug-summary.txt')
154      stack_parser.parse_fuzzer_output(result.stacktrace,
155                                       bug_summary_artifact_path)
156
157      bug_found = True
158      if self.quit_on_bug_found:
159        logging.info('Bug found. Stopping fuzzing.')
160        return bug_found
161
162    return bug_found
163
164
165class CiFuzzTargetRunner(BaseFuzzTargetRunner):
166  """Runner for fuzz targets used in CI (patch-fuzzing) context."""
167
168  @property
169  def quit_on_bug_found(self):
170    return True
171
172
173class BatchFuzzTargetRunner(BaseFuzzTargetRunner):
174  """Runner for fuzz targets used in batch fuzzing context."""
175
176  @property
177  def quit_on_bug_found(self):
178    return False
179
180
181def get_fuzz_target_runner(config):
182  """Returns a fuzz target runner object based on the run_fuzzers_mode of
183  |config|."""
184  logging.info('RUN_FUZZERS_MODE is: %s', config.run_fuzzers_mode)
185  if config.run_fuzzers_mode == 'batch':
186    return BatchFuzzTargetRunner(config)
187  return CiFuzzTargetRunner(config)
188
189
190def run_fuzzers(config):  # pylint: disable=too-many-locals
191  """Runs fuzzers for a specific OSS-Fuzz project.
192
193  Args:
194    config: A RunFuzzTargetsConfig.
195
196  Returns:
197    A RunFuzzersResult enum value indicating what happened during fuzzing.
198  """
199  fuzz_target_runner = get_fuzz_target_runner(config)
200  if not fuzz_target_runner.initialize():
201    # We didn't fuzz at all because of internal (CIFuzz) errors. And we didn't
202    # find any bugs.
203    return RunFuzzersResult.ERROR
204
205  if not fuzz_target_runner.run_fuzz_targets():
206    # We fuzzed successfully, but didn't find any bugs (in the fuzz target).
207    return RunFuzzersResult.NO_BUG_FOUND
208
209  # We fuzzed successfully and found bug(s) in the fuzz targets.
210  return RunFuzzersResult.BUG_FOUND
211