#!/usr/bin/env python # Copyright 2014 the V8 project authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import itertools import js2c import multiprocessing import optparse import os import random import re import shutil import signal import string import subprocess import sys import time FILENAME = "src/runtime.cc" HEADERFILENAME = "src/runtime.h" FUNCTION = re.compile("^RUNTIME_FUNCTION\(Runtime_(\w+)") ARGSLENGTH = re.compile(".*ASSERT\(.*args\.length\(\) == (\d+)\);") FUNCTIONEND = "}\n" MACRO = re.compile(r"^#define ([^ ]+)\(([^)]*)\) *([^\\]*)\\?\n$") FIRST_WORD = re.compile("^\s*(.*?)[\s({\[]") WORKSPACE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..")) BASEPATH = os.path.join(WORKSPACE, "test", "mjsunit", "runtime-gen") THIS_SCRIPT = os.path.relpath(sys.argv[0]) # Expand these macros, they define further runtime functions. EXPAND_MACROS = [ "BUFFER_VIEW_GETTER", "DATA_VIEW_GETTER", "DATA_VIEW_SETTER", "RUNTIME_UNARY_MATH", ] # TODO(jkummerow): We could also whitelist the following macros, but the # functions they define are so trivial that it's unclear how much benefit # that would provide: # ELEMENTS_KIND_CHECK_RUNTIME_FUNCTION # FIXED_TYPED_ARRAYS_CHECK_RUNTIME_FUNCTION # TYPED_ARRAYS_CHECK_RUNTIME_FUNCTION # Counts of functions in each detection state. These are used to assert # that the parser doesn't bit-rot. Change the values as needed when you add, # remove or change runtime functions, but make sure we don't lose our ability # to parse them! EXPECTED_FUNCTION_COUNT = 419 EXPECTED_FUZZABLE_COUNT = 334 EXPECTED_CCTEST_COUNT = 8 EXPECTED_UNKNOWN_COUNT = 4 EXPECTED_BUILTINS_COUNT = 812 # Don't call these at all. BLACKLISTED = [ "Abort", # Kills the process. "AbortJS", # Kills the process. "CompileForOnStackReplacement", # Riddled with ASSERTs. "IS_VAR", # Not implemented in the runtime. "ListNatives", # Not available in Release mode. "SetAllocationTimeout", # Too slow for fuzzing. "SystemBreak", # Kills (int3) the process. # These are weird. They violate some invariants when called after # bootstrapping. "DisableAccessChecks", "EnableAccessChecks", # The current LiveEdit implementation relies on and messes with internals # in ways that makes it fundamentally unfuzzable :-( "DebugGetLoadedScripts", "DebugSetScriptSource", "LiveEditFindSharedFunctionInfosForScript", "LiveEditFunctionSourceUpdated", "LiveEditGatherCompileInfo", "LiveEditPatchFunctionPositions", "LiveEditReplaceFunctionCode", "LiveEditReplaceRefToNestedFunction", "LiveEditReplaceScript", "LiveEditRestartFrame", "SetScriptBreakPoint", # TODO(jkummerow): Fix these and un-blacklist them! "CreateDateTimeFormat", "CreateNumberFormat", # TODO(danno): Fix these internal function that are only callable form stubs # and un-blacklist them! "NumberToString", "RxegExpConstructResult", "RegExpExec", "StringAdd", "SubString", "StringCompare", "StringCharCodeAt", "GetFromCache", # Compilation "CompileUnoptimized", "CompileOptimized", "TryInstallOptimizedCode", "NotifyDeoptimized", "NotifyStubFailure", # Utilities "AllocateInNewSpace", "AllocateInTargetSpace", "AllocateHeapNumber", "NumberToSmi", "NumberToStringSkipCache", "NewSloppyArguments", "NewStrictArguments", # Harmony "CreateJSGeneratorObject", "SuspendJSGeneratorObject", "ResumeJSGeneratorObject", "ThrowGeneratorStateError", # Arrays "ArrayConstructor", "InternalArrayConstructor", # Literals "MaterializeRegExpLiteral", "CreateObjectLiteral", "CreateArrayLiteral", "CreateArrayLiteralStubBailout", # Statements "NewClosure", "NewClosureFromStubFailure", "NewObject", "NewObjectWithAllocationSite", "FinalizeInstanceSize", "Throw", "ReThrow", "ThrowReferenceError", "ThrowNotDateError", "StackGuard", "Interrupt", "PromoteScheduledException", # Contexts "NewGlobalContext", "NewFunctionContext", "PushWithContext", "PushCatchContext", "PushBlockContext", "PushModuleContext", "DeleteLookupSlot", "LoadLookupSlot", "LoadLookupSlotNoReferenceError", "StoreLookupSlot", # Declarations "DeclareGlobals", "DeclareModules", "DeclareContextSlot", "InitializeConstGlobal", "InitializeConstContextSlot", # Eval "ResolvePossiblyDirectEval", # Maths "MathPowSlow", "MathPowRT" ] # These will always throw. THROWS = [ "CheckExecutionState", # Needs to hit a break point. "CheckIsBootstrapping", # Needs to be bootstrapping. "DebugEvaluate", # Needs to hit a break point. "DebugEvaluateGlobal", # Needs to hit a break point. "DebugIndexedInterceptorElementValue", # Needs an indexed interceptor. "DebugNamedInterceptorPropertyValue", # Needs a named interceptor. "DebugSetScriptSource", # Checks compilation state of script. "GetAllScopesDetails", # Needs to hit a break point. "GetFrameCount", # Needs to hit a break point. "GetFrameDetails", # Needs to hit a break point. "GetRootNaN", # Needs to be bootstrapping. "GetScopeCount", # Needs to hit a break point. "GetScopeDetails", # Needs to hit a break point. "GetStepInPositions", # Needs to hit a break point. "GetTemplateField", # Needs a {Function,Object}TemplateInfo. "GetThreadCount", # Needs to hit a break point. "GetThreadDetails", # Needs to hit a break point. "IsAccessAllowedForObserver", # Needs access-check-required object. "UnblockConcurrentRecompilation" # Needs --block-concurrent-recompilation. ] # Definitions used in CUSTOM_KNOWN_GOOD_INPUT below. _BREAK_ITERATOR = ( "%GetImplFromInitializedIntlObject(new Intl.v8BreakIterator())") _COLLATOR = "%GetImplFromInitializedIntlObject(new Intl.Collator('en-US'))" _DATETIME_FORMAT = ( "%GetImplFromInitializedIntlObject(new Intl.DateTimeFormat('en-US'))") _NUMBER_FORMAT = ( "%GetImplFromInitializedIntlObject(new Intl.NumberFormat('en-US'))") # Custom definitions for function input that does not throw. # Format: "FunctionName": ["arg0", "arg1", ..., argslength]. # None means "fall back to autodetected value". CUSTOM_KNOWN_GOOD_INPUT = { "AddNamedProperty": [None, "\"bla\"", None, None, None], "AddPropertyForTemplate": [None, 10, None, None, None], "Apply": ["function() {}", None, None, None, None, None], "ArrayBufferSliceImpl": [None, None, 0, None], "ArrayConcat": ["[1, 'a']", None], "BreakIteratorAdoptText": [_BREAK_ITERATOR, None, None], "BreakIteratorBreakType": [_BREAK_ITERATOR, None], "BreakIteratorCurrent": [_BREAK_ITERATOR, None], "BreakIteratorFirst": [_BREAK_ITERATOR, None], "BreakIteratorNext": [_BREAK_ITERATOR, None], "CompileString": [None, "false", None], "CreateBreakIterator": ["'en-US'", "{type: 'string'}", None, None], "CreateJSFunctionProxy": [None, "function() {}", None, None, None], "CreatePrivateSymbol": ["\"foo\"", None], "CreateSymbol": ["\"foo\"", None], "DateParseString": [None, "new Array(8)", None], "DefineAccessorPropertyUnchecked": [None, None, "function() {}", "function() {}", 2, None], "FunctionBindArguments": [None, None, "undefined", None, None], "GetBreakLocations": [None, 0, None], "GetDefaultReceiver": ["function() {}", None], "GetImplFromInitializedIntlObject": ["new Intl.NumberFormat('en-US')", None], "InternalCompare": [_COLLATOR, None, None, None], "InternalDateFormat": [_DATETIME_FORMAT, None, None], "InternalDateParse": [_DATETIME_FORMAT, None, None], "InternalNumberFormat": [_NUMBER_FORMAT, None, None], "InternalNumberParse": [_NUMBER_FORMAT, None, None], "IsSloppyModeFunction": ["function() {}", None], "LoadMutableDouble": ["{foo: 1.2}", None, None], "NewObjectFromBound": ["(function() {}).bind({})", None], "NumberToRadixString": [None, "2", None], "ParseJson": ["\"{}\"", 1], "RegExpExecMultiple": [None, None, "['a']", "['a']", None], "DefineAccessorProperty": [None, None, "undefined", "undefined", None, None], "SetIteratorInitialize": [None, None, "2", None], "SetDebugEventListener": ["undefined", None, None], "SetFunctionBreakPoint": [None, 200, None, None], "StringBuilderConcat": ["[1, 2, 3]", 3, None, None], "StringBuilderJoin": ["['a', 'b']", 4, None, None], "StringMatch": [None, None, "['a', 'b']", None], "StringNormalize": [None, 2, None], "StringReplaceGlobalRegExpWithString": [None, None, None, "['a']", None], "TypedArrayInitialize": [None, 6, "new ArrayBuffer(8)", None, 4, None], "TypedArrayInitializeFromArrayLike": [None, 6, None, None, None], "TypedArraySetFastCases": [None, None, "0", None], "FunctionIsArrow": ["() => null", None], } # Types of arguments that cannot be generated in a JavaScript testcase. NON_JS_TYPES = [ "Code", "Context", "FixedArray", "FunctionTemplateInfo", "JSFunctionResultCache", "JSMessageObject", "Map", "ScopeInfo", "SharedFunctionInfo"] class Generator(object): def RandomVariable(self, varname, vartype, simple): if simple: return self._Variable(varname, self.GENERATORS[vartype][0]) return self.GENERATORS[vartype][1](self, varname, self.DEFAULT_RECURSION_BUDGET) @staticmethod def IsTypeSupported(typename): return typename in Generator.GENERATORS USUAL_SUSPECT_PROPERTIES = ["size", "length", "byteLength", "__proto__", "prototype", "0", "1", "-1"] DEFAULT_RECURSION_BUDGET = 2 PROXY_TRAPS = """{ getOwnPropertyDescriptor: function(name) { return {value: function() {}, configurable: true, writable: true, enumerable: true}; }, getPropertyDescriptor: function(name) { return {value: function() {}, configurable: true, writable: true, enumerable: true}; }, getOwnPropertyNames: function() { return []; }, getPropertyNames: function() { return []; }, defineProperty: function(name, descriptor) {}, delete: function(name) { return true; }, fix: function() {} }""" def _Variable(self, name, value, fallback=None): args = { "name": name, "value": value, "fallback": fallback } if fallback: wrapper = "try { %%s } catch(e) { var %(name)s = %(fallback)s; }" % args else: wrapper = "%s" return [wrapper % ("var %(name)s = %(value)s;" % args)] def _Boolean(self, name, recursion_budget): return self._Variable(name, random.choice(["true", "false"])) def _Oddball(self, name, recursion_budget): return self._Variable(name, random.choice(["true", "false", "undefined", "null"])) def _StrictMode(self, name, recursion_budget): return self._Variable(name, random.choice([0, 1])) def _Int32(self, name, recursion_budget=0): die = random.random() if die < 0.5: value = random.choice([-3, -1, 0, 1, 2, 10, 515, 0x3fffffff, 0x7fffffff, 0x40000000, -0x40000000, -0x80000000]) elif die < 0.75: value = random.randint(-1000, 1000) else: value = random.randint(-0x80000000, 0x7fffffff) return self._Variable(name, value) def _Uint32(self, name, recursion_budget=0): die = random.random() if die < 0.5: value = random.choice([0, 1, 2, 3, 4, 8, 0x3fffffff, 0x40000000, 0x7fffffff, 0xffffffff]) elif die < 0.75: value = random.randint(0, 1000) else: value = random.randint(0, 0xffffffff) return self._Variable(name, value) def _Smi(self, name, recursion_budget): die = random.random() if die < 0.5: value = random.choice([-5, -1, 0, 1, 2, 3, 0x3fffffff, -0x40000000]) elif die < 0.75: value = random.randint(-1000, 1000) else: value = random.randint(-0x40000000, 0x3fffffff) return self._Variable(name, value) def _Number(self, name, recursion_budget): die = random.random() if die < 0.5: return self._Smi(name, recursion_budget) elif die < 0.6: value = random.choice(["Infinity", "-Infinity", "NaN", "-0", "1.7976931348623157e+308", # Max value. "2.2250738585072014e-308", # Min value. "4.9406564584124654e-324"]) # Min subnormal. else: value = random.lognormvariate(0, 15) return self._Variable(name, value) def _RawRandomString(self, minlength=0, maxlength=100, alphabet=string.ascii_letters): length = random.randint(minlength, maxlength) result = "" for i in xrange(length): result += random.choice(alphabet) return result def _SeqString(self, name, recursion_budget): s1 = self._RawRandomString(1, 5) s2 = self._RawRandomString(1, 5) # 'foo' + 'bar' return self._Variable(name, "\"%s\" + \"%s\"" % (s1, s2)) def _SeqTwoByteString(self, name): s1 = self._RawRandomString(1, 5) s2 = self._RawRandomString(1, 5) # 'foo' + unicode + 'bar' return self._Variable(name, "\"%s\" + \"\\2082\" + \"%s\"" % (s1, s2)) def _SlicedString(self, name): s = self._RawRandomString(20, 30) # 'ffoo12345678901234567890'.substr(1) return self._Variable(name, "\"%s\".substr(1)" % s) def _ConsString(self, name): s1 = self._RawRandomString(8, 15) s2 = self._RawRandomString(8, 15) # 'foo12345' + (function() { return 'bar12345';})() return self._Variable(name, "\"%s\" + (function() { return \"%s\";})()" % (s1, s2)) def _InternalizedString(self, name): return self._Variable(name, "\"%s\"" % self._RawRandomString(0, 20)) def _String(self, name, recursion_budget): die = random.random() if die < 0.5: string = random.choice(self.USUAL_SUSPECT_PROPERTIES) return self._Variable(name, "\"%s\"" % string) elif die < 0.6: number_name = name + "_number" result = self._Number(number_name, recursion_budget) return result + self._Variable(name, "\"\" + %s" % number_name) elif die < 0.7: return self._SeqString(name, recursion_budget) elif die < 0.8: return self._ConsString(name) elif die < 0.9: return self._InternalizedString(name) else: return self._SlicedString(name) def _Symbol(self, name, recursion_budget): raw_string_name = name + "_1" result = self._String(raw_string_name, recursion_budget) return result + self._Variable(name, "Symbol(%s)" % raw_string_name) def _Name(self, name, recursion_budget): if random.random() < 0.2: return self._Symbol(name, recursion_budget) return self._String(name, recursion_budget) def _JSValue(self, name, recursion_budget): die = random.random() raw_name = name + "_1" if die < 0.33: result = self._String(raw_name, recursion_budget) return result + self._Variable(name, "new String(%s)" % raw_name) elif die < 0.66: result = self._Boolean(raw_name, recursion_budget) return result + self._Variable(name, "new Boolean(%s)" % raw_name) else: result = self._Number(raw_name, recursion_budget) return result + self._Variable(name, "new Number(%s)" % raw_name) def _RawRandomPropertyName(self): if random.random() < 0.5: return random.choice(self.USUAL_SUSPECT_PROPERTIES) return self._RawRandomString(0, 10) def _AddProperties(self, name, result, recursion_budget): propcount = random.randint(0, 3) propname = None for i in range(propcount): die = random.random() if die < 0.5: propname = "%s_prop%d" % (name, i) result += self._Name(propname, recursion_budget - 1) else: propname = "\"%s\"" % self._RawRandomPropertyName() propvalue_name = "%s_val%d" % (name, i) result += self._Object(propvalue_name, recursion_budget - 1) result.append("try { %s[%s] = %s; } catch (e) {}" % (name, propname, propvalue_name)) if random.random() < 0.2 and propname: # Force the object to slow mode. result.append("delete %s[%s];" % (name, propname)) def _RandomElementIndex(self, element_name, result): if random.random() < 0.5: return random.randint(-1000, 1000) result += self._Smi(element_name, 0) return element_name def _AddElements(self, name, result, recursion_budget): elementcount = random.randint(0, 3) for i in range(elementcount): element_name = "%s_idx%d" % (name, i) index = self._RandomElementIndex(element_name, result) value_name = "%s_elt%d" % (name, i) result += self._Object(value_name, recursion_budget - 1) result.append("try { %s[%s] = %s; } catch(e) {}" % (name, index, value_name)) def _AddAccessors(self, name, result, recursion_budget): accessorcount = random.randint(0, 3) for i in range(accessorcount): propname = self._RawRandomPropertyName() what = random.choice(["get", "set"]) function_name = "%s_access%d" % (name, i) result += self._PlainFunction(function_name, recursion_budget - 1) result.append("try { Object.defineProperty(%s, \"%s\", {%s: %s}); } " "catch (e) {}" % (name, propname, what, function_name)) def _PlainArray(self, name, recursion_budget): die = random.random() if die < 0.5: literal = random.choice(["[]", "[1, 2]", "[1.5, 2.5]", "['a', 'b', 1, true]"]) return self._Variable(name, literal) else: new = random.choice(["", "new "]) length = random.randint(0, 101000) return self._Variable(name, "%sArray(%d)" % (new, length)) def _PlainObject(self, name, recursion_budget): die = random.random() if die < 0.67: literal_propcount = random.randint(0, 3) properties = [] result = [] for i in range(literal_propcount): propname = self._RawRandomPropertyName() propvalue_name = "%s_lit%d" % (name, i) result += self._Object(propvalue_name, recursion_budget - 1) properties.append("\"%s\": %s" % (propname, propvalue_name)) return result + self._Variable(name, "{%s}" % ", ".join(properties)) else: return self._Variable(name, "new Object()") def _JSArray(self, name, recursion_budget): result = self._PlainArray(name, recursion_budget) self._AddAccessors(name, result, recursion_budget) self._AddProperties(name, result, recursion_budget) self._AddElements(name, result, recursion_budget) return result def _RawRandomBufferLength(self): if random.random() < 0.2: return random.choice([0, 1, 8, 0x40000000, 0x80000000]) return random.randint(0, 1000) def _JSArrayBuffer(self, name, recursion_budget): length = self._RawRandomBufferLength() return self._Variable(name, "new ArrayBuffer(%d)" % length) def _JSDataView(self, name, recursion_budget): buffer_name = name + "_buffer" result = self._JSArrayBuffer(buffer_name, recursion_budget) args = [buffer_name] die = random.random() if die < 0.67: offset = self._RawRandomBufferLength() args.append("%d" % offset) if die < 0.33: length = self._RawRandomBufferLength() args.append("%d" % length) result += self._Variable(name, "new DataView(%s)" % ", ".join(args), fallback="new DataView(new ArrayBuffer(8))") return result def _JSDate(self, name, recursion_budget): die = random.random() if die < 0.25: return self._Variable(name, "new Date()") elif die < 0.5: ms_name = name + "_ms" result = self._Number(ms_name, recursion_budget) return result + self._Variable(name, "new Date(%s)" % ms_name) elif die < 0.75: str_name = name + "_str" month = random.choice(["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]) day = random.randint(1, 28) year = random.randint(1900, 2100) hour = random.randint(0, 23) minute = random.randint(0, 59) second = random.randint(0, 59) str_value = ("\"%s %s, %s %s:%s:%s\"" % (month, day, year, hour, minute, second)) result = self._Variable(str_name, str_value) return result + self._Variable(name, "new Date(%s)" % str_name) else: components = tuple(map(lambda x: "%s_%s" % (name, x), ["y", "m", "d", "h", "min", "s", "ms"])) return ([j for i in map(self._Int32, components) for j in i] + self._Variable(name, "new Date(%s)" % ", ".join(components))) def _PlainFunction(self, name, recursion_budget): result_name = "result" body = ["function() {"] body += self._Object(result_name, recursion_budget - 1) body.append("return result;\n}") return self._Variable(name, "%s" % "\n".join(body)) def _JSFunction(self, name, recursion_budget): result = self._PlainFunction(name, recursion_budget) self._AddAccessors(name, result, recursion_budget) self._AddProperties(name, result, recursion_budget) self._AddElements(name, result, recursion_budget) return result def _JSFunctionProxy(self, name, recursion_budget): # TODO(jkummerow): Revisit this as the Proxy implementation evolves. return self._Variable(name, "Proxy.createFunction(%s, function() {})" % self.PROXY_TRAPS) def _JSGeneratorObject(self, name, recursion_budget): # TODO(jkummerow): Be more creative here? return self._Variable(name, "(function*() { yield 1; })()") def _JSMap(self, name, recursion_budget, weak=""): result = self._Variable(name, "new %sMap()" % weak) num_entries = random.randint(0, 3) for i in range(num_entries): key_name = "%s_k%d" % (name, i) value_name = "%s_v%d" % (name, i) if weak: result += self._JSObject(key_name, recursion_budget - 1) else: result += self._Object(key_name, recursion_budget - 1) result += self._Object(value_name, recursion_budget - 1) result.append("%s.set(%s, %s)" % (name, key_name, value_name)) return result def _JSMapIterator(self, name, recursion_budget): map_name = name + "_map" result = self._JSMap(map_name, recursion_budget) iterator_type = random.choice(['keys', 'values', 'entries']) return (result + self._Variable(name, "%s.%s()" % (map_name, iterator_type))) def _JSProxy(self, name, recursion_budget): # TODO(jkummerow): Revisit this as the Proxy implementation evolves. return self._Variable(name, "Proxy.create(%s)" % self.PROXY_TRAPS) def _JSRegExp(self, name, recursion_budget): flags = random.choice(["", "g", "i", "m", "gi"]) string = "a(b|c)*a" # TODO(jkummerow): Be more creative here? ctor = random.choice(["/%s/%s", "new RegExp(\"%s\", \"%s\")"]) return self._Variable(name, ctor % (string, flags)) def _JSSet(self, name, recursion_budget, weak=""): result = self._Variable(name, "new %sSet()" % weak) num_entries = random.randint(0, 3) for i in range(num_entries): element_name = "%s_e%d" % (name, i) if weak: result += self._JSObject(element_name, recursion_budget - 1) else: result += self._Object(element_name, recursion_budget - 1) result.append("%s.add(%s)" % (name, element_name)) return result def _JSSetIterator(self, name, recursion_budget): set_name = name + "_set" result = self._JSSet(set_name, recursion_budget) iterator_type = random.choice(['values', 'entries']) return (result + self._Variable(name, "%s.%s()" % (set_name, iterator_type))) def _JSTypedArray(self, name, recursion_budget): arraytype = random.choice(["Int8", "Int16", "Int32", "Uint8", "Uint16", "Uint32", "Float32", "Float64", "Uint8Clamped"]) ctor_type = random.randint(0, 3) if ctor_type == 0: length = random.randint(0, 1000) return self._Variable(name, "new %sArray(%d)" % (arraytype, length), fallback="new %sArray(8)" % arraytype) elif ctor_type == 1: input_name = name + "_typedarray" result = self._JSTypedArray(input_name, recursion_budget - 1) return (result + self._Variable(name, "new %sArray(%s)" % (arraytype, input_name), fallback="new %sArray(8)" % arraytype)) elif ctor_type == 2: arraylike_name = name + "_arraylike" result = self._JSObject(arraylike_name, recursion_budget - 1) length = random.randint(0, 1000) result.append("try { %s.length = %d; } catch(e) {}" % (arraylike_name, length)) return (result + self._Variable(name, "new %sArray(%s)" % (arraytype, arraylike_name), fallback="new %sArray(8)" % arraytype)) else: die = random.random() buffer_name = name + "_buffer" args = [buffer_name] result = self._JSArrayBuffer(buffer_name, recursion_budget) if die < 0.67: offset_name = name + "_offset" args.append(offset_name) result += self._Int32(offset_name) if die < 0.33: length_name = name + "_length" args.append(length_name) result += self._Int32(length_name) return (result + self._Variable(name, "new %sArray(%s)" % (arraytype, ", ".join(args)), fallback="new %sArray(8)" % arraytype)) def _JSArrayBufferView(self, name, recursion_budget): if random.random() < 0.4: return self._JSDataView(name, recursion_budget) else: return self._JSTypedArray(name, recursion_budget) def _JSWeakCollection(self, name, recursion_budget): ctor = random.choice([self._JSMap, self._JSSet]) return ctor(name, recursion_budget, weak="Weak") def _PropertyDetails(self, name, recursion_budget): # TODO(jkummerow): Be more clever here? return self._Int32(name) def _JSObject(self, name, recursion_budget): die = random.random() if die < 0.4: function = random.choice([self._PlainObject, self._PlainArray, self._PlainFunction]) elif die < 0.5: return self._Variable(name, "this") # Global object. else: function = random.choice([self._JSArrayBuffer, self._JSDataView, self._JSDate, self._JSFunctionProxy, self._JSGeneratorObject, self._JSMap, self._JSMapIterator, self._JSRegExp, self._JSSet, self._JSSetIterator, self._JSTypedArray, self._JSValue, self._JSWeakCollection]) result = function(name, recursion_budget) self._AddAccessors(name, result, recursion_budget) self._AddProperties(name, result, recursion_budget) self._AddElements(name, result, recursion_budget) return result def _JSReceiver(self, name, recursion_budget): if random.random() < 0.9: return self._JSObject(name, recursion_budget) return self._JSProxy(name, recursion_budget) def _HeapObject(self, name, recursion_budget): die = random.random() if die < 0.9: return self._JSReceiver(name, recursion_budget) elif die < 0.95: return self._Oddball(name, recursion_budget) else: return self._Name(name, recursion_budget) def _Object(self, name, recursion_budget): if recursion_budget <= 0: function = random.choice([self._Oddball, self._Number, self._Name, self._JSValue, self._JSRegExp]) return function(name, recursion_budget) if random.random() < 0.2: return self._Smi(name, recursion_budget) return self._HeapObject(name, recursion_budget) GENERATORS = { "Boolean": ["true", _Boolean], "HeapObject": ["new Object()", _HeapObject], "Int32": ["32", _Int32], "JSArray": ["new Array()", _JSArray], "JSArrayBuffer": ["new ArrayBuffer(8)", _JSArrayBuffer], "JSArrayBufferView": ["new Int32Array(2)", _JSArrayBufferView], "JSDataView": ["new DataView(new ArrayBuffer(24))", _JSDataView], "JSDate": ["new Date()", _JSDate], "JSFunction": ["function() {}", _JSFunction], "JSFunctionProxy": ["Proxy.createFunction({}, function() {})", _JSFunctionProxy], "JSGeneratorObject": ["(function*(){ yield 1; })()", _JSGeneratorObject], "JSMap": ["new Map()", _JSMap], "JSMapIterator": ["new Map().entries()", _JSMapIterator], "JSObject": ["new Object()", _JSObject], "JSProxy": ["Proxy.create({})", _JSProxy], "JSReceiver": ["new Object()", _JSReceiver], "JSRegExp": ["/ab/g", _JSRegExp], "JSSet": ["new Set()", _JSSet], "JSSetIterator": ["new Set().values()", _JSSetIterator], "JSTypedArray": ["new Int32Array(2)", _JSTypedArray], "JSValue": ["new String('foo')", _JSValue], "JSWeakCollection": ["new WeakMap()", _JSWeakCollection], "Name": ["\"name\"", _Name], "Number": ["1.5", _Number], "Object": ["new Object()", _Object], "PropertyDetails": ["513", _PropertyDetails], "SeqOneByteString": ["\"seq 1-byte\"", _SeqString], "SeqString": ["\"seqstring\"", _SeqString], "SeqTwoByteString": ["\"seq \\u2082-byte\"", _SeqTwoByteString], "Smi": ["1", _Smi], "StrictMode": ["1", _StrictMode], "String": ["\"foo\"", _String], "Symbol": ["Symbol(\"symbol\")", _Symbol], "Uint32": ["32", _Uint32], } class ArgParser(object): def __init__(self, regex, ctor): self.regex = regex self.ArgCtor = ctor class Arg(object): def __init__(self, typename, varname, index): self.type = typename self.name = "_%s" % varname self.index = index class Function(object): def __init__(self, match): self.name = match.group(1) self.argslength = -1 self.args = {} self.inline = "" handle_arg_parser = ArgParser( re.compile("^\s*CONVERT_ARG_HANDLE_CHECKED\((\w+), (\w+), (\d+)\)"), lambda match: Arg(match.group(1), match.group(2), int(match.group(3)))) plain_arg_parser = ArgParser( re.compile("^\s*CONVERT_ARG_CHECKED\((\w+), (\w+), (\d+)\)"), lambda match: Arg(match.group(1), match.group(2), int(match.group(3)))) number_handle_arg_parser = ArgParser( re.compile("^\s*CONVERT_NUMBER_ARG_HANDLE_CHECKED\((\w+), (\d+)\)"), lambda match: Arg("Number", match.group(1), int(match.group(2)))) smi_arg_parser = ArgParser( re.compile("^\s*CONVERT_SMI_ARG_CHECKED\((\w+), (\d+)\)"), lambda match: Arg("Smi", match.group(1), int(match.group(2)))) double_arg_parser = ArgParser( re.compile("^\s*CONVERT_DOUBLE_ARG_CHECKED\((\w+), (\d+)\)"), lambda match: Arg("Number", match.group(1), int(match.group(2)))) number_arg_parser = ArgParser( re.compile( "^\s*CONVERT_NUMBER_CHECKED\(\w+, (\w+), (\w+), args\[(\d+)\]\)"), lambda match: Arg(match.group(2), match.group(1), int(match.group(3)))) strict_mode_arg_parser = ArgParser( re.compile("^\s*CONVERT_STRICT_MODE_ARG_CHECKED\((\w+), (\d+)\)"), lambda match: Arg("StrictMode", match.group(1), int(match.group(2)))) boolean_arg_parser = ArgParser( re.compile("^\s*CONVERT_BOOLEAN_ARG_CHECKED\((\w+), (\d+)\)"), lambda match: Arg("Boolean", match.group(1), int(match.group(2)))) property_details_parser = ArgParser( re.compile("^\s*CONVERT_PROPERTY_DETAILS_CHECKED\((\w+), (\d+)\)"), lambda match: Arg("PropertyDetails", match.group(1), int(match.group(2)))) arg_parsers = [handle_arg_parser, plain_arg_parser, number_handle_arg_parser, smi_arg_parser, double_arg_parser, number_arg_parser, strict_mode_arg_parser, boolean_arg_parser, property_details_parser] def SetArgsLength(self, match): self.argslength = int(match.group(1)) def TryParseArg(self, line): for parser in Function.arg_parsers: match = parser.regex.match(line) if match: arg = parser.ArgCtor(match) self.args[arg.index] = arg return True return False def Filename(self): return "%s.js" % self.name.lower() def __str__(self): s = [self.name, "("] argcount = self.argslength if argcount < 0: print("WARNING: unknown argslength for function %s" % self.name) if self.args: argcount = max([self.args[i].index + 1 for i in self.args]) else: argcount = 0 for i in range(argcount): if i > 0: s.append(", ") s.append(self.args[i].type if i in self.args else "") s.append(")") return "".join(s) class Macro(object): def __init__(self, match): self.name = match.group(1) self.args = [s.strip() for s in match.group(2).split(",")] self.lines = [] self.indentation = 0 self.AddLine(match.group(3)) def AddLine(self, line): if not line: return if not self.lines: # This is the first line, detect indentation. self.indentation = len(line) - len(line.lstrip()) line = line.rstrip("\\\n ") if not line: return assert len(line[:self.indentation].strip()) == 0, \ ("expected whitespace: '%s', full line: '%s'" % (line[:self.indentation], line)) line = line[self.indentation:] if not line: return self.lines.append(line + "\n") def Finalize(self): for arg in self.args: pattern = re.compile(r"(##|\b)%s(##|\b)" % arg) for i in range(len(self.lines)): self.lines[i] = re.sub(pattern, "%%(%s)s" % arg, self.lines[i]) def FillIn(self, arg_values): filler = {} assert len(arg_values) == len(self.args) for i in range(len(self.args)): filler[self.args[i]] = arg_values[i] result = [] for line in self.lines: result.append(line % filler) return result # Parses HEADERFILENAME to find out which runtime functions are "inline". def FindInlineRuntimeFunctions(): inline_functions = [] with open(HEADERFILENAME, "r") as f: inline_list = "#define INLINE_FUNCTION_LIST(F) \\\n" inline_function = re.compile(r"^\s*F\((\w+), \d+, \d+\)\s*\\?") mode = "SEARCHING" for line in f: if mode == "ACTIVE": match = inline_function.match(line) if match: inline_functions.append(match.group(1)) if not line.endswith("\\\n"): mode = "SEARCHING" elif mode == "SEARCHING": if line == inline_list: mode = "ACTIVE" return inline_functions def ReadFileAndExpandMacros(filename): found_macros = {} expanded_lines = [] with open(filename, "r") as f: found_macro = None for line in f: if found_macro is not None: found_macro.AddLine(line) if not line.endswith("\\\n"): found_macro.Finalize() found_macro = None continue match = MACRO.match(line) if match: found_macro = Macro(match) if found_macro.name in EXPAND_MACROS: found_macros[found_macro.name] = found_macro else: found_macro = None continue match = FIRST_WORD.match(line) if match: first_word = match.group(1) if first_word in found_macros: MACRO_CALL = re.compile("%s\(([^)]*)\)" % first_word) match = MACRO_CALL.match(line) assert match args = [s.strip() for s in match.group(1).split(",")] expanded_lines += found_macros[first_word].FillIn(args) continue expanded_lines.append(line) return expanded_lines # Detects runtime functions by parsing FILENAME. def FindRuntimeFunctions(): inline_functions = FindInlineRuntimeFunctions() functions = [] expanded_lines = ReadFileAndExpandMacros(FILENAME) function = None partial_line = "" for line in expanded_lines: # Multi-line definition support, ignoring macros. if line.startswith("RUNTIME_FUNCTION") and not line.endswith("{\n"): if line.endswith("\\\n"): continue partial_line = line.rstrip() continue if partial_line: partial_line += " " + line.strip() if partial_line.endswith("{"): line = partial_line partial_line = "" else: continue match = FUNCTION.match(line) if match: function = Function(match) if function.name in inline_functions: function.inline = "_" continue if function is None: continue match = ARGSLENGTH.match(line) if match: function.SetArgsLength(match) continue if function.TryParseArg(line): continue if line == FUNCTIONEND: if function is not None: functions.append(function) function = None return functions # Hack: This must have the same fields as class Function above, because the # two are used polymorphically in RunFuzzer(). We could use inheritance... class Builtin(object): def __init__(self, match): self.name = match.group(1) args = match.group(2) self.argslength = 0 if args == "" else args.count(",") + 1 self.inline = "" self.args = {} if self.argslength > 0: args = args.split(",") for i in range(len(args)): # a = args[i].strip() # TODO: filter out /* comments */ first. a = "" self.args[i] = Arg("Object", a, i) def __str__(self): return "%s(%d)" % (self.name, self.argslength) def FindJSBuiltins(): PATH = "src" fileslist = [] for (root, dirs, files) in os.walk(PATH): for f in files: if f.endswith(".js"): fileslist.append(os.path.join(root, f)) builtins = [] regexp = re.compile("^function (\w+)\s*\((.*?)\) {") matches = 0 for filename in fileslist: with open(filename, "r") as f: file_contents = f.read() file_contents = js2c.ExpandInlineMacros(file_contents) lines = file_contents.split("\n") partial_line = "" for line in lines: if line.startswith("function") and not '{' in line: partial_line += line.rstrip() continue if partial_line: partial_line += " " + line.strip() if '{' in line: line = partial_line partial_line = "" else: continue match = regexp.match(line) if match: builtins.append(Builtin(match)) return builtins # Classifies runtime functions. def ClassifyFunctions(functions): # Can be fuzzed with a JavaScript testcase. js_fuzzable_functions = [] # We have enough information to fuzz these, but they need inputs that # cannot be created or passed around in JavaScript. cctest_fuzzable_functions = [] # This script does not have enough information about these. unknown_functions = [] types = {} for f in functions: if f.name in BLACKLISTED: continue decision = js_fuzzable_functions custom = CUSTOM_KNOWN_GOOD_INPUT.get(f.name, None) if f.argslength < 0: # Unknown length -> give up unless there's a custom definition. if custom and custom[-1] is not None: f.argslength = custom[-1] assert len(custom) == f.argslength + 1, \ ("%s: last custom definition must be argslength" % f.name) else: decision = unknown_functions else: if custom: # Any custom definitions must match the known argslength. assert len(custom) == f.argslength + 1, \ ("%s should have %d custom definitions but has %d" % (f.name, f.argslength + 1, len(custom))) for i in range(f.argslength): if custom and custom[i] is not None: # All good, there's a custom definition. pass elif not i in f.args: # No custom definition and no parse result -> give up. decision = unknown_functions else: t = f.args[i].type if t in NON_JS_TYPES: decision = cctest_fuzzable_functions else: assert Generator.IsTypeSupported(t), \ ("type generator not found for %s, function: %s" % (t, f)) decision.append(f) return (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) def _GetKnownGoodArgs(function, generator): custom_input = CUSTOM_KNOWN_GOOD_INPUT.get(function.name, None) definitions = [] argslist = [] for i in range(function.argslength): if custom_input and custom_input[i] is not None: name = "arg%d" % i definitions.append("var %s = %s;" % (name, custom_input[i])) else: arg = function.args[i] name = arg.name definitions += generator.RandomVariable(name, arg.type, simple=True) argslist.append(name) return (definitions, argslist) def _GenerateTestcase(function, definitions, argslist, throws): s = ["// Copyright 2014 the V8 project authors. All rights reserved.", "// AUTO-GENERATED BY tools/generate-runtime-tests.py, DO NOT MODIFY", "// Flags: --allow-natives-syntax --harmony"] + definitions call = "%%%s%s(%s);" % (function.inline, function.name, ", ".join(argslist)) if throws: s.append("try {") s.append(call); s.append("} catch(e) {}") else: s.append(call) testcase = "\n".join(s) return testcase def GenerateJSTestcaseForFunction(function): gen = Generator() (definitions, argslist) = _GetKnownGoodArgs(function, gen) testcase = _GenerateTestcase(function, definitions, argslist, function.name in THROWS) path = os.path.join(BASEPATH, function.Filename()) with open(path, "w") as f: f.write("%s\n" % testcase) def GenerateTestcases(functions): shutil.rmtree(BASEPATH) # Re-generate everything. os.makedirs(BASEPATH) for f in functions: GenerateJSTestcaseForFunction(f) def _SaveFileName(save_path, process_id, save_file_index): return "%s/fuzz_%d_%d.js" % (save_path, process_id, save_file_index) def _GetFuzzableRuntimeFunctions(): functions = FindRuntimeFunctions() (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) = \ ClassifyFunctions(functions) return js_fuzzable_functions FUZZ_TARGET_LISTS = { "runtime": _GetFuzzableRuntimeFunctions, "builtins": FindJSBuiltins, } def RunFuzzer(process_id, options, stop_running): MAX_SLEEP_TIME = 0.1 INITIAL_SLEEP_TIME = 0.001 SLEEP_TIME_FACTOR = 1.25 base_file_name = "/dev/shm/runtime_fuzz_%d" % process_id test_file_name = "%s.js" % base_file_name stderr_file_name = "%s.out" % base_file_name save_file_index = 0 while os.path.exists(_SaveFileName(options.save_path, process_id, save_file_index)): save_file_index += 1 targets = FUZZ_TARGET_LISTS[options.fuzz_target]() try: for i in range(options.num_tests): if stop_running.is_set(): break function = None while function is None or function.argslength == 0: function = random.choice(targets) args = [] definitions = [] gen = Generator() for i in range(function.argslength): arg = function.args[i] argname = "arg%d%s" % (i, arg.name) args.append(argname) definitions += gen.RandomVariable(argname, arg.type, simple=False) testcase = _GenerateTestcase(function, definitions, args, True) with open(test_file_name, "w") as f: f.write("%s\n" % testcase) with open("/dev/null", "w") as devnull: with open(stderr_file_name, "w") as stderr: process = subprocess.Popen( [options.binary, "--allow-natives-syntax", "--harmony", "--enable-slow-asserts", test_file_name], stdout=devnull, stderr=stderr) end_time = time.time() + options.timeout timed_out = False exit_code = None sleep_time = INITIAL_SLEEP_TIME while exit_code is None: if time.time() >= end_time: # Kill the process and wait for it to exit. os.kill(process.pid, signal.SIGTERM) exit_code = process.wait() timed_out = True else: exit_code = process.poll() time.sleep(sleep_time) sleep_time = sleep_time * SLEEP_TIME_FACTOR if sleep_time > MAX_SLEEP_TIME: sleep_time = MAX_SLEEP_TIME if exit_code != 0 and not timed_out: oom = False with open(stderr_file_name, "r") as stderr: for line in stderr: if line.strip() == "# Allocation failed - process out of memory": oom = True break if oom: continue save_name = _SaveFileName(options.save_path, process_id, save_file_index) shutil.copyfile(test_file_name, save_name) save_file_index += 1 except KeyboardInterrupt: stop_running.set() finally: if os.path.exists(test_file_name): os.remove(test_file_name) if os.path.exists(stderr_file_name): os.remove(stderr_file_name) def BuildOptionParser(): usage = """Usage: %%prog [options] ACTION where ACTION can be: info Print diagnostic info. check Check that runtime functions can be parsed as expected, and that test cases exist. generate Parse source code for runtime functions, and auto-generate test cases for them. Warning: this will nuke and re-create %(path)s. fuzz Generate fuzz tests, run them, save those that crashed (see options). """ % {"path": os.path.relpath(BASEPATH)} o = optparse.OptionParser(usage=usage) o.add_option("--binary", default="out/x64.debug/d8", help="d8 binary used for running fuzz tests (default: %default)") o.add_option("--fuzz-target", default="runtime", help="Set of functions targeted by fuzzing. Allowed values: " "%s (default: %%default)" % ", ".join(FUZZ_TARGET_LISTS)) o.add_option("-n", "--num-tests", default=1000, type="int", help="Number of fuzz tests to generate per worker process" " (default: %default)") o.add_option("--save-path", default="~/runtime_fuzz_output", help="Path to directory where failing tests will be stored" " (default: %default)") o.add_option("--timeout", default=20, type="int", help="Timeout for each fuzz test (in seconds, default:" "%default)") return o def ProcessOptions(options, args): options.save_path = os.path.expanduser(options.save_path) if options.fuzz_target not in FUZZ_TARGET_LISTS: print("Invalid fuzz target: %s" % options.fuzz_target) return False if len(args) != 1 or args[0] == "help": return False return True def Main(): parser = BuildOptionParser() (options, args) = parser.parse_args() if not ProcessOptions(options, args): parser.print_help() return 1 action = args[0] functions = FindRuntimeFunctions() (js_fuzzable_functions, cctest_fuzzable_functions, unknown_functions) = \ ClassifyFunctions(functions) builtins = FindJSBuiltins() if action == "test": print("put your temporary debugging code here") return 0 if action == "info": print("%d functions total; js_fuzzable_functions: %d, " "cctest_fuzzable_functions: %d, unknown_functions: %d" % (len(functions), len(js_fuzzable_functions), len(cctest_fuzzable_functions), len(unknown_functions))) print("%d JavaScript builtins" % len(builtins)) print("unknown functions:") for f in unknown_functions: print(f) return 0 if action == "check": errors = 0 def CheckCount(actual, expected, description): if len(actual) != expected: print("Expected to detect %d %s, but found %d." % ( expected, description, len(actual))) print("If this change is intentional, please update the expectations" " at the top of %s." % THIS_SCRIPT) return 1 return 0 errors += CheckCount(functions, EXPECTED_FUNCTION_COUNT, "functions in total") errors += CheckCount(js_fuzzable_functions, EXPECTED_FUZZABLE_COUNT, "JavaScript-fuzzable functions") errors += CheckCount(cctest_fuzzable_functions, EXPECTED_CCTEST_COUNT, "cctest-fuzzable functions") errors += CheckCount(unknown_functions, EXPECTED_UNKNOWN_COUNT, "functions with incomplete type information") errors += CheckCount(builtins, EXPECTED_BUILTINS_COUNT, "JavaScript builtins") def CheckTestcasesExisting(functions): errors = 0 for f in functions: if not os.path.isfile(os.path.join(BASEPATH, f.Filename())): print("Missing testcase for %s, please run '%s generate'" % (f.name, THIS_SCRIPT)) errors += 1 files = filter(lambda filename: not filename.startswith("."), os.listdir(BASEPATH)) if (len(files) != len(functions)): unexpected_files = set(files) - set([f.Filename() for f in functions]) for f in unexpected_files: print("Unexpected testcase: %s" % os.path.join(BASEPATH, f)) errors += 1 print("Run '%s generate' to automatically clean these up." % THIS_SCRIPT) return errors errors += CheckTestcasesExisting(js_fuzzable_functions) def CheckNameClashes(runtime_functions, builtins): errors = 0 runtime_map = {} for f in runtime_functions: runtime_map[f.name] = 1 for b in builtins: if b.name in runtime_map: print("Builtin/Runtime_Function name clash: %s" % b.name) errors += 1 return errors errors += CheckNameClashes(functions, builtins) if errors > 0: return 1 print("Generated runtime tests: all good.") return 0 if action == "generate": GenerateTestcases(js_fuzzable_functions) return 0 if action == "fuzz": processes = [] if not os.path.isdir(options.save_path): os.makedirs(options.save_path) stop_running = multiprocessing.Event() for i in range(multiprocessing.cpu_count()): args = (i, options, stop_running) p = multiprocessing.Process(target=RunFuzzer, args=args) p.start() processes.append(p) try: for i in range(len(processes)): processes[i].join() except KeyboardInterrupt: stop_running.set() for i in range(len(processes)): processes[i].join() return 0 if __name__ == "__main__": sys.exit(Main())