aboutsummaryrefslogtreecommitdiff
path: root/infra/cifuzz/run_fuzzers.py
blob: 513cfb6faf916895c429b2de2dd2df23ba6af1f4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module for running fuzzers."""
import enum
import logging
import os
import shutil
import sys
import time

import clusterfuzz_deployment
import fuzz_target
import stack_parser

# pylint: disable=wrong-import-position,import-error
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import utils


class RunFuzzersResult(enum.Enum):
  """Enum result from running fuzzers."""
  ERROR = 0
  BUG_FOUND = 1
  NO_BUG_FOUND = 2


class BaseFuzzTargetRunner:
  """Base class for fuzzer runners."""

  def __init__(self, config):
    self.config = config
    self.clusterfuzz_deployment = (
        clusterfuzz_deployment.get_clusterfuzz_deployment(self.config))
    # Set by the initialize method.
    self.out_dir = None
    self.fuzz_target_paths = None
    self.artifacts_dir = None

  def initialize(self):
    """Initialization method. Must be called before calling run_fuzz_targets.
    Returns True on success."""
    # Use a seperate initialization function so we can return False on failure
    # instead of exceptioning like we need to do if this were done in the
    # __init__ method.

    logging.info('Using %s sanitizer.', self.config.sanitizer)

    # TODO(metzman) Add a check to ensure we aren't over time limit.
    if not self.config.fuzz_seconds or self.config.fuzz_seconds < 1:
      logging.error(
          'Fuzz_seconds argument must be greater than 1, but was: %s.',
          self.config.fuzz_seconds)
      return False

    self.out_dir = os.path.join(self.config.workspace, 'out')
    if not os.path.exists(self.out_dir):
      logging.error('Out directory: %s does not exist.', self.out_dir)
      return False

    self.artifacts_dir = os.path.join(self.out_dir, 'artifacts')
    if not os.path.exists(self.artifacts_dir):
      os.mkdir(self.artifacts_dir)
    elif (not os.path.isdir(self.artifacts_dir) or
          os.listdir(self.artifacts_dir)):
      logging.error('Artifacts path: %s exists and is not an empty directory.',
                    self.artifacts_dir)
      return False

    self.fuzz_target_paths = utils.get_fuzz_targets(self.out_dir)
    logging.info('Fuzz targets: %s', self.fuzz_target_paths)
    if not self.fuzz_target_paths:
      logging.error('No fuzz targets were found in out directory: %s.',
                    self.out_dir)
      return False

    return True

  def run_fuzz_target(self, fuzz_target_obj):  # pylint: disable=no-self-use
    """Fuzzes with |fuzz_target_obj| and returns the result."""
    # TODO(metzman): Make children implement this so that the batch runner can
    # do things differently.
    result = fuzz_target_obj.fuzz()
    fuzz_target_obj.free_disk_if_needed()
    return result

  @property
  def quit_on_bug_found(self):
    """Property that is checked to determine if fuzzing should quit after first
    bug is found."""
    raise NotImplementedError('Child class must implement method')

  def get_fuzz_target_artifact(self, target, artifact_name):
    """Returns the path of a fuzzing artifact named |artifact_name| for
    |fuzz_target|."""
    artifact_name = '{target_name}-{sanitizer}-{artifact_name}'.format(
        target_name=target.target_name,
        sanitizer=self.config.sanitizer,
        artifact_name=artifact_name)
    return os.path.join(self.artifacts_dir, artifact_name)

  def create_fuzz_target_obj(self, target_path, run_seconds):
    """Returns a fuzz target object."""
    return fuzz_target.FuzzTarget(target_path, run_seconds, self.out_dir,
                                  self.clusterfuzz_deployment, self.config)

  def run_fuzz_targets(self):
    """Runs fuzz targets. Returns True if a bug was found."""
    fuzzers_left_to_run = len(self.fuzz_target_paths)

    # Make a copy since we will mutate it.
    fuzz_seconds = self.config.fuzz_seconds

    min_seconds_per_fuzzer = fuzz_seconds // fuzzers_left_to_run
    bug_found = False
    for target_path in self.fuzz_target_paths:
      # By doing this, we can ensure that every fuzz target runs for at least
      # min_seconds_per_fuzzer, but that other fuzzers will have longer to run
      # if one ends early.
      run_seconds = max(fuzz_seconds // fuzzers_left_to_run,
                        min_seconds_per_fuzzer)

      target = self.create_fuzz_target_obj(target_path, run_seconds)
      start_time = time.time()
      result = self.run_fuzz_target(target)

      # It's OK if this goes negative since we take max when determining
      # run_seconds.
      fuzz_seconds -= time.time() - start_time

      fuzzers_left_to_run -= 1
      if not result.testcase or not result.stacktrace:
        logging.info('Fuzzer %s finished running without crashes.',
                     target.target_name)
        continue

      # TODO(metzman): Do this with filestore.
      testcase_artifact_path = self.get_fuzz_target_artifact(
          target, os.path.basename(result.testcase))
      shutil.move(result.testcase, testcase_artifact_path)
      bug_summary_artifact_path = self.get_fuzz_target_artifact(
          target, 'bug-summary.txt')
      stack_parser.parse_fuzzer_output(result.stacktrace,
                                       bug_summary_artifact_path)

      bug_found = True
      if self.quit_on_bug_found:
        logging.info('Bug found. Stopping fuzzing.')
        return bug_found

    return bug_found


class CiFuzzTargetRunner(BaseFuzzTargetRunner):
  """Runner for fuzz targets used in CI (patch-fuzzing) context."""

  @property
  def quit_on_bug_found(self):
    return True


class BatchFuzzTargetRunner(BaseFuzzTargetRunner):
  """Runner for fuzz targets used in batch fuzzing context."""

  @property
  def quit_on_bug_found(self):
    return False


def get_fuzz_target_runner(config):
  """Returns a fuzz target runner object based on the run_fuzzers_mode of
  |config|."""
  logging.info('RUN_FUZZERS_MODE is: %s', config.run_fuzzers_mode)
  if config.run_fuzzers_mode == 'batch':
    return BatchFuzzTargetRunner(config)
  return CiFuzzTargetRunner(config)


def run_fuzzers(config):  # pylint: disable=too-many-locals
  """Runs fuzzers for a specific OSS-Fuzz project.

  Args:
    config: A RunFuzzTargetsConfig.

  Returns:
    A RunFuzzersResult enum value indicating what happened during fuzzing.
  """
  fuzz_target_runner = get_fuzz_target_runner(config)
  if not fuzz_target_runner.initialize():
    # We didn't fuzz at all because of internal (CIFuzz) errors. And we didn't
    # find any bugs.
    return RunFuzzersResult.ERROR

  if not fuzz_target_runner.run_fuzz_targets():
    # We fuzzed successfully, but didn't find any bugs (in the fuzz target).
    return RunFuzzersResult.NO_BUG_FOUND

  # We fuzzed successfully and found bug(s) in the fuzz targets.
  return RunFuzzersResult.BUG_FOUND