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""" 7V8 correctness fuzzer launcher script. 8""" 9 10import argparse 11import hashlib 12import itertools 13import json 14import os 15import random 16import re 17import sys 18import traceback 19 20import v8_commands 21import v8_suppressions 22 23CONFIGS = 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>). 72ADDITIONAL_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. 81TIMEOUT = 3 82 83# Return codes. 84RETURN_PASS = 0 85RETURN_FAIL = 2 86 87BASE_PATH = os.path.dirname(os.path.abspath(__file__)) 88PREAMBLE = [ 89 os.path.join(BASE_PATH, 'v8_mock.js'), 90 os.path.join(BASE_PATH, 'v8_suppressions.js'), 91] 92ARCH_MOCKS = os.path.join(BASE_PATH, 'v8_mock_archs.js') 93 94FLAGS = ['--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 99SUPPORTED_ARCHS = ['ia32', 'x64', 'arm', 'arm64'] 100 101# Output for suppressed failure case. 102FAILURE_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. 110FAILURE_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 135FUZZ_TEST_RE = re.compile(r'.*fuzz(-\d+\.js)') 136SOURCE_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. 140ORIGINAL_SOURCE_HASH_LENGTH = 3 141 142# Placeholder string if no original source file could be determined. 143ORIGINAL_SOURCE_DEFAULT = 'none' 144 145 146def 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 155def 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 208def 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 218def 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 228def 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 241def 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 251def 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 353if __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