1 #!/usr/bin/env python 2 # Copyright 2016 the V8 project authors. All rights reserved. 3 # Use of this source code is governed by a BSD-style license that can be 4 # found in the LICENSE file. 5 6 """ 7 V8 correctness fuzzer launcher script. 8 """ 9 10 import argparse 11 import hashlib 12 import itertools 13 import json 14 import os 15 import random 16 import re 17 import sys 18 import traceback 19 20 import v8_commands 21 import v8_suppressions 22 23 CONFIGS = dict( 24 default=[], 25 ignition=[ 26 '--turbo-filter=~', 27 '--noopt', 28 '--liftoff', 29 '--no-wasm-tier-up', 30 ], 31 ignition_asm=[ 32 '--turbo-filter=~', 33 '--noopt', 34 '--validate-asm', 35 '--stress-validate-asm', 36 ], 37 ignition_eager=[ 38 '--turbo-filter=~', 39 '--noopt', 40 '--no-lazy', 41 '--no-lazy-inner-functions', 42 ], 43 ignition_turbo=[], 44 ignition_turbo_opt=[ 45 '--always-opt', 46 '--no-liftoff', 47 '--no-wasm-tier-up', 48 ], 49 ignition_turbo_opt_eager=[ 50 '--always-opt', 51 '--no-lazy', 52 '--no-lazy-inner-functions', 53 ], 54 slow_path=[ 55 '--force-slow-path', 56 ], 57 slow_path_opt=[ 58 '--always-opt', 59 '--force-slow-path', 60 ], 61 trusted=[ 62 '--no-untrusted-code-mitigations', 63 ], 64 trusted_opt=[ 65 '--always-opt', 66 '--no-untrusted-code-mitigations', 67 ], 68 ) 69 70 # Additional flag experiments. List of tuples like 71 # (<likelihood to use flags in [0,1)>, <flag>). 72 ADDITIONAL_FLAGS = [ 73 (0.1, '--stress-marking=100'), 74 (0.1, '--stress-scavenge=100'), 75 (0.1, '--stress-compaction-random'), 76 (0.1, '--random-gc-interval=2000'), 77 (0.2, '--noanalyze-environment-liveness'), 78 ] 79 80 # Timeout in seconds for one d8 run. 81 TIMEOUT = 3 82 83 # Return codes. 84 RETURN_PASS = 0 85 RETURN_FAIL = 2 86 87 BASE_PATH = os.path.dirname(os.path.abspath(__file__)) 88 PREAMBLE = [ 89 os.path.join(BASE_PATH, 'v8_mock.js'), 90 os.path.join(BASE_PATH, 'v8_suppressions.js'), 91 ] 92 ARCH_MOCKS = os.path.join(BASE_PATH, 'v8_mock_archs.js') 93 94 FLAGS = ['--abort_on_stack_or_string_length_overflow', '--expose-gc', 95 '--allow-natives-syntax', '--invoke-weak-callbacks', '--omit-quit', 96 '--es-staging', '--wasm-num-compilation-tasks=0', 97 '--suppress-asm-messages'] 98 99 SUPPORTED_ARCHS = ['ia32', 'x64', 'arm', 'arm64'] 100 101 # Output for suppressed failure case. 102 FAILURE_HEADER_TEMPLATE = """# 103 # V8 correctness failure 104 # V8 correctness configs: %(configs)s 105 # V8 correctness sources: %(source_key)s 106 # V8 correctness suppression: %(suppression)s 107 """ 108 109 # Extended output for failure case. The 'CHECK' is for the minimizer. 110 FAILURE_TEMPLATE = FAILURE_HEADER_TEMPLATE + """# 111 # CHECK 112 # 113 # Compared %(first_config_label)s with %(second_config_label)s 114 # 115 # Flags of %(first_config_label)s: 116 %(first_config_flags)s 117 # Flags of %(second_config_label)s: 118 %(second_config_flags)s 119 # 120 # Difference: 121 %(difference)s 122 # 123 # Source file: 124 %(source)s 125 # 126 ### Start of configuration %(first_config_label)s: 127 %(first_config_output)s 128 ### End of configuration %(first_config_label)s 129 # 130 ### Start of configuration %(second_config_label)s: 131 %(second_config_output)s 132 ### End of configuration %(second_config_label)s 133 """ 134 135 FUZZ_TEST_RE = re.compile(r'.*fuzz(-\d+\.js)') 136 SOURCE_RE = re.compile(r'print\("v8-foozzie source: (.*)"\);') 137 138 # The number of hex digits used from the hash of the original source file path. 139 # Keep the number small to avoid duplicate explosion. 140 ORIGINAL_SOURCE_HASH_LENGTH = 3 141 142 # Placeholder string if no original source file could be determined. 143 ORIGINAL_SOURCE_DEFAULT = 'none' 144 145 146 def infer_arch(d8): 147 """Infer the V8 architecture from the build configuration next to the 148 executable. 149 """ 150 with open(os.path.join(os.path.dirname(d8), 'v8_build_config.json')) as f: 151 arch = json.load(f)['v8_current_cpu'] 152 return 'ia32' if arch == 'x86' else arch 153 154 155 def parse_args(): 156 parser = argparse.ArgumentParser() 157 parser.add_argument( 158 '--random-seed', type=int, required=True, 159 help='random seed passed to both runs') 160 parser.add_argument( 161 '--first-config', help='first configuration', default='ignition') 162 parser.add_argument( 163 '--second-config', help='second configuration', default='ignition_turbo') 164 parser.add_argument( 165 '--first-d8', default='d8', 166 help='optional path to first d8 executable, ' 167 'default: bundled in the same directory as this script') 168 parser.add_argument( 169 '--second-d8', 170 help='optional path to second d8 executable, default: same as first') 171 parser.add_argument('testcase', help='path to test case') 172 options = parser.parse_args() 173 174 # Ensure we have a test case. 175 assert (os.path.exists(options.testcase) and 176 os.path.isfile(options.testcase)), ( 177 'Test case %s doesn\'t exist' % options.testcase) 178 179 # Use first d8 as default for second d8. 180 options.second_d8 = options.second_d8 or options.first_d8 181 182 # Ensure absolute paths. 183 if not os.path.isabs(options.first_d8): 184 options.first_d8 = os.path.join(BASE_PATH, options.first_d8) 185 if not os.path.isabs(options.second_d8): 186 options.second_d8 = os.path.join(BASE_PATH, options.second_d8) 187 188 # Ensure executables exist. 189 assert os.path.exists(options.first_d8) 190 assert os.path.exists(options.second_d8) 191 192 # Infer architecture from build artifacts. 193 options.first_arch = infer_arch(options.first_d8) 194 options.second_arch = infer_arch(options.second_d8) 195 196 # Ensure we make a sane comparison. 197 if (options.first_arch == options.second_arch and 198 options.first_config == options.second_config): 199 parser.error('Need either arch or config difference.') 200 assert options.first_arch in SUPPORTED_ARCHS 201 assert options.second_arch in SUPPORTED_ARCHS 202 assert options.first_config in CONFIGS 203 assert options.second_config in CONFIGS 204 205 return options 206 207 208 def get_meta_data(content): 209 """Extracts original-source-file paths from test case content.""" 210 sources = [] 211 for line in content.splitlines(): 212 match = SOURCE_RE.match(line) 213 if match: 214 sources.append(match.group(1)) 215 return {'sources': sources} 216 217 218 def content_bailout(content, ignore_fun): 219 """Print failure state and return if ignore_fun matches content.""" 220 bug = (ignore_fun(content) or '').strip() 221 if bug: 222 print FAILURE_HEADER_TEMPLATE % dict( 223 configs='', source_key='', suppression=bug) 224 return True 225 return False 226 227 228 def pass_bailout(output, step_number): 229 """Print info and return if in timeout or crash pass states.""" 230 if output.HasTimedOut(): 231 # Dashed output, so that no other clusterfuzz tools can match the 232 # words timeout or crash. 233 print '# V8 correctness - T-I-M-E-O-U-T %d' % step_number 234 return True 235 if output.HasCrashed(): 236 print '# V8 correctness - C-R-A-S-H %d' % step_number 237 return True 238 return False 239 240 241 def fail_bailout(output, ignore_by_output_fun): 242 """Print failure state and return if ignore_by_output_fun matches output.""" 243 bug = (ignore_by_output_fun(output.stdout) or '').strip() 244 if bug: 245 print FAILURE_HEADER_TEMPLATE % dict( 246 configs='', source_key='', suppression=bug) 247 return True 248 return False 249 250 251 def main(): 252 options = parse_args() 253 rng = random.Random(options.random_seed) 254 255 # Suppressions are architecture and configuration specific. 256 suppress = v8_suppressions.get_suppression( 257 options.first_arch, options.first_config, 258 options.second_arch, options.second_config, 259 ) 260 261 # Static bailout based on test case content or metadata. 262 with open(options.testcase) as f: 263 content = f.read() 264 if content_bailout(get_meta_data(content), suppress.ignore_by_metadata): 265 return RETURN_FAIL 266 if content_bailout(content, suppress.ignore_by_content): 267 return RETURN_FAIL 268 269 # Set up runtime arguments. 270 common_flags = FLAGS + ['--random-seed', str(options.random_seed)] 271 first_config_flags = common_flags + CONFIGS[options.first_config] 272 second_config_flags = common_flags + CONFIGS[options.second_config] 273 274 # Add additional flags to second config based on experiment percentages. 275 for p, flag in ADDITIONAL_FLAGS: 276 if rng.random() < p: 277 second_config_flags.append(flag) 278 279 def run_d8(d8, config_flags): 280 preamble = PREAMBLE[:] 281 if options.first_arch != options.second_arch: 282 preamble.append(ARCH_MOCKS) 283 args = [d8] + config_flags + preamble + [options.testcase] 284 print " ".join(args) 285 if d8.endswith('.py'): 286 # Wrap with python in tests. 287 args = [sys.executable] + args 288 return v8_commands.Execute( 289 args, 290 cwd=os.path.dirname(os.path.abspath(options.testcase)), 291 timeout=TIMEOUT, 292 ) 293 294 first_config_output = run_d8(options.first_d8, first_config_flags) 295 296 # Early bailout based on first run's output. 297 if pass_bailout(first_config_output, 1): 298 return RETURN_PASS 299 300 second_config_output = run_d8(options.second_d8, second_config_flags) 301 302 # Bailout based on second run's output. 303 if pass_bailout(second_config_output, 2): 304 return RETURN_PASS 305 306 difference, source = suppress.diff( 307 first_config_output.stdout, second_config_output.stdout) 308 309 if source: 310 source_key = hashlib.sha1(source).hexdigest()[:ORIGINAL_SOURCE_HASH_LENGTH] 311 else: 312 source = ORIGINAL_SOURCE_DEFAULT 313 source_key = ORIGINAL_SOURCE_DEFAULT 314 315 if difference: 316 # Only bail out due to suppressed output if there was a difference. If a 317 # suppression doesn't show up anymore in the statistics, we might want to 318 # remove it. 319 if fail_bailout(first_config_output, suppress.ignore_by_output1): 320 return RETURN_FAIL 321 if fail_bailout(second_config_output, suppress.ignore_by_output2): 322 return RETURN_FAIL 323 324 # The first three entries will be parsed by clusterfuzz. Format changes 325 # will require changes on the clusterfuzz side. 326 first_config_label = '%s,%s' % (options.first_arch, options.first_config) 327 second_config_label = '%s,%s' % (options.second_arch, options.second_config) 328 print (FAILURE_TEMPLATE % dict( 329 configs='%s:%s' % (first_config_label, second_config_label), 330 source_key=source_key, 331 suppression='', # We can't tie bugs to differences. 332 first_config_label=first_config_label, 333 second_config_label=second_config_label, 334 first_config_flags=' '.join(first_config_flags), 335 second_config_flags=' '.join(second_config_flags), 336 first_config_output= 337 first_config_output.stdout.decode('utf-8', 'replace'), 338 second_config_output= 339 second_config_output.stdout.decode('utf-8', 'replace'), 340 source=source, 341 difference=difference.decode('utf-8', 'replace'), 342 )).encode('utf-8', 'replace') 343 return RETURN_FAIL 344 345 # TODO(machenbach): Figure out if we could also return a bug in case there's 346 # no difference, but one of the line suppressions has matched - and without 347 # the match there would be a difference. 348 349 print '# V8 correctness - pass' 350 return RETURN_PASS 351 352 353 if __name__ == "__main__": 354 try: 355 result = main() 356 except SystemExit: 357 # Make sure clusterfuzz reports internal errors and wrong usage. 358 # Use one label for all internal and usage errors. 359 print FAILURE_HEADER_TEMPLATE % dict( 360 configs='', source_key='', suppression='wrong_usage') 361 result = RETURN_FAIL 362 except MemoryError: 363 # Running out of memory happens occasionally but is not actionable. 364 print '# V8 correctness - pass' 365 result = RETURN_PASS 366 except Exception as e: 367 print FAILURE_HEADER_TEMPLATE % dict( 368 configs='', source_key='', suppression='internal_error') 369 print '# Internal error: %s' % e 370 traceback.print_exc(file=sys.stdout) 371 result = RETURN_FAIL 372 373 sys.exit(result) 374