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