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