1# Copyright 2018 the V8 project authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5from collections import namedtuple 6import time 7 8from . import base 9 10 11# Extra flags randomly added to all fuzz tests with numfuzz. List of tuples 12# (probability, flag). 13EXTRA_FLAGS = [ 14 (0.1, '--always-opt'), 15 (0.1, '--assert-types'), 16 (0.1, '--interrupt-budget-for-feedback-allocation=0'), 17 (0.1, '--cache=code'), 18 (0.25, '--compact-maps'), 19 (0.1, '--force-slow-path'), 20 (0.2, '--future'), 21 (0.1, '--interrupt-budget=100'), 22 (0.1, '--liftoff'), 23 (0.2, '--no-analyze-environment-liveness'), 24 # TODO(machenbach): Enable when it doesn't collide with crashing on missing 25 # simd features. 26 #(0.1, '--no-enable-sse3'), 27 #(0.1, '--no-enable-ssse3'), 28 #(0.1, '--no-enable-sse4_1'), 29 (0.1, '--no-enable-sse4_2'), 30 (0.1, '--no-enable-sahf'), 31 (0.1, '--no-enable-avx'), 32 (0.1, '--no-enable-fma3'), 33 (0.1, '--no-enable-bmi1'), 34 (0.1, '--no-enable-bmi2'), 35 (0.1, '--no-enable-lzcnt'), 36 (0.1, '--no-enable-popcnt'), 37 (0.3, '--no-lazy-feedback-allocation'), 38 (0.1, '--no-liftoff'), 39 (0.1, '--no-opt'), 40 (0.2, '--no-regexp-tier-up'), 41 (0.1, '--no-wasm-tier-up'), 42 (0.1, '--regexp-interpret-all'), 43 (0.1, '--regexp-tier-up-ticks=10'), 44 (0.1, '--regexp-tier-up-ticks=100'), 45 (0.1, '--stress-background-compile'), 46 (0.1, '--stress-concurrent-inlining'), 47 (0.1, '--stress-flush-code'), 48 (0.1, '--stress-lazy-source-positions'), 49 (0.1, '--stress-wasm-code-gc'), 50 (0.1, '--turbo-instruction-scheduling'), 51 (0.1, '--turbo-stress-instruction-scheduling'), 52 (0.1, '--turbo-force-mid-tier-regalloc'), 53] 54 55def random_extra_flags(rng): 56 """Returns a random list of flags chosen from the configurations in 57 EXTRA_FLAGS. 58 """ 59 return [flag for prob, flag in EXTRA_FLAGS if rng.random() < prob] 60 61 62class FuzzerConfig(object): 63 def __init__(self, probability, analyzer, fuzzer): 64 """ 65 Args: 66 probability: of choosing this fuzzer (0; 10] 67 analyzer: instance of Analyzer class, can be None if no analysis is needed 68 fuzzer: instance of Fuzzer class 69 """ 70 assert probability > 0 and probability <= 10 71 72 self.probability = probability 73 self.analyzer = analyzer 74 self.fuzzer = fuzzer 75 76 77class Analyzer(object): 78 def get_analysis_flags(self): 79 raise NotImplementedError() 80 81 def do_analysis(self, result): 82 raise NotImplementedError() 83 84 85class Fuzzer(object): 86 def create_flags_generator(self, rng, test, analysis_value): 87 """ 88 Args: 89 rng: random number generator 90 test: test for which to create flags 91 analysis_value: value returned by the analyzer. None if there is no 92 corresponding analyzer to this fuzzer or the analysis phase is disabled 93 """ 94 raise NotImplementedError() 95 96 97# TODO(majeski): Allow multiple subtests to run at once. 98class FuzzerProc(base.TestProcProducer): 99 def __init__(self, rng, count, fuzzers, disable_analysis=False): 100 """ 101 Args: 102 rng: random number generator used to select flags and values for them 103 count: number of tests to generate based on each base test 104 fuzzers: list of FuzzerConfig instances 105 disable_analysis: disable analysis phase and filtering base on it. When 106 set, processor passes None as analysis result to fuzzers 107 """ 108 super(FuzzerProc, self).__init__('Fuzzer') 109 110 self._rng = rng 111 self._count = count 112 self._fuzzer_configs = fuzzers 113 self._disable_analysis = disable_analysis 114 self._gens = {} 115 116 def setup(self, requirement=base.DROP_RESULT): 117 # Fuzzer is optimized to not store the results 118 assert requirement == base.DROP_RESULT 119 super(FuzzerProc, self).setup(requirement) 120 121 def _next_test(self, test): 122 if self.is_stopped: 123 return False 124 125 analysis_subtest = self._create_analysis_subtest(test) 126 if analysis_subtest: 127 return self._send_test(analysis_subtest) 128 129 self._gens[test.procid] = self._create_gen(test) 130 return self._try_send_next_test(test) 131 132 def _create_analysis_subtest(self, test): 133 if self._disable_analysis: 134 return None 135 136 analysis_flags = [] 137 for fuzzer_config in self._fuzzer_configs: 138 if fuzzer_config.analyzer: 139 analysis_flags += fuzzer_config.analyzer.get_analysis_flags() 140 141 if analysis_flags: 142 analysis_flags = list(set(analysis_flags)) 143 return self._create_subtest(test, 'analysis', flags=analysis_flags, 144 keep_output=True) 145 146 def _result_for(self, test, subtest, result): 147 if not self._disable_analysis: 148 if result is not None: 149 # Analysis phase, for fuzzing we drop the result. 150 if result.has_unexpected_output: 151 self._send_result(test, None) 152 return 153 154 self._gens[test.procid] = self._create_gen(test, result) 155 156 self._try_send_next_test(test) 157 158 def _create_gen(self, test, analysis_result=None): 159 # It will be called with analysis_result==None only when there is no 160 # analysis phase at all, so no fuzzer has it's own analyzer. 161 gens = [] 162 indexes = [] 163 for fuzzer_config in self._fuzzer_configs: 164 analysis_value = None 165 if analysis_result and fuzzer_config.analyzer: 166 analysis_value = fuzzer_config.analyzer.do_analysis(analysis_result) 167 if not analysis_value: 168 # Skip fuzzer for this test since it doesn't have analysis data 169 continue 170 p = fuzzer_config.probability 171 flag_gen = fuzzer_config.fuzzer.create_flags_generator(self._rng, test, 172 analysis_value) 173 indexes += [len(gens)] * p 174 gens.append((p, flag_gen)) 175 176 if not gens: 177 # No fuzzers for this test, skip it 178 return 179 180 i = 0 181 while not self._count or i < self._count: 182 main_index = self._rng.choice(indexes) 183 _, main_gen = gens[main_index] 184 185 flags = random_extra_flags(self._rng) + next(main_gen) 186 for index, (p, gen) in enumerate(gens): 187 if index == main_index: 188 continue 189 if self._rng.randint(1, 10) <= p: 190 flags += next(gen) 191 192 flags.append('--fuzzer-random-seed=%s' % self._next_seed()) 193 yield self._create_subtest(test, str(i), flags=flags) 194 195 i += 1 196 197 def _try_send_next_test(self, test): 198 if not self.is_stopped: 199 for subtest in self._gens[test.procid]: 200 if self._send_test(subtest): 201 return True 202 203 del self._gens[test.procid] 204 return False 205 206 def _next_seed(self): 207 seed = None 208 while not seed: 209 seed = self._rng.randint(-2147483648, 2147483647) 210 return seed 211 212 213class ScavengeAnalyzer(Analyzer): 214 def get_analysis_flags(self): 215 return ['--fuzzer-gc-analysis'] 216 217 def do_analysis(self, result): 218 for line in reversed(result.output.stdout.splitlines()): 219 if line.startswith('### Maximum new space size reached = '): 220 return int(float(line.split()[7])) 221 222 223class ScavengeFuzzer(Fuzzer): 224 def create_flags_generator(self, rng, test, analysis_value): 225 while True: 226 yield ['--stress-scavenge=%d' % (analysis_value or 100)] 227 228 229class MarkingAnalyzer(Analyzer): 230 def get_analysis_flags(self): 231 return ['--fuzzer-gc-analysis'] 232 233 def do_analysis(self, result): 234 for line in reversed(result.output.stdout.splitlines()): 235 if line.startswith('### Maximum marking limit reached = '): 236 return int(float(line.split()[6])) 237 238 239class MarkingFuzzer(Fuzzer): 240 def create_flags_generator(self, rng, test, analysis_value): 241 while True: 242 yield ['--stress-marking=%d' % (analysis_value or 100)] 243 244 245class GcIntervalAnalyzer(Analyzer): 246 def get_analysis_flags(self): 247 return ['--fuzzer-gc-analysis'] 248 249 def do_analysis(self, result): 250 for line in reversed(result.output.stdout.splitlines()): 251 if line.startswith('### Allocations = '): 252 return int(float(line.split()[3][:-1])) 253 254 255class GcIntervalFuzzer(Fuzzer): 256 def create_flags_generator(self, rng, test, analysis_value): 257 if analysis_value: 258 value = analysis_value // 10 259 else: 260 value = 10000 261 while True: 262 yield ['--random-gc-interval=%d' % value] 263 264 265class CompactionFuzzer(Fuzzer): 266 def create_flags_generator(self, rng, test, analysis_value): 267 while True: 268 yield ['--stress-compaction-random'] 269 270 271class InterruptBudgetFuzzer(Fuzzer): 272 def create_flags_generator(self, rng, test, analysis_value): 273 while True: 274 # Half with half without lazy feedback allocation. The first flag 275 # overwrites potential flag negations from the extra flags list. 276 flag1 = rng.choice( 277 '--lazy-feedback-allocation', '--no-lazy-feedback-allocation') 278 # For most code paths, only one of the flags below has a meaning 279 # based on the flag above. 280 flag2 = '--interrupt-budget=%d' % rng.randint(0, 135168) 281 flag3 = '--interrupt-budget-for-feedback-allocation=%d' % rng.randint( 282 0, 940) 283 284 yield [flag1, flag2, flag3] 285 286 287class StackSizeFuzzer(Fuzzer): 288 def create_flags_generator(self, rng, test, analysis_value): 289 while True: 290 yield ['--stack-size=%d' % rng.randint(54, 983)] 291 292 293class TaskDelayFuzzer(Fuzzer): 294 def create_flags_generator(self, rng, test, analysis_value): 295 while True: 296 yield ['--stress-delay-tasks'] 297 298 299class ThreadPoolSizeFuzzer(Fuzzer): 300 def create_flags_generator(self, rng, test, analysis_value): 301 while True: 302 yield ['--thread-pool-size=%d' % rng.randint(1, 8)] 303 304 305class DeoptAnalyzer(Analyzer): 306 MAX_DEOPT=1000000000 307 308 def __init__(self, min_interval): 309 super(DeoptAnalyzer, self).__init__() 310 self._min = min_interval 311 312 def get_analysis_flags(self): 313 return ['--deopt-every-n-times=%d' % self.MAX_DEOPT, 314 '--print-deopt-stress'] 315 316 def do_analysis(self, result): 317 for line in reversed(result.output.stdout.splitlines()): 318 if line.startswith('=== Stress deopt counter: '): 319 counter = self.MAX_DEOPT - int(line.split(' ')[-1]) 320 if counter < self._min: 321 # Skip this test since we won't generate any meaningful interval with 322 # given minimum. 323 return None 324 return counter 325 326 327class DeoptFuzzer(Fuzzer): 328 def __init__(self, min_interval): 329 super(DeoptFuzzer, self).__init__() 330 self._min = min_interval 331 332 def create_flags_generator(self, rng, test, analysis_value): 333 while True: 334 if analysis_value: 335 value = analysis_value // 2 336 else: 337 value = 10000 338 interval = rng.randint(self._min, max(value, self._min)) 339 yield ['--deopt-every-n-times=%d' % interval] 340 341 342FUZZERS = { 343 'compaction': (None, CompactionFuzzer), 344 'delay': (None, TaskDelayFuzzer), 345 'deopt': (DeoptAnalyzer, DeoptFuzzer), 346 'gc_interval': (GcIntervalAnalyzer, GcIntervalFuzzer), 347 'interrupt': InterruptBudgetFuzzer, 348 'marking': (MarkingAnalyzer, MarkingFuzzer), 349 'scavenge': (ScavengeAnalyzer, ScavengeFuzzer), 350 'stack': (None, StackSizeFuzzer), 351 'threads': (None, ThreadPoolSizeFuzzer), 352} 353 354 355def create_fuzzer_config(name, probability, *args, **kwargs): 356 analyzer_class, fuzzer_class = FUZZERS[name] 357 return FuzzerConfig( 358 probability, 359 analyzer_class(*args, **kwargs) if analyzer_class else None, 360 fuzzer_class(*args, **kwargs), 361 ) 362