Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

1
deps/v8/tools/clusterfuzz/OWNERS vendored Normal file
View File

@ -0,0 +1 @@
file:../../INFRA_OWNERS

View File

@ -0,0 +1,25 @@
# Copyright 2017 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("../../../gni/v8.gni")
if (v8_correctness_fuzzer) {
copy("v8_correctness_fuzzer_resources") {
sources = [
"v8_commands.py",
"v8_foozzie.py",
"v8_foozzie_harness_adjust.js",
"v8_fuzz_config.py",
"v8_fuzz_experiments.json",
"v8_fuzz_flags.json",
"v8_mock.js",
"v8_mock_archs.js",
"v8_mock_webassembly.js",
"v8_smoke_tests.js",
"v8_suppressions.js",
"v8_suppressions.py",
]
outputs = [ "$root_out_dir/{{source_file_part}}" ]
}
}

View File

@ -0,0 +1,31 @@
# Copyright 2018 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 json
# This line is 'magic' in that git-cl looks for it to decide whether to
# use Python3 instead of Python2 when running the code in this file.
USE_PYTHON3 = True
def _RunTests(input_api, output_api):
return input_api.RunTests(
input_api.canned_checks.GetUnitTestsInDirectory(
input_api, output_api, '.', files_to_check=[r'.+_test\.py$'],
run_on_python2=False))
def _CommonChecks(input_api, output_api):
"""Checks common to both upload and commit."""
checks = [
_RunTests,
]
return sum([check(input_api, output_api) for check in checks], [])
def CheckChangeOnCommit(input_api, output_api):
return _CommonChecks(input_api, output_api)
def CheckChangeOnUpload(input_api, output_api):
return _CommonChecks(input_api, output_api)

View File

@ -0,0 +1,22 @@
# Copyright 2016 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 sys
print("""
Some smoke-test output.
___foozzie___smoke_test_end___
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
""")
if '--bad-flag' in sys.argv:
print('bad behavior')
if '--avoid-cross-arch' in sys.argv:
print('Warning: This run cannot be compared across architectures.')

View File

@ -0,0 +1 @@
{"v8_current_cpu": "x64"}

View File

@ -0,0 +1,16 @@
# Copyright 2016 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.
print("""
Some smoke-test output.
___foozzie___smoke_test_end___
Warning: --dont-use-this is deprecated
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
""")

View File

@ -0,0 +1 @@
{"v8_current_cpu": "x64"}

View File

@ -0,0 +1,15 @@
# Copyright 2016 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.
print("""
Some smoke-test output.
___foozzie___smoke_test_end___
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
not unknown
""")

View File

@ -0,0 +1 @@
{"v8_current_cpu": "x64"}

View File

@ -0,0 +1,24 @@
# Copyright 2016 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 sys
print("""
Some smoke-test output.
___foozzie___smoke_test_end___
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
""")
if '--bad-flag' in sys.argv:
print('bad behavior')
if '--very-bad-flag' in sys.argv:
print('very bad behavior')
if '--jitless' in sys.argv:
print('jitless flag passed')

View File

@ -0,0 +1 @@
{"v8_current_cpu": "x86"}

View File

@ -0,0 +1,16 @@
# Copyright 2024 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 sys
if '--crash-the-smoke-test' in sys.argv:
sys.exit(73)
# Simulate different output during the smoke test.
print("""
Different smoke-test output.
___foozzie___smoke_test_end___
Other different output.
""")

View File

@ -0,0 +1 @@
{"v8_current_cpu": "x64"}

View File

@ -0,0 +1,20 @@
# Copyright 2024 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 sys
print("""
Some smoke-test output.
___foozzie___smoke_test_end___
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
""")
if '--simulate-errors' in sys.argv:
sys.exit(73)

View File

@ -0,0 +1 @@
{"v8_current_cpu": "x64"}

View File

@ -0,0 +1,47 @@
#
# V8 correctness failure
# V8 correctness sources: f6
# V8 correctness suppression:
#
# CHECK
#
# Compared x64 with x64
#
# Common flags:
--correctness-fuzzer-suppressions --expose-gc --fuzzing --allow-natives-for-differential-fuzzing --invoke-weak-callbacks --omit-quit --harmony --js-staging --wasm-staging --no-wasm-async-compilation --wasm-max-mem-pages=32767 --suppress-asm-messages --random-seed 12345
# Baseline flags:
--turbo-filter=~ --no-turbofan --no-sparkplug --liftoff --no-wasm-tier-up --no-maglev --flag1 --flag2=0
# Comparison flags:
--flag3
#
# Difference:
- unknown
+ not unknown
#
# Source file:
name/to/file.js
#
### Start of baseline output:
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
### End of baseline output
#
### Start of comparison output:
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
not unknown
### End of comparison output

View File

@ -0,0 +1,47 @@
#
# V8 correctness failure
# V8 correctness sources: f6
# V8 correctness suppression:
#
# CHECK
#
# Compared x64 with x64
#
# Common flags:
--correctness-fuzzer-suppressions --expose-gc --fuzzing --allow-natives-for-differential-fuzzing --invoke-weak-callbacks --omit-quit --harmony --js-staging --wasm-staging --no-wasm-async-compilation --wasm-max-mem-pages=32767 --suppress-asm-messages --random-seed 12345
# Baseline flags:
--turbo-filter=~ --no-turbofan --no-sparkplug --liftoff --no-wasm-tier-up --no-maglev
# Comparison flags:
--bad-flag
#
# Difference:
+ bad behavior
#
# Source file:
name/to/file.js
#
### Start of baseline output:
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
### End of baseline output
#
### Start of comparison output:
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
bad behavior
### End of comparison output

View File

@ -0,0 +1,47 @@
#
# V8 correctness failure
# V8 correctness sources: f6
# V8 correctness suppression:
#
# CHECK
#
# Compared x64 with ia32
#
# Common flags:
--correctness-fuzzer-suppressions --expose-gc --fuzzing --allow-natives-for-differential-fuzzing --invoke-weak-callbacks --omit-quit --harmony --js-staging --wasm-staging --no-wasm-async-compilation --wasm-max-mem-pages=32767 --suppress-asm-messages --random-seed 12345
# Baseline flags:
--turbo-filter=~ --no-turbofan --no-sparkplug --liftoff --no-wasm-tier-up --no-maglev
# Comparison flags:
--very-bad-flag
#
# Difference:
+ very bad behavior
#
# Source file:
name/to/file.js
#
### Start of baseline output:
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
### End of baseline output
#
### Start of comparison output:
1
v8-foozzie source: name/to/a/file.js
2
v8-foozzie source: name/to/file.js
3
unknown
very bad behavior
### End of comparison output

View File

@ -0,0 +1,9 @@
// Copyright 2016 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.
// Empty test dummy.
print("js-mutation: start generated test case");
// For testing filters based on content.
let b = %DontUseThis();

View File

@ -0,0 +1,7 @@
// Copyright 2024 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.
// Test dummy to check flags for particular content.
print("js-mutation: start generated test case");
print(%WasmStruct());

View File

@ -0,0 +1,5 @@
#
# V8 correctness failure
# V8 correctness sources:
# V8 correctness suppression: _simulated_crash_

View File

@ -0,0 +1,5 @@
#
# V8 correctness failure
# V8 correctness sources:
# V8 correctness suppression: _unexpected_crash_

View File

@ -0,0 +1,35 @@
#
# V8 correctness failure
# V8 correctness sources: smoke test failed
# V8 correctness suppression:
#
# CHECK
#
# Compared x64 with x64
#
# Common flags:
--correctness-fuzzer-suppressions --expose-gc --fuzzing --allow-natives-for-differential-fuzzing --invoke-weak-callbacks --omit-quit --harmony --js-staging --wasm-staging --no-wasm-async-compilation --wasm-max-mem-pages=32767 --suppress-asm-messages --random-seed 12345
# Baseline flags:
--turbo-filter=~ --no-turbofan --no-sparkplug --liftoff --no-wasm-tier-up --no-maglev
# Comparison flags:
#
# Difference:
- Some smoke-test output.
+ Different smoke-test output.
#
# Source file:
foozzie smoke test
#
### Start of baseline output:
Some smoke-test output.
### End of baseline output
#
### Start of comparison output:
Different smoke-test output.
### End of comparison output

View File

@ -0,0 +1,15 @@
# Copyright 2019 The Chromium 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("//build/toolchain/gcc_toolchain.gni")
# Fake toolchain to enable build output for an experimental build in a
# nested build sub-directory. We toggle certain build flags when this
# toolchain is used in v8/BUILD.gn.
clang_toolchain("clang_x64_fuzzer_experiments") {
toolchain_args = {
current_cpu = "x64"
current_os = "linux"
}
}

View File

@ -0,0 +1,169 @@
# Copyright 2016 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.
# Fork from commands.py and output.py in v8 test driver.
import signal
import subprocess
import sys
from pathlib import Path
from threading import Event, Timer
# List of default flags passed to each d8 run.
DEFAULT_FLAGS = [
'--correctness-fuzzer-suppressions',
'--expose-gc',
'--fuzzing',
'--allow-natives-for-differential-fuzzing',
'--invoke-weak-callbacks',
'--omit-quit',
'--harmony',
'--js-staging',
'--wasm-staging',
'--no-wasm-async-compilation',
# Limit wasm memory to just below 2GiB, to avoid differences between 32-bit
# and 64-bit builds.
'--wasm-max-mem-pages=32767',
'--suppress-asm-messages',
]
BASE_PATH = Path(__file__).parent.resolve()
# List of files passed to each d8 run before the testcase.
DEFAULT_MOCK = BASE_PATH / 'v8_mock.js'
SMOKE_TESTS = BASE_PATH / 'v8_smoke_tests.js'
# Suppressions on JavaScript level for known issues.
JS_SUPPRESSIONS = BASE_PATH / 'v8_suppressions.js'
# Config-specific mock files.
ARCH_MOCKS = BASE_PATH / 'v8_mock_archs.js'
WEBASSEMBLY_MOCKS = BASE_PATH / 'v8_mock_webassembly.js'
# Non-standard exit code only used to simulate crashes in tests.
CRASH_CODE_FOR_TESTING = 73
def _startup_files(options):
"""Default files and optional config-specific mock files."""
files = [DEFAULT_MOCK]
if not options.skip_suppressions:
files.append(JS_SUPPRESSIONS)
if options.first.arch != options.second.arch:
files.append(ARCH_MOCKS)
# Mock out WebAssembly when comparing with jitless mode.
if '--jitless' in options.first.flags + options.second.flags:
files.append(WEBASSEMBLY_MOCKS)
# Keep the smoke tests last, right before the actual test case.
return files + [SMOKE_TESTS]
class BaseException(Exception):
"""Used to abort the comparison workflow and print the given message."""
def __init__(self, message):
self.message = message
class PassException(BaseException):
"""Represents an early abort making the overall run pass."""
pass
class FailException(BaseException):
"""Represents an early abort making the overall run fail."""
pass
class Command(object):
"""Represents a configuration for running V8 multiple times with certain
flags and files.
"""
def __init__(self, options, label, executable, config_flags):
self.label = label
self.executable = executable
self.config_flags = config_flags
self.common_flags = DEFAULT_FLAGS[:]
self.common_flags.extend(['--random-seed', str(options.random_seed)])
self.files = _startup_files(options)
def run(self, testcase, timeout):
"""Run the executable with a specific testcase."""
raw_args = [self.executable] + self.flags + self.files + [testcase]
args = list(map(str, raw_args))
print(f'# Command line for {self.label} comparison:')
print(' '.join(args))
if self.executable.suffix == '.py':
# Wrap with python in tests.
args = [sys.executable] + args
return Execute(
args,
cwd=testcase.parent,
timeout=timeout,
)
@property
def flags(self):
return self.common_flags + self.config_flags
def remove_config_flag(self, remove_flag):
"""Removes all occurences of `remove_flag` from the config flags."""
self.config_flags = [
flag for flag in self.config_flags if flag != remove_flag
]
class Output(object):
def __init__(self, exit_code, stdout_bytes, pid):
self.exit_code = exit_code
self.stdout_bytes = stdout_bytes
self.pid = pid
@property
def stdout(self):
try:
return self.stdout_bytes.decode('utf-8')
except UnicodeDecodeError:
return self.stdout_bytes.decode('latin-1')
def HasCrashed(self):
return self.exit_code < 0 or self.exit_code == CRASH_CODE_FOR_TESTING
def Execute(args, cwd, timeout=None):
popen_args = [c for c in args if c != ""]
try:
process = subprocess.Popen(
args=popen_args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=cwd,
)
except Exception as e:
sys.stderr.write(f'Error executing: {popen_args}\n')
raise e
timeout_event = Event()
def kill_process():
timeout_event.set()
try:
process.kill()
except OSError:
sys.stderr.write(f'Error: Process {process.pid} already ended.\n')
timer = Timer(timeout, kill_process)
timer.start()
stdout_bytes, _ = process.communicate()
timer.cancel()
if timeout_event.is_set():
raise PassException('# V8 correctness - T-I-M-E-O-U-T')
return Output(
process.returncode,
stdout_bytes,
process.pid,
)

View File

@ -0,0 +1,613 @@
#!/usr/bin/env python3
# Copyright 2016 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.
"""
V8 correctness fuzzer launcher script.
"""
import argparse
import hashlib
import json
import re
import sys
import traceback
from collections import namedtuple
from pathlib import Path
from v8_commands import Command, FailException, PassException
import v8_suppressions
CONFIGS = dict(
default=[],
ignition=[
'--turbo-filter=~',
'--no-turbofan',
'--no-sparkplug',
'--liftoff',
'--no-wasm-tier-up',
'--no-maglev',
],
ignition_asm=[
'--turbo-filter=~',
'--no-turbofan',
'--no-sparkplug',
'--validate-asm',
'--stress-validate-asm',
'--no-maglev',
],
ignition_eager=[
'--turbo-filter=~',
'--no-turbofan',
'--no-sparkplug',
'--no-lazy',
'--no-lazy-inner-functions',
'--no-maglev',
],
ignition_no_ic=[
'--turbo-filter=~',
'--no-turbofan',
'--no-sparkplug',
'--liftoff',
'--no-wasm-tier-up',
'--no-use-ic',
'--no-lazy-feedback-allocation',
'--no-maglev',
],
ignition_turbo=[],
ignition_turbo_no_ic=[
'--no-use-ic',
],
ignition_turbo_opt=[
'--always-turbofan',
'--no-liftoff',
],
ignition_turbo_opt_eager=[
'--always-turbofan',
'--no-lazy',
'--no-lazy-inner-functions',
],
ignition_maglev=[
'--maglev',
'--turbo-filter=~',
'--no-turbofan',
],
jitless=[
'--jitless',
],
slow_path=[
'--force-slow-path',
],
slow_path_opt=[
'--always-turbofan',
'--force-slow-path',
],
)
BASELINE_CONFIG = 'ignition'
DEFAULT_CONFIG = 'default'
DEFAULT_D8 = 'd8'
# Return codes.
RETURN_PASS = 0
RETURN_FAIL = 2
BASE_PATH = Path(__file__).parent.resolve()
# Timeout for one d8 run.
TEST_TIMEOUT_SEC = 3
SUPPORTED_ARCHS = ['ia32', 'x64', 'arm', 'arm64']
FAILURE_HEADER_TEMPLATE = """#
# V8 correctness failure
# V8 correctness sources: %(source_key)s
# V8 correctness suppression: %(suppression)s
"""
# Extended output for failure case. The 'CHECK' is for the minimizer.
DETAILS_TEMPLATE = """#
# CHECK
#
# Compared %(first_config_arch)s with %(second_config_arch)s
#
# Common flags:
%(common_flags)s
# Baseline flags:
%(first_config_flags)s
# Comparison flags:
%(second_config_flags)s
#
# Difference:
%(difference)s%(source_file_text)s
#
### Start of baseline output:
%(first_config_output)s
### End of baseline output
#
### Start of comparison output:
%(second_config_output)s
### End of comparison output
"""
FAILURE_TEMPLATE = FAILURE_HEADER_TEMPLATE + DETAILS_TEMPLATE
SOURCE_FILE_TEMPLATE = """
#
# Source file:
%s"""
FUZZ_TEST_RE = re.compile(r'.*fuzz(-\d+\.js)')
# The number of hex digits used from the hash of the original source file path.
# Keep the number small to avoid duplicate explosion.
SOURCE_HASH_LENGTH = 2
# Placeholder string if no original source file could be determined.
ORIGINAL_SOURCE_DEFAULT = 'none'
# Placeholder string for failures from crash tests. If a failure is found with
# this signature, the matching sources should be moved to the mapping below.
ORIGINAL_SOURCE_CRASHTESTS = 'placeholder for CrashTests'
# Mapping from relative original source path (e.g. CrashTests/path/to/file.js)
# to a string key. Map to the same key for duplicate issues. The key should
# have more than 3 characters to not collide with other existing hashes.
# If a symptom from a particular original source file is known to map to a
# known failure, it can be added to this mapping. This should be done for all
# failures from CrashTests, as those by default map to the placeholder above.
KNOWN_FAILURES = {
# Foo.caller with asm.js: https://crbug.com/1042556
'CrashTests/4782147262545920/494.js': '.caller',
'CrashTests/5637524389167104/01457.js': '.caller',
'CrashTests/5703451898085376/02176.js': '.caller',
'CrashTests/4846282433495040/04342.js': '.caller',
'CrashTests/5712410200899584/04483.js': '.caller',
'v8/test/mjsunit/regress/regress-105.js': '.caller',
# Flaky issue that almost never repros.
'CrashTests/5694376231632896/1033966.js': 'flaky',
}
# Flags that crash smoke tests or produce non-deterministic output should
# not be used.
DISALLOWED_FLAGS = [
# Bails out when sorting, leading to differences in sorted output.
'--multi-mapped-mock-allocator',
# TODO(https://crbug.com/1393020): Changes the global object.
'--harmony-struct',
# Cuts off stdout earlier in some situations and might alter the behavior
# of neutered runtime functions.
'--sandbox-testing',
# Various non-deterministic stats.
'--dump-counters',
'--expose-statistics',
'--log',
'--rcs',
]
# The same as above, but prefixes that either match multiple flags or flags
# that assign values.
DISALLOWED_FLAG_PREFIXES = [
# Already passed as a default flag. That default value should not be
# overwritten in one of the runs.
'--wasm-max-mem-pages=',
]
# List pairs of flags that lead to contradictory cycles, i.e.:
# A -> no-C and B -> C makes (A, B) contradictory.
# No need to list other contradictions, they are omitted by the
# --fuzzing flag).
CONTRADICTORY_FLAGS = [
('--always-turbofan', '--jitless'),
('--assert-types', '--stress-concurrent-inlining'),
('--assert-types', '--stress-concurrent-inlining-attach-code'),
('--jitless', '--stress-concurrent-inlining'),
('--jitless', '--stress-concurrent-inlining-attach-code'),
]
def is_disallowed_flag_prefix(flag):
return any(flag.startswith(prefix) for prefix in DISALLOWED_FLAG_PREFIXES)
def filter_flags(flags):
"""Drop disallowed and contradictory flags.
The precedence for contradictions is right to left, similar to the V8 test
framework.
"""
result = []
flags_to_drop = set(DISALLOWED_FLAGS)
for flag in reversed(flags):
if flag in flags_to_drop or is_disallowed_flag_prefix(flag):
continue
result.append(flag)
for contradicting_pair in CONTRADICTORY_FLAGS:
if contradicting_pair[0] == flag:
flags_to_drop.add(contradicting_pair[1])
if contradicting_pair[1] == flag:
flags_to_drop.add(contradicting_pair[0])
return list(reversed(result))
def infer_arch(d8):
"""Infer the V8 architecture from the build configuration next to the
executable.
"""
with (d8.parent / 'v8_build_config.json').open() as f:
arch = json.load(f)['v8_current_cpu']
arch = 'ia32' if arch == 'x86' else arch
assert arch in SUPPORTED_ARCHS
return arch
class ExecutionArgumentsConfig(object):
def __init__(self, label):
self.label = label
def add_arguments(self, parser, default_config):
def add_argument(flag_template, help_template, **kwargs):
parser.add_argument(
flag_template % self.label,
help=help_template % self.label,
**kwargs)
add_argument(
'--%s-config',
'%s configuration',
default=default_config)
add_argument(
'--%s-config-extra-flags',
'additional flags passed to the %s run',
action='append',
default=[])
add_argument(
'--%s-d8',
'optional path to %s d8 executable, '
'default: bundled in the directory of this script',
default=DEFAULT_D8)
def make_options(self, options, default_config=None, default_d8=None):
def get(name):
return getattr(options, f'{self.label}_{name}')
config = default_config or get('config')
assert config in CONFIGS
d8 = Path(default_d8 or get('d8'))
if not d8.is_absolute():
d8 = BASE_PATH / d8
assert d8.exists()
flags = filter_flags(CONFIGS[config] + get('config_extra_flags'))
RunOptions = namedtuple('RunOptions', ['arch', 'd8', 'flags'])
return RunOptions(infer_arch(d8), d8, flags)
class ExecutionConfig(object):
def __init__(self, options, label):
self.options = options
self.label = label
self.arch = getattr(options, label).arch
d8 = getattr(options, label).d8
flags = getattr(options, label).flags
self.command = Command(options, label, d8, flags)
# Options for a fallback configuration only exist when comparing
# different architectures.
fallback_label = label + '_fallback'
self.fallback = None
if getattr(options, fallback_label, None):
self.fallback = ExecutionConfig(options, fallback_label)
@property
def common_flags(self):
return self.command.common_flags
@property
def config_flags(self):
return self.command.config_flags
@property
def flags(self):
return self.command.flags
def has_config_flag(self, flag):
return (flag in self.config_flags or
self.fallback and self.fallback.has_config_flag(flag))
def remove_config_flag(self, flag):
self.command.remove_config_flag(flag)
if self.fallback:
self.fallback.remove_config_flag(flag)
@property
def is_error_simulation(self):
return '--simulate-errors' in self.flags
def parse_args(args):
first_config_arguments = ExecutionArgumentsConfig('first')
second_config_arguments = ExecutionArgumentsConfig('second')
parser = argparse.ArgumentParser()
parser.add_argument(
'--random-seed', type=int, required=True,
help='random seed passed to both runs')
parser.add_argument(
'--skip-suppressions', default=False, action='store_true',
help='skip suppressions to reproduce known issues')
parser.add_argument(
'--compact', default=False, action='store_true',
help='deprecated: compact error reporting is the default')
# Add arguments for each run configuration.
first_config_arguments.add_arguments(parser, BASELINE_CONFIG)
second_config_arguments.add_arguments(parser, DEFAULT_CONFIG)
parser.add_argument('testcase', help='path to test case')
options = parser.parse_args(args)
# Ensure we have a test case.
options.testcase = Path(options.testcase)
assert options.testcase.is_file(), (
f"Test case {options.testcase} doesn't exist")
options.first = first_config_arguments.make_options(options)
options.second = second_config_arguments.make_options(options)
options.default = second_config_arguments.make_options(
options, default_config=DEFAULT_CONFIG)
# Use fallback configurations only on different architectures. In this
# case we are going to re-test against the first architecture.
if options.first.arch != options.second.arch:
options.second_fallback = second_config_arguments.make_options(
options, default_d8=options.first.d8)
options.default_fallback = second_config_arguments.make_options(
options, default_config=DEFAULT_CONFIG, default_d8=options.first.d8)
# Ensure we make a valid comparison.
if (options.first.d8 == options.second.d8 and
options.first.flags == options.second.flags):
parser.error('Need either executable or flag difference.')
return options
def create_execution_configs(options):
"""Prepare the baseline, default and secondary configuration to compare to.
The default (turbofan) takes precedence as many of the secondary configs
are based on the turbofan config with additional parameters.
"""
return [
ExecutionConfig(options, 'first'),
ExecutionConfig(options, 'default'),
ExecutionConfig(options, 'second'),
]
def fail_bailout(output, ignore_by_output_fun):
"""Print failure state and return if ignore_by_output_fun matches output."""
bug = (ignore_by_output_fun(output.stdout) or '').strip()
if bug:
raise FailException(FAILURE_HEADER_TEMPLATE % dict(
source_key='', suppression=bug))
def format_difference(
first_config, second_config,
first_config_output, second_config_output,
difference, source=None):
# The first three entries will be parsed by clusterfuzz. Format changes
# will require changes on the clusterfuzz side.
source_file_text = SOURCE_FILE_TEMPLATE % source if source else ''
assert first_config.common_flags == second_config.common_flags
return FAILURE_TEMPLATE % dict(
source_file_text=source_file_text,
source_key=cluster_failures(source),
suppression='', # We can't tie bugs to differences.
first_config_arch=first_config.arch,
second_config_arch=second_config.arch,
common_flags=' '.join(first_config.common_flags),
first_config_flags=' '.join(first_config.config_flags),
second_config_flags=' '.join(second_config.config_flags),
first_config_output=first_config_output,
second_config_output=second_config_output,
source=source,
difference=difference,
)
def cluster_failures(source, known_failures=None):
"""Returns a string key for clustering duplicate failures.
Args:
source: The original source path where the failure happened.
known_failures: Mapping from original source path to failure key.
"""
known_failures = known_failures or KNOWN_FAILURES
# No source known. Typical for manually uploaded issues. This
# requires also manual issue creation.
if not source:
return ORIGINAL_SOURCE_DEFAULT
# Source is known to produce a particular failure.
if source in known_failures:
return known_failures[source]
# Subsume all other sources from CrashTests under one key. Otherwise
# failures lead to new crash tests which in turn lead to new failures.
if source.startswith('CrashTests'):
return ORIGINAL_SOURCE_CRASHTESTS
# Report all deviations from the smoke test as one to avoid a bug flood.
if source == v8_suppressions.SMOKE_TEST_SOURCE:
return 'smoke test failed'
# We map all remaining failures to a short hash of the original source.
long_key = hashlib.sha1(source.encode('utf-8')).hexdigest()
return long_key[:SOURCE_HASH_LENGTH]
class RepeatedRuns(object):
"""Helper class for storing statistical data from repeated runs."""
def __init__(self, test_case, timeout):
self.test_case = test_case
self.timeout = timeout
# Stores if any run has crashed or was simulated.
self.has_crashed = False
self.simulated = False
def run(self, config):
comparison_output = config.command.run(
self.test_case, timeout=self.timeout)
self.has_crashed = self.has_crashed or comparison_output.HasCrashed()
self.simulated = self.simulated or config.is_error_simulation
return comparison_output
@property
def crash_state(self):
return '_simulated_crash_' if self.simulated else '_unexpected_crash_'
def run_comparisons(suppress, execution_configs, test_case, timeout):
"""Runs different configurations and bails out on output difference.
Args:
suppress: The helper object for textual suppressions.
execution_configs: Two or more configurations to run. The first one will be
used as baseline to compare all others to.
test_case: The test case to run.
timeout: Timeout in seconds for one run.
"""
runner = RepeatedRuns(test_case, timeout)
# Run the baseline configuration.
baseline_config = execution_configs[0]
baseline_output = runner.run(baseline_config)
# The output of the baseline run might change the shape of the remaining
# configurations.
remaining_configs = execution_configs[1:]
changes = suppress.adjust_configs_by_output(
remaining_configs, baseline_output.stdout)
if changes:
print('# Adjusted experiments based on baseline output:')
print('\n'.join(changes))
# Iterate over the remaining configurations, run and compare.
for comparison_config in remaining_configs:
comparison_output = runner.run(comparison_config)
difference, source = suppress.diff(baseline_output, comparison_output)
if difference:
# Only bail out due to suppressed output if there was a difference. If a
# suppression doesn't show up anymore in the statistics, we might want to
# remove it.
fail_bailout(baseline_output, suppress.ignore_by_output)
fail_bailout(comparison_output, suppress.ignore_by_output)
# Check if a difference also occurs with the fallback configuration and
# give it precedence. E.g. we always prefer x64 differences.
if comparison_config.fallback:
fallback_output = runner.run(comparison_config.fallback)
fallback_difference, fallback_source = suppress.diff(
baseline_output, fallback_output)
# Ensure also that the source doesn't differ, e.g. in case we have
# a real failure in the cross-arch comparison, but find the smoke
# tests failing in the fallback.
if fallback_difference and source == fallback_source:
fail_bailout(fallback_output, suppress.ignore_by_output)
comparison_config = comparison_config.fallback
comparison_output = fallback_output
difference = fallback_difference
# We print only output relevant to the source of the failure. E.g.
# only print smoke-test output or real failure output.
reduced_baseline_output = suppress.reduced_output(
baseline_output.stdout, source)
reduced_comparison_output = suppress.reduced_output(
comparison_output.stdout, source)
raise FailException(format_difference(
baseline_config, comparison_config, reduced_baseline_output,
reduced_comparison_output, difference, source))
# Bail out if we crash during smoke tests already or if the crash is
# expected based on a simulation flag.
if (runner.has_crashed and (
source == v8_suppressions.SMOKE_TEST_SOURCE or runner.simulated)):
# Subsume such crashes with one failure state.
raise FailException(FAILURE_HEADER_TEMPLATE % dict(
source_key='', suppression=runner.crash_state))
if runner.has_crashed:
# Show if a crash has happened in one of the runs, but no difference was
# detected. This is only for the statistics during experiments.
raise PassException('# V8 correctness - C-R-A-S-H')
def main():
options = parse_args(sys.argv[1:])
suppress = v8_suppressions.get_suppression(options.skip_suppressions)
# Static bailout based on test case content or metadata.
with options.testcase.open(encoding='utf-8') as f:
content = f.read()
execution_configs = create_execution_configs(options)
# Some configs will be dropped or altered based on the testcase content.
changes = suppress.adjust_configs_by_content(execution_configs, content)
if changes:
print('# Adjusted flags and experiments based on the test case:')
print('\n'.join(changes))
# Run all configs against the fuzz test case.
run_comparisons(
suppress, execution_configs,
test_case=options.testcase,
timeout=TEST_TIMEOUT_SEC,
)
# TODO(machenbach): Figure out if we could also return a bug in case
# there's no difference, but one of the line suppressions has matched -
# and without the match there would be a difference.
print('# V8 correctness - pass')
return RETURN_PASS
if __name__ == "__main__":
try:
result = main()
except FailException as e:
print(e.message)
result = RETURN_FAIL
except PassException as e:
print(e.message)
result = RETURN_PASS
except SystemExit:
# Make sure clusterfuzz reports internal errors and wrong usage.
# Use one label for all internal and usage errors.
print(FAILURE_HEADER_TEMPLATE % dict(
source_key='', suppression='wrong_usage'))
result = RETURN_FAIL
except MemoryError:
# Running out of memory happens occasionally but is not actionable.
print('# V8 correctness - pass')
result = RETURN_PASS
except Exception as e:
print(FAILURE_HEADER_TEMPLATE % dict(
source_key='', suppression='internal_error'))
print(f'# Internal error: {e}')
traceback.print_exc(file=sys.stdout)
result = RETURN_FAIL
sys.exit(result)

View File

@ -0,0 +1,100 @@
// Copyright 2018 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.
// Extensions to mjsunit and other test harnesses added between harness and
// fuzzing code.
try {
// Scope for utility functions.
(function() {
// Same as in mjsunit.js.
function classOf(object) {
// Argument must not be null or undefined.
var string = Object.prototype.toString.call(object);
// String has format [object <ClassName>].
return string.substring(8, string.length - 1);
}
// Override prettyPrinted with a version that also recusively prints object
// properties (with a depth of 3).
let origPrettyPrinted = prettyPrinted;
prettyPrinted = function prettyPrinted(value, depth=3) {
if (depth == 0) {
return "...";
}
switch (typeof value) {
case "object":
if (value === null) return "null";
var objectClass = classOf(value);
switch (objectClass) {
case "Object":
var name = value.constructor.name;
if (!name)
name = "Object";
return name + "{" + Object.keys(value).map(function(key, index) {
return (
prettyPrinted(key, depth - 1) +
": " +
prettyPrinted(value[key], depth - 1)
);
}).join(",") + "}";
}
}
// Fall through to original version for all other types.
return origPrettyPrinted(value);
}
// We're not interested in stack traces.
MjsUnitAssertionError = function MjsUnitAssertionError(message) {}
MjsUnitAssertionError.prototype.toString = function () { return ""; };
// Do more printing in assertions for more correctness coverage.
failWithMessage = function failWithMessage(message) {
print(prettyPrinted(message))
}
assertSame = function assertSame(expected, found, name_opt) {
print(prettyPrinted(found));
}
assertNotSame = function assertNotSame(expected, found, name_opt) {
print(prettyPrinted(found));
}
assertEquals = function assertEquals(expected, found, name_opt) {
print(prettyPrinted(found));
}
assertNotEquals = function assertNotEquals(expected, found, name_opt) {
print(prettyPrinted(found));
}
assertNull = function assertNull(value, name_opt) {
print(prettyPrinted(value));
}
assertNotNull = function assertNotNull(value, name_opt) {
print(prettyPrinted(value));
}
// Suppress optimization status as it leads to false positives.
assertUnoptimized = function assertUnoptimized() {}
assertOptimized = function assertOptimized() {}
isNeverOptimize = function isNeverOptimize() {}
isAlwaysOptimize = function isAlwaysOptimize() {}
isInterpreted = function isInterpreted() {}
isBaseline = function isBaseline() {}
isUnoptimized = function isUnoptimized() {}
isOptimized = function isOptimized() {}
isTurboFanned = function isTurboFanned() {}
})();
} catch(e) { }

View File

@ -0,0 +1,674 @@
#!/usr/bin/env python3
# Copyright 2016 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 os
import random
import re
import subprocess
import sys
import textwrap
import unittest
import unittest.mock
from pathlib import Path
import v8_commands
import v8_foozzie
import v8_fuzz_config
import v8_suppressions
BASE_DIR = Path(__file__).parent.resolve()
FOOZZIE = BASE_DIR / 'v8_foozzie.py'
TEST_DATA = BASE_DIR / 'testdata'
KNOWN_BUILDS = [
'd8',
'clang_x86/d8',
'clang_x86_v8_arm/d8',
'clang_x64_v8_arm64/d8',
'clang_x64_fuzzer_experiments/d8',
]
def output(stdout, is_crash):
exit_code = -1 if is_crash else 0
return v8_commands.Output(
exit_code=exit_code, stdout_bytes=stdout.encode('utf-8'), pid=0)
class ConfigTest(unittest.TestCase):
def testExperiments(self):
"""Test integrity of probabilities and configs."""
CONFIGS = v8_foozzie.CONFIGS
EXPERIMENTS = v8_fuzz_config.FOOZZIE_EXPERIMENTS
FLAGS = v8_fuzz_config.ADDITIONAL_FLAGS
# Probabilities add up to 100%.
first_is_int = lambda x: type(x[0]) == int
assert all(map(first_is_int, EXPERIMENTS))
assert sum(x[0] for x in EXPERIMENTS) == 100
# Configs used in experiments are defined.
assert all(map(lambda x: x[1] in CONFIGS, EXPERIMENTS))
assert all(map(lambda x: x[2] in CONFIGS, EXPERIMENTS))
# The last config item points to a known build configuration.
assert all(map(lambda x: x[3] in KNOWN_BUILDS, EXPERIMENTS))
# All flags have a probability.
first_is_float = lambda x: type(x[0]) == float
assert all(map(first_is_float, FLAGS))
first_between_0_and_1 = lambda x: x[0] > 0 and x[0] < 1
assert all(map(first_between_0_and_1, FLAGS))
# Test consistent flags.
second_is_string = lambda x: isinstance(x[1], str)
assert all(map(second_is_string, FLAGS))
# We allow spaces to separate more flags. We don't allow spaces in the flag
# value.
is_flag = lambda x: x.startswith('--')
all_parts_are_flags = lambda x: all(map(is_flag, x[1].split()))
assert all(map(all_parts_are_flags, FLAGS))
def testConfig(self):
"""Smoke test how to choose experiments."""
config = v8_fuzz_config.Config('foo', random.Random(42))
experiments = [
[25, 'ignition', 'jitless', 'd8'],
[75, 'ignition', 'ignition', 'clang_x86/d8'],
]
flags = [
[0.1, '--flag'],
[0.3, '--baz'],
[0.3, '--foo --bar'],
]
self.assertEqual(
[
'--first-config=ignition',
'--second-config=jitless',
'--second-d8=d8',
'--second-config-extra-flags=--baz',
'--second-config-extra-flags=--foo',
'--second-config-extra-flags=--bar',
],
config.choose_foozzie_flags(experiments, flags),
)
self.assertEqual(
[
'--first-config=ignition',
'--second-config=jitless',
'--second-d8=d8',
],
config.choose_foozzie_flags(experiments, flags),
)
class UnitTest(unittest.TestCase):
def testCluster(self):
crash_test_example_path = 'CrashTests/path/to/file.js'
self.assertEqual(
v8_foozzie.ORIGINAL_SOURCE_DEFAULT,
v8_foozzie.cluster_failures(''))
self.assertEqual(
v8_foozzie.ORIGINAL_SOURCE_CRASHTESTS,
v8_foozzie.cluster_failures(crash_test_example_path))
self.assertEqual(
'_o_O_',
v8_foozzie.cluster_failures(
crash_test_example_path,
known_failures={crash_test_example_path: '_o_O_'}))
self.assertEqual(
'98',
v8_foozzie.cluster_failures('v8/test/mjsunit/apply.js'))
def testDiff(self):
def diff_fun(one, two, skip=False):
suppress = v8_suppressions.get_suppression(skip)
return suppress.diff_lines(one.splitlines(), two.splitlines())
smoke = v8_suppressions.SMOKE_TEST_SOURCE
one = ''
two = ''
diff = None, smoke
self.assertEqual(diff, diff_fun(one, two))
one = 'a \n b\nc();'
two = 'a \n b\nc();'
diff = None, smoke
self.assertEqual(diff, diff_fun(one, two))
one = """
Still equal
Extra line
"""
two = """
Still equal
"""
diff = '- Extra line', smoke
self.assertEqual(diff, diff_fun(one, two))
one = """
Still equal
"""
two = """
Still equal
Extra line
"""
diff = '+ Extra line', smoke
self.assertEqual(diff, diff_fun(one, two))
one = """
undefined
somefile.js: TypeError: undefined is not a constructor
"""
two = """
undefined
otherfile.js: TypeError: undefined is not a constructor
"""
diff = """- somefile.js: TypeError: undefined is not a constructor
+ otherfile.js: TypeError: undefined is not a constructor""", smoke
self.assertEqual(diff, diff_fun(one, two))
def testOutputCapping(self):
def check(stdout1, stdout2, is_crash1, is_crash2, capped_lines1,
capped_lines2):
output1 = output(stdout1, is_crash1)
output2 = output(stdout2, is_crash2)
self.assertEqual(
(capped_lines1.encode('utf-8'), capped_lines2.encode('utf-8')),
v8_suppressions.get_output_capped(output1, output2))
# No capping, already equal.
check('1\n2', '1\n2', True, True, '1\n2', '1\n2')
# No crash, no capping.
check('1\n2', '1\n2\n3', False, False, '1\n2', '1\n2\n3')
check('1\n2\n3', '1\n2', False, False, '1\n2\n3', '1\n2')
# Cap smallest if all runs crash.
check('1\n2', '1\n2\n3', True, True, '1\n2', '1\n2')
check('1\n2\n3', '1\n2', True, True, '1\n2', '1\n2')
check('1\n2', '1\n23', True, True, '1\n2', '1\n2')
check('1\n23', '1\n2', True, True, '1\n2', '1\n2')
# Cap the non-crashy run.
check('1\n2\n3', '1\n2', False, True, '1\n2', '1\n2')
check('1\n2', '1\n2\n3', True, False, '1\n2', '1\n2')
check('1\n23', '1\n2', False, True, '1\n2', '1\n2')
check('1\n2', '1\n23', True, False, '1\n2', '1\n2')
# The crashy run has more output.
check('1\n2\n3', '1\n2', True, False, '1\n2\n3', '1\n2')
check('1\n2', '1\n2\n3', False, True, '1\n2', '1\n2\n3')
check('1\n23', '1\n2', True, False, '1\n23', '1\n2')
check('1\n2', '1\n23', False, True, '1\n2', '1\n23')
# Keep output difference when capping.
check('1\n2', '3\n4\n5', True, True, '1\n2', '3\n4')
check('1\n2\n3', '4\n5', True, True, '1\n2', '4\n5')
check('12', '345', True, True, '12', '34')
check('123', '45', True, True, '12', '45')
@unittest.mock.patch(
'v8_suppressions.IGNORE_LINES',
[re.compile('^ign1.*\n'.encode('utf-8'), re.M),
re.compile('^ign2.*\n'.encode('utf-8'), re.M)])
def testIgnoredLines(self):
def check(stdout1, stdout2, is_crash1, is_crash2, diff):
output1 = output(stdout1, is_crash1)
output2 = output(stdout2, is_crash2)
suppress = v8_suppressions.get_suppression()
self.assertEqual(
(diff, v8_suppressions.SMOKE_TEST_SOURCE),
suppress.diff(output1, output2))
# One run has lines to ignore, the other crashes.
check('ign1X\n111\nign2X\n222\n333', '111\n22', False, True, None)
check('111\n22', 'ign1X\n111\nign2X\n222\n333', True, False, None)
# Ignored lines in both runs at different positions.
check('ign1X\n111\n222\n333', '111\nign2X\n22', False, True, None)
check('111\nign2X\n22', 'ign1X\n111\n222\n333', True, False, None)
# Ignored lines at different positions, no crash.
check('ign2X\n111\n\n222', 'ign1X\n111\nign1X\n\n222', False, False, None)
check('ign1X\n111\nign1X\n\n222', 'ign2X\n111\n\n222', False, False, None)
# Ignored lines and a difference, no crash.
check('1\n2\nign2\n3', 'ign1X\n1\nign1\n2', False, False, '- 3')
check('ign1X\n1\nign1\n2', '1\n2\nign2\n3', False, False, '+ 3')
# Ignored lines, a difference and a crash.
check('\n1\n3\nign1X\n4', '\nign2\n1\nign1\n2', False, True, '- 3\n+ 2')
check('\nign2\n1\nign1\n2', '\n1\n3\nign1X\n4', True, False, '- 2\n+ 3')
def testReduceOutput(self):
suppress = v8_suppressions.get_suppression()
proper_test_output = textwrap.dedent(f"""\
Smoke-test output.
{v8_suppressions.SMOKE_TEST_END_TOKEN}
Real-test output.
Some more.""")
# The source is some test. Don't show smoke-test output.
result = suppress.reduced_output(proper_test_output, 'some/file')
expected = textwrap.dedent(f"""\
Real-test output.
Some more.""")
self.assertEqual(expected, result)
# The source is the smoke test. Only show smoke-test output.
result = suppress.reduced_output(
proper_test_output, v8_suppressions.SMOKE_TEST_SOURCE)
self.assertEqual('Smoke-test output.\n', result)
# Smoke-test output is not properly wrapped. Check that we print
# everything.
invalid_test_output = textwrap.dedent(f"""\
Smoke-test output.
Real-test output.
Some more.""")
result = suppress.reduced_output(invalid_test_output, 'some/file')
self.assertEqual(invalid_test_output, result)
@unittest.mock.patch('v8_foozzie.DISALLOWED_FLAGS', ['A'])
@unittest.mock.patch('v8_foozzie.CONTRADICTORY_FLAGS',
[('B', 'C'), ('B', 'D')])
def testFilterFlags(self):
def check(input_flags, expected):
self.assertEqual(expected, v8_foozzie.filter_flags(input_flags))
check([], [])
check(['A'], [])
check(['D', 'A'], ['D'])
check(['A', 'D'], ['D'])
check(['C', 'D'], ['C', 'D'])
check(['E', 'C', 'D', 'F'], ['E', 'C', 'D', 'F'])
check(['B', 'D'], ['D'])
check(['D', 'B'], ['B'])
check(['C', 'B', 'D'], ['C', 'D'])
check(['E', 'C', 'A', 'F', 'B', 'G', 'D'], ['E', 'C', 'F', 'G', 'D'])
@unittest.mock.patch('v8_foozzie.DISALLOWED_FLAG_PREFIXES', ['A', 'B='])
def testFilterFlagPrefixes(self):
def check(input_flags, expected):
self.assertEqual(expected, v8_foozzie.filter_flags(input_flags))
check([], [])
check(['A'], [])
check(['D', 'A1'], ['D'])
check(['A1', 'D'], ['D'])
check(['B=42', 'D'], ['D'])
check(['D', 'B=42'], ['D'])
check(['A', 'B', 'C=42', 'D', 'B=-1'], ['B', 'C=42', 'D'])
def _test_content(self, filename):
with (TEST_DATA / filename).open() as f:
return f.read()
def _create_execution_configs(self, *extra_flags, **kwargs):
"""Create three execution configs as in production with a fake config
called `special`.
"""
# If we need the configs to be cross-arch with same-arch fallbacks,
# we use build3 (x86) otherwise we compare with build1 (x64).
build = 'build3' if kwargs.pop('cross_arch', True) else 'build1'
argv = create_test_cmd_line(build, 'special', 'fuzz-123.js',
*extra_flags)
options = v8_foozzie.parse_args(argv[2:])
return v8_foozzie.create_execution_configs(options)
@unittest.mock.patch('v8_suppressions.DROP_FLAGS_ON_CONTENT',
[('--bat', r'\%DontUseThat\(|\%DontUseThis\(')])
@unittest.mock.patch(
'v8_foozzie.CONFIGS', {
'ignition': ['--foo', '--baz'],
'default': ['--bar', '--baz'],
'special': ['--bat'],
})
def testAdjustConfigsByContent_Matches1(self):
suppress = v8_suppressions.get_suppression()
content = self._test_content('fuzz-123.js')
configs = self._create_execution_configs()
logs = suppress.adjust_configs_by_content(configs, content)
self.assertEqual(
['Dropped second config using --bat based on content rule.'], logs)
self.assertEqual(2, len(configs))
self.assertEqual(['--foo', '--baz'], configs[0].config_flags)
self.assertEqual(['--bar', '--baz'], configs[1].config_flags)
configs = self._create_execution_configs('--first-config-extra-flags=--bat')
logs = suppress.adjust_configs_by_content(configs, content)
expected_logs = [
'Dropped --bat from first config based on content rule.',
'Dropped second config using --bat based on content rule.',
]
self.assertEqual(expected_logs, logs)
self.assertEqual(2, len(configs))
self.assertEqual(['--foo', '--baz'], configs[0].config_flags)
self.assertEqual(['--bar', '--baz'], configs[1].config_flags)
@unittest.mock.patch('v8_suppressions.DROP_FLAGS_ON_CONTENT',
[('--baz', r'\%DontUseThat\(|\%DontUseThis\(')])
@unittest.mock.patch(
'v8_foozzie.CONFIGS', {
'ignition': ['--foo', '--baz'],
'default': ['--bar', '--baz'],
'special': ['--bat'],
})
def testAdjustConfigsByContent_Matches2(self):
suppress = v8_suppressions.get_suppression()
content = self._test_content('fuzz-123.js')
configs = self._create_execution_configs()
logs = suppress.adjust_configs_by_content(configs, content)
expected_logs = [
'Dropped --baz from first config based on content rule.',
'Dropped --baz from default config based on content rule.',
]
self.assertEqual(expected_logs, logs)
self.assertEqual(3, len(configs))
self.assertEqual(['--foo'], configs[0].config_flags)
self.assertEqual(['--bar'], configs[1].config_flags)
self.assertEqual(['--bar'], configs[1].fallback.config_flags)
self.assertEqual(['--bat'], configs[2].config_flags)
self.assertEqual(['--bat'], configs[2].fallback.config_flags)
configs = self._create_execution_configs(
'--second-config-extra-flags=--baz')
logs = suppress.adjust_configs_by_content(configs, content)
expected_logs = [
'Dropped --baz from first config based on content rule.',
'Dropped --baz from default config based on content rule.',
'Dropped second config using --baz based on content rule.',
]
self.assertEqual(expected_logs, logs)
self.assertEqual(2, len(configs))
self.assertEqual(['--foo'], configs[0].config_flags)
self.assertEqual(['--bar'], configs[1].config_flags)
self.assertEqual(['--bar'], configs[1].fallback.config_flags)
@unittest.mock.patch('v8_suppressions.DROP_FLAGS_ON_CONTENT',
[('--baz', r'\%UnusedFun\(')])
@unittest.mock.patch('v8_foozzie.CONFIGS', {
'ignition': ['--foo', '--baz'],
'default': ['--bar'],
'special': ['--bat'],
})
def testAdjustConfigsByContent_DoesntMatch(self):
suppress = v8_suppressions.get_suppression()
content = self._test_content('fuzz-123.js')
configs = self._create_execution_configs(
'--second-config-extra-flags=--baz')
logs = suppress.adjust_configs_by_content(configs, content)
self.assertEqual([], logs)
self.assertEqual(3, len(configs))
self.assertEqual(['--foo', '--baz'], configs[0].config_flags)
self.assertEqual(['--bar', '--baz'], configs[1].config_flags)
self.assertEqual(['--bar', '--baz'], configs[1].fallback.config_flags)
self.assertEqual(['--bat', '--baz'], configs[2].config_flags)
self.assertEqual(['--bat', '--baz'], configs[2].fallback.config_flags)
@unittest.mock.patch(
'v8_foozzie.CONFIGS', {
'ignition': ['--foo'],
'default': [],
'special': ['--bar'],
})
def testAdjustConfigsByOutput_Matches(self):
"""Test scenarios where the directive to avoid cross-arch comparison is in
the output.
"""
suppress = v8_suppressions.get_suppression()
matching_output = textwrap.dedent("""\
Some lines...
Indentation doesn't matter.
Warning: This run cannot be compared across architectures.
That's it.""")
# Scenario 1: We have a match, but the comparisons are in the same
# architecture. So there's nothing to do.
configs = self._create_execution_configs(cross_arch=False)
baseline_config = configs[0]
remaining_configs = configs[1:]
logs = suppress.adjust_configs_by_output(
remaining_configs, matching_output)
self.assertEqual([], logs)
self.assertEqual(2, len(remaining_configs))
for config in remaining_configs:
self.assertEqual(baseline_config.arch, config.arch)
self.assertIsNone(config.fallback)
# Scenario 2: We have a match and compare cross-arch. Ensure the
# adjustments turns this into a same-arch comparison.
configs = self._create_execution_configs()
baseline_config = configs[0]
remaining_configs = configs[1:]
logs = suppress.adjust_configs_by_output(
remaining_configs, matching_output)
expected_logs = [
'Running the default config on the same architecture.',
'Running the second config on the same architecture.'
]
self.assertEqual(expected_logs, logs)
self.assertEqual(2, len(remaining_configs))
for config in remaining_configs:
self.assertEqual(baseline_config.arch, config.arch)
self.assertIsNone(config.fallback)
@unittest.mock.patch(
'v8_foozzie.CONFIGS', {
'ignition': ['--foo'],
'default': [],
'special': ['--bar'],
})
def testAdjustConfigsByOutput_DoesntMatch(self):
"""Test that cross-arch comparisons stay untouched if the directive
from above is not in the output.
"""
suppress = v8_suppressions.get_suppression()
non_matching_output = textwrap.dedent("""\
Some lines...
Indentation doesn't matter.
That's it.""")
configs = self._create_execution_configs(cross_arch=True)
baseline_config = configs[0]
remaining_configs = configs[1:]
logs = suppress.adjust_configs_by_output(
remaining_configs, non_matching_output)
self.assertEqual([], logs)
self.assertEqual(2, len(remaining_configs))
for config in remaining_configs:
self.assertNotEqual(baseline_config.arch, config.arch)
self.assertEqual(baseline_config.arch, config.fallback.arch)
def cut_verbose_output(stdout, n_comp):
# This removes the first lines containing d8 commands of `n_comp` comparison
# runs.
return '\n'.join(stdout.split('\n')[n_comp * 2:])
def create_test_cmd_line(second_d8_dir, second_config, filename, *extra_flags):
return list(
map(str, [
sys.executable,
FOOZZIE,
'--random-seed',
'12345',
'--first-d8',
TEST_DATA / 'baseline' / 'd8.py',
'--second-d8',
TEST_DATA / second_d8_dir / 'd8.py',
'--first-config',
'ignition',
'--second-config',
second_config,
TEST_DATA / filename,
] + list(extra_flags)))
def run_foozzie(second_d8_dir, *extra_flags, **kwargs):
filename = kwargs.pop('filename', 'fuzz-123.js')
second_config = kwargs.pop('second_config', 'ignition_turbo')
cmd = create_test_cmd_line(second_d8_dir, second_config, filename,
*extra_flags)
return subprocess.check_output(cmd, text=True, **kwargs)
class SystemTest(unittest.TestCase):
"""This tests the whole correctness-fuzzing harness with fake build
artifacts.
Overview of fakes:
baseline: Example foozzie output.
build1: No difference to baseline but ignored lines.
build2: Output difference causing the script to fail.
build3: As build1 but with an architecture difference as well.
"""
def assert_expected(self, file_name, expected):
if os.environ.get('GENERATE'):
with (TEST_DATA / file_name).open('w') as f:
f.write(expected)
with (TEST_DATA / file_name).open() as f:
self.assertEqual(f.read(), expected)
def testPass(self):
stdout = run_foozzie('build1')
self.assertEqual('# V8 correctness - pass\n',
cut_verbose_output(stdout, 3))
# Default comparison includes suppressions.
self.assertIn('v8_suppressions.js', stdout)
# Default comparison doesn't include any specific mock files.
self.assertNotIn('v8_mock_archs.js', stdout)
self.assertNotIn('v8_mock_webassembly.js', stdout)
def _testDifferentOutputFail(self, expected_path, *args):
with self.assertRaises(subprocess.CalledProcessError) as ctx:
run_foozzie('build2',
'--first-config-extra-flags=--flag1',
'--first-config-extra-flags=--flag2=0',
'--second-config-extra-flags=--flag3', *args)
e = ctx.exception
self.assertEqual(v8_foozzie.RETURN_FAIL, e.returncode)
self.assert_expected(expected_path, cut_verbose_output(e.output, 2))
def testDifferentOutputFail(self):
self._testDifferentOutputFail('failure_output.txt')
def testSmokeTest_Fails(self):
with self.assertRaises(subprocess.CalledProcessError) as ctx:
run_foozzie('build4')
e = ctx.exception
self.assertEqual(v8_foozzie.RETURN_FAIL, e.returncode)
self.assert_expected(
'smoke_test_output.txt', cut_verbose_output(e.output, 2))
def testSmokeTest_Crashes(self):
with self.assertRaises(subprocess.CalledProcessError) as ctx:
run_foozzie('build4',
'--second-config-extra-flags=--crash-the-smoke-test')
e = ctx.exception
self.assertEqual(v8_foozzie.RETURN_FAIL, e.returncode)
self.assert_expected(
'smoke_test_crash_output.txt', cut_verbose_output(e.output, 2))
def testSimulatedCrash(self):
with self.assertRaises(subprocess.CalledProcessError) as ctx:
run_foozzie('build5', '--second-config-extra-flags=--simulate-errors')
e = ctx.exception
self.assertEqual(v8_foozzie.RETURN_FAIL, e.returncode)
self.assert_expected(
'simulated_crash_output.txt', cut_verbose_output(e.output, 2))
def testDifferentArch(self):
"""Test that the architecture-specific mocks are passed to both runs when
we use executables with different architectures.
"""
# Build 3 simulates x86, while the baseline is x64.
stdout = run_foozzie('build3')
lines = stdout.split('\n')
# TODO(machenbach): Don't depend on the command-lines being printed in
# particular lines.
self.assertIn('v8_mock_archs.js', lines[1])
self.assertIn('v8_mock_archs.js', lines[3])
def testDifferentArchFailFirst(self):
"""Test that we re-test against x64. This tests the path that also fails
on x64 and then reports the error as x64.
"""
# Build 3 simulates x86 and produces a difference on --bad-flag, but
# the baseline build shows the same difference when --bad-flag is passed.
with self.assertRaises(subprocess.CalledProcessError) as ctx:
run_foozzie('build3', '--second-config-extra-flags=--bad-flag')
e = ctx.exception
self.assertEqual(v8_foozzie.RETURN_FAIL, e.returncode)
self.assert_expected(
'failure_output_arch.txt', cut_verbose_output(e.output, 3))
def testDifferentArchFailSecond(self):
"""As above, but we test the path that only fails in the second (ia32)
run and not with x64 and then reports the error as ia32.
"""
# Build 3 simulates x86 and produces a difference on --very-bad-flag,
# which the baseline build doesn't.
with self.assertRaises(subprocess.CalledProcessError) as ctx:
run_foozzie('build3', '--second-config-extra-flags=--very-bad-flag')
e = ctx.exception
self.assertEqual(v8_foozzie.RETURN_FAIL, e.returncode)
self.assert_expected(
'failure_output_second.txt', cut_verbose_output(e.output, 3))
def testJitless(self):
"""Test that webassembly is mocked out when comparing with jitless."""
stdout = run_foozzie(
'build1', second_config='jitless')
lines = stdout.split('\n')
# TODO(machenbach): Don't depend on the command-lines being printed in
# particular lines.
self.assertIn('v8_mock_webassembly.js', lines[1])
self.assertIn('v8_mock_webassembly.js', lines[3])
def testJitlessAndWasmStruct_FlagPassed(self):
"""We keep passing the --jitless flag when no content rule matches.
The flag passed to one run of build3 causes an output difference.
"""
with self.assertRaises(subprocess.CalledProcessError) as ctx:
run_foozzie('build3', second_config='jitless')
self.assertIn('jitless flag passed', ctx.exception.stdout)
self.assertNotIn('Adjusted flags and experiments based on the test case',
ctx.exception.stdout)
def testJitlessAndWasmStruct_FlagDropped(self):
"""We drop the --jitless flag when the content rule matches."""
stdout = run_foozzie(
'build3',
second_config='jitless',
filename='fuzz-wasm-struct-123.js')
self.assertIn('Adjusted flags and experiments based on the test case',
stdout)
self.assertIn(
'Dropped second config using --jitless based on content rule.', stdout)
def testAvoidCrossArchComparison(self):
"""We turn a cross-arch into a same-arch comparison if a directive is in
the baseline output.
"""
stdout = run_foozzie(
'build3',
'--first-config-extra-flags=--avoid-cross-arch',
second_config='ignition_turbo_opt',
filename='fuzz-123.js')
self.assertIn('# Adjusted experiments based on baseline output', stdout)
self.assertIn(
'Running the default config on the same architecture.', stdout)
self.assertIn(
'Running the second config on the same architecture.', stdout)
def testSkipSuppressions(self):
"""Test that the suppressions file is not passed when skipping
suppressions.
"""
# Compare baseline with baseline. This passes as there is no difference.
stdout = run_foozzie('baseline', '--skip-suppressions')
self.assertNotIn('v8_suppressions.js', stdout)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,65 @@
# Copyright 2018 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 json
import random
from pathlib import Path
THIS_DIR = Path(__file__).parent.resolve()
# List of configuration experiments for correctness fuzzing.
# List of <probability>, <1st config name>, <2nd config name>, <2nd d8>.
# Probabilities must add up to 100.
with (THIS_DIR / 'v8_fuzz_experiments.json').open() as f:
FOOZZIE_EXPERIMENTS = json.load(f)
# Additional flag experiments. List of tuples like
# (<likelihood to use flags in [0,1)>, <flag>).
with (THIS_DIR / 'v8_fuzz_flags.json').open() as f:
ADDITIONAL_FLAGS = json.load(f)
class Config(object):
def __init__(self, name, rng=None):
"""
Args:
name: Name of the used fuzzer.
rng: Random number generator for generating experiments.
random_seed: Random-seed used for d8 throughout one fuzz session.
"""
self.name = name
self.rng = rng or random.Random()
def choose_foozzie_flags(self, foozzie_experiments=None, additional_flags=None):
"""Randomly chooses a configuration from FOOZZIE_EXPERIMENTS.
Args:
foozzie_experiments: Override experiment config for testing.
additional_flags: Override additional flags for testing.
Returns: List of flags to pass to v8_foozzie.py fuzz harness.
"""
foozzie_experiments = foozzie_experiments or FOOZZIE_EXPERIMENTS
additional_flags = additional_flags or ADDITIONAL_FLAGS
# Add additional flags to second config based on experiment percentages.
extra_flags = []
for p, flags in additional_flags:
if self.rng.random() < p:
for flag in flags.split():
extra_flags.append(f'--second-config-extra-flags={flag}')
# Calculate flags determining the experiment.
acc = 0
threshold = self.rng.random() * 100
for prob, first_config, second_config, second_d8 in foozzie_experiments:
acc += prob
if acc > threshold:
return [
f'--first-config={first_config}',
f'--second-config={second_config}',
f'--second-d8={second_d8}',
] + extra_flags
assert False

View File

@ -0,0 +1,16 @@
[
[10, "ignition", "jitless", "d8"],
[10, "ignition", "slow_path", "d8"],
[5, "ignition_no_ic", "slow_path", "d8"],
[5, "ignition", "slow_path_opt", "d8"],
[5, "ignition", "ignition_turbo_no_ic", "d8"],
[15, "ignition", "ignition_turbo_opt", "d8"],
[5, "ignition_no_ic", "ignition_turbo_opt", "d8"],
[15, "ignition", "ignition_maglev", "d8"],
[5, "ignition_turbo", "ignition_turbo_opt", "clang_x64_fuzzer_experiments/d8"],
[5, "ignition", "ignition", "clang_x86/d8"],
[5, "ignition", "ignition_turbo_opt", "clang_x86/d8"],
[5, "ignition", "slow_path", "clang_x86/d8"],
[5, "ignition", "ignition_turbo_opt", "clang_x64_v8_arm64/d8"],
[5, "ignition", "ignition_turbo_opt", "clang_x86_v8_arm/d8"]
]

View File

@ -0,0 +1,41 @@
[
[0.05, "--stress-compaction-random"],
[0.05, "--stress-delay-tasks"],
[0.05, "--stress-marking=100"],
[0.05, "--stress-scavenge=100"],
[0.05, "--random-gc-interval=2000"],
[0.2, "--noanalyze-environment-liveness"],
[0.01, "--thread-pool-size=1"],
[0.01, "--thread-pool-size=2"],
[0.01, "--thread-pool-size=4"],
[0.01, "--thread-pool-size=8"],
[0.25, "--jit-fuzzing"],
[0.25, "--future"],
[0.1, "--regexp-interpret-all"],
[0.1, "--regexp-tier-up-ticks=0"],
[0.1, "--regexp-tier-up-ticks=10"],
[0.1, "--regexp-tier-up-ticks=100"],
[0.1, "--turbo-instruction-scheduling"],
[0.1, "--turbo-stress-instruction-scheduling"],
[0.1, "--no-enable-sse4_2"],
[0.1, "--no-enable-sahf"],
[0.1, "--no-enable-avx"],
[0.1, "--no-enable-fma3"],
[0.1, "--no-enable-bmi1"],
[0.1, "--no-enable-bmi2"],
[0.1, "--no-enable-lzcnt"],
[0.1, "--no-enable-popcnt"],
[0.25, "--no-lazy-feedback-allocation"],
[0.1, "--no-wasm-generic-wrapper"],
[0.2, "--turboshaft"],
[0.05, "--turbolev"],
[0.5, "--optimize-on-next-call-optimizes-to-maglev"],
[0.0001, "--simulate-errors"],
[0.1, "--minor-ms"],
[0.1, "--shared-string-table"],
[0.1, "--shared-heap"],
[0.1, "--stress-wasm-memory-moving"],
[0.1, "--stress-scavenger-conservative-object-pinning-random"],
[0.1, "--precise-object-pinning"],
[0.1, "--ephemeron-fixpoint-iterations=0"]
]

View File

@ -0,0 +1,291 @@
// Copyright 2016 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.
// This is intended for permanent JS behavior changes for mocking out
// non-deterministic behavior. For temporary suppressions, please refer to
// v8_suppressions.js.
// This file is loaded before each correctness test cases and won't get
// minimized.
// This will be overridden in the test cases. The override can be minimized.
var prettyPrinted = function prettyPrinted(msg) { return msg; };
// Mock Math.random.
(function() {
let index = 1;
Math.random = function() {
const x = Math.sin(index++) * 10000;
return x - Math.floor(x);
}
})();
// Mock Date.
(function() {
let index = 0;
let mockDate = 1477662728696;
const mockDateNow = function() {
index = (index + 1) % 10;
mockDate = mockDate + index + 1;
return mockDate;
}
const origDate = Date;
const construct = Reflect.construct;
const constructDate = function(args) {
let result;
if (args.length) {
result = construct(origDate, args);
} else {
result = new origDate(mockDateNow());
}
result.constructor = function(...args) { return constructDate(args); }
Object.defineProperty(
result, "constructor", { configurable: false, writable: false });
return result;
}
origDate.prototype.constructor = function(...args) {
return constructDate(args);
};
var handler = {
apply: function(target, thisArg, args) {
return constructDate(args);
},
construct: function(target, args, newTarget) {
return constructDate(args);
},
get: function(target, property, receiver) {
if (property == "now") {
return mockDateNow;
}
if (property == "prototype") {
return origDate.prototype;
}
},
}
Date = new Proxy(Date, handler);
})();
// Mock performance methods.
performance.now = function() { return 1.2; };
performance.mark = function() { return undefined; };
performance.measure = function() { return undefined; };
performance.measureMemory = function() { return []; };
// Mock readline so that test cases don't hang.
readline = function() { return "foo"; };
// Mock stack traces.
Error.prepareStackTrace = function(error, structuredStackTrace) {
return "";
};
Object.defineProperty(
Error, 'prepareStackTrace', { configurable: false, writable: false });
// Mock buffer access in float typed arrays because of varying NaN patterns.
(function() {
const origArrayFrom = Array.from;
const origArrayIsArray = Array.isArray;
const origFunctionPrototype = Function.prototype;
const origArrayMap = Array.prototype.map;
const applyOrigArrayMap = origFunctionPrototype.apply.bind(origArrayMap);
const origIsNaN = isNaN;
const origIterator = Symbol.iterator;
const deNaNify = function(value) { return origIsNaN(value) ? 1 : value; };
const mock = function(type) {
// Remove NaN values from parameters to "set" function.
const set = type.prototype.set;
type.prototype.set = function(array, offset) {
if (origArrayIsArray(array)) {
array = applyOrigArrayMap(array, [deNaNify]);
}
set.apply(this, [array, offset]);
};
const handler = {
// Remove NaN values from parameters to constructor.
construct: function(target, args) {
for (let i = 0; i < args.length; i++) {
if (args[i] != null &&
typeof args[i][origIterator] === 'function') {
// Consume iterators.
args[i] = origArrayFrom(args[i]);
}
if (origArrayIsArray(args[i])) {
args[i] = applyOrigArrayMap(args[i], [deNaNify]);
}
}
const obj = new (
origFunctionPrototype.bind.call(type, null, ...args));
return new Proxy(obj, {
get: function(x, prop) {
if (typeof x[prop] == "function")
return x[prop].bind(obj);
return x[prop];
},
// Remove NaN values that get assigned.
set: function(target, prop, value, receiver) {
target[prop] = deNaNify(value);
return value;
}
});
},
};
return new Proxy(type, handler);
}
Float32Array = mock(Float32Array);
Float64Array = mock(Float64Array);
})();
// Mock buffer access via DataViews because of varying NaN patterns.
(function() {
const origIsNaN = isNaN;
const deNaNify = function(value) { return origIsNaN(value) ? 1 : value; };
const origSetFloat32 = DataView.prototype.setFloat32;
DataView.prototype.setFloat32 = function(offset, value, ...rest) {
origSetFloat32.call(this, offset, deNaNify(value), ...rest);
};
const origSetFloat64 = DataView.prototype.setFloat64;
DataView.prototype.setFloat64 = function(offset, value, ...rest) {
origSetFloat64.call(this, offset, deNaNify(value), ...rest);
};
})();
// Mock maximum typed-array buffer and limit to 1MiB. Otherwise we might
// get range errors. We ignore those by crashing, but that reduces coverage,
// hence, let's reduce the range-error rate.
(function() {
// Math.min might be manipulated in test cases.
const min = Math.min;
const maxBytes = 1048576;
const mock = function(type) {
const maxLength = maxBytes / (type.BYTES_PER_ELEMENT || 1);
const handler = {
construct: function(target, args) {
if (args[0] && typeof args[0] != "object") {
// Length used as first argument.
args[0] = min(maxLength, Number(args[0]));
} else if (args[0] instanceof ArrayBuffer && args.length > 1) {
// Buffer used as first argument.
const buffer = args[0];
args[1] = Number(args[1]);
// Ensure offset is multiple of bytes per element.
args[1] = args[1] - (args[1] % type.BYTES_PER_ELEMENT);
// Limit offset to length of buffer.
args[1] = min(args[1], buffer.byteLength || 0);
if (args.length > 2) {
// If also length is given, limit it to the maximum that's possible
// given buffer and offset. Avoid NaN offset turning the length
// NaN, too.
const maxBytesLeft = buffer.byteLength - (args[1] || 0);
const maxLengthLeft = maxBytesLeft / type.BYTES_PER_ELEMENT;
args[2] = min(Number(args[2]), maxLengthLeft);
}
}
return new (Function.prototype.bind.apply(type, [null].concat(args)));
},
};
return new Proxy(type, handler);
}
ArrayBuffer = mock(ArrayBuffer);
SharedArrayBuffer = mock(SharedArrayBuffer);
Int8Array = mock(Int8Array);
Uint8Array = mock(Uint8Array);
Uint8ClampedArray = mock(Uint8ClampedArray);
Int16Array = mock(Int16Array);
Uint16Array = mock(Uint16Array);
Int32Array = mock(Int32Array);
Uint32Array = mock(Uint32Array);
BigInt64Array = mock(BigInt64Array);
BigUint64Array = mock(BigUint64Array);
Float32Array = mock(Float32Array);
Float64Array = mock(Float64Array);
})();
// Mock typed array set function and cap offset to not throw a range error.
(function() {
// Math.min might be manipulated in test cases.
const min = Math.min;
const types = [
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
BigInt64Array,
BigUint64Array,
Float32Array,
Float64Array,
];
for (const type of types) {
const set = type.prototype.set;
type.prototype.set = function(array, offset) {
if (Array.isArray(array)) {
offset = Number(offset);
offset = min(offset, this.length - array.length);
}
set.call(this, array, offset);
};
}
})();
// Mock Worker.
(function() {
let index = 0;
// TODO(machenbach): Randomize this for each test case, but keep stable
// during comparison. Also data and random above.
const workerMessages = [
undefined, 0, -1, "", "foo", 42, [], {}, [0], {"x": 0}
];
Worker = function(code){
try {
print(prettyPrinted(eval(code)));
} catch(e) {
print(prettyPrinted(e));
}
this.getMessage = function(){
index = (index + 1) % 10;
return workerMessages[index];
}
this.postMessage = function(msg){
print(prettyPrinted(msg));
}
};
})();
// Mock Realm.
Realm.eval = function(realm, code) { return eval(code) };
// Mock the nondeterministic parts of WeakRef and FinalizationRegistry.
WeakRef.prototype.deref = function() { };
FinalizationRegistry = function(callback) { };
FinalizationRegistry.prototype.register = function(target, holdings) { };
FinalizationRegistry.prototype.unregister = function(unregisterToken) { };
FinalizationRegistry.prototype.cleanupSome = function() { };
FinalizationRegistry.prototype[Symbol.toStringTag] = "FinalizationRegistry";
// Mock the nondeterministic Atomics.waitAsync.
Atomics.waitAsync = function() {
// Return a mock "Promise" whose "then" function will call the callback
// immediately.
return {'value': {'then': function (f) { f(); }}};
}
// Mock serializer API with no-ops.
d8.serializer = {'serialize': (x) => x, 'deserialize': (x) => x}
// Mock profiler API with no-ops.
d8.profiler = {'setOnProfileEndListener': (x) => x, 'triggerSample': (x) => x}
// Mock logging API with no-ops.
d8.log = {'getAndStop': (x) => x}

View File

@ -0,0 +1,25 @@
// Copyright 2017 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.
// This is intended for permanent JS behavior changes for mocking out
// non-deterministic behavior. For temporary suppressions, please refer to
// v8_suppressions.js.
// This mocks only architecture specific differences. Refer to v8_mocks.js
// for the general case.
// This file is loaded before each correctness test cases and won't get
// minimized.
// Mock Math.pow due to precision differences between 32 and 64 bits.
// https://crbug.com/380147861
// https://crbug.com/380322452
// https://crbug.com/381129314
(function() {
const origMathPow = Math.pow;
const origNumber = Number;
const origToExponential = Number.prototype.toExponential;
Math.pow = function(a, b) {
let result = origMathPow(a, b);
return origNumber(origToExponential.call(result, 11));
}
})();

View File

@ -0,0 +1,18 @@
// Copyright 2020 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.
// This mocks out the WebAssembly object with a permissive dummy.
(function() {
const handler = {
get: function(x, prop) {
if (prop == Symbol.toPrimitive) {
return function() { return undefined; };
}
return dummy;
},
};
const dummy = new Proxy(function() { return dummy; }, handler);
WebAssembly = dummy;
})();

View File

@ -0,0 +1,55 @@
// Copyright 2019 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.
// This file is executed separately before the correctness test case. Add here
// checking of global properties that should never differ in any configuration.
// A difference found in the prints below will prevent any further correctness
// comparison for the selected configurations to avoid flooding bugs.
print("https://crbug.com/932656");
print(Object.getOwnPropertyNames(this));
print("https://crbug.com/935800");
(function () {
function foo() {
"use asm";
function baz() {}
return {bar: baz};
}
print(Object.getOwnPropertyNames(foo().bar));
})();
print("https://crbug.com/985154");
(function () {
"use strict";
function foo() {
"use asm";
function baz() {}
return {bar: baz};
}
print(Object.getOwnPropertyNames(foo().bar));
})();
print("Sensitive runtime functions are neutered");
(function () {
function foo() {}
%PrepareFunctionForOptimization(foo);
foo();
foo();
%OptimizeFunctionOnNextCall(foo);
foo();
print(%GetOptimizationStatus(foo));
const fun = new Function("f", "return %GetOptimizationStatus(f);");
print(fun(foo));
})();
print("Same constants across builds");
(function () {
for (let [k, v] of Object.entries(this.d8.constants)) {
print(`${k}:${v}`)
}
})();
// Don't change this line, it's used to separate smoketest output.
print("___foozzie___smoke_test_end___");

View File

@ -0,0 +1,8 @@
// Copyright 2016 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.
// This file is loaded before each correctness test case and after v8_mock.js.
// You can temporarily change JS behavior here to silence known problems.
// Please refer to a bug in a comment and remove the suppression once the
// problem is fixed.

View File

@ -0,0 +1,290 @@
# Copyright 2016 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.
"""
Suppressions for V8 correctness fuzzer failures.
We support three types of suppressions:
1. Ignore test case by pattern.
Map a regular expression to a bug entry. A new failure will be reported
when the pattern matches a JS test case.
Subsequent matches will be recoreded under the first failure.
2. Ignore test run by output pattern:
Map a regular expression to a bug entry. A new failure will be reported
when the pattern matches the output of a particular run.
Subsequent matches will be recoreded under the first failure.
3. Relax line-to-line comparisons with expressions of lines to ignore and
lines to be normalized (i.e. ignore only portions of lines).
These are not tied to bugs, be careful to not silently switch off this tool!
Alternatively, think about adding a behavior change to v8_suppressions.js
to silence a particular class of problems.
"""
import re
from itertools import zip_longest
# Max line length for regular experessions checking for lines to ignore.
MAX_LINE_LENGTH = 512
# Ignore by output pattern. Map from bug->regexp like above.
IGNORE_OUTPUT = {
'_fake_difference_':
re.compile(r'^.*___fake_difference___$', re.M),
}
# Avoid cross-arch comparisons if we find this string in the output.
# We also need to ignore this output in the diffs as this string might
# only be printed in the baseline run.
AVOID_CROSS_ARCH_COMPARISON_RE = (
r'^Warning: This run cannot be compared across architectures\.$')
# Lines matching any of the following regular expressions will be ignored.
# Use uncompiled regular expressions - they'll be compiled later.
# All expressions must end with a $ without linebreak.
IGNORE_LINES = [
r'^Warning: .+ is deprecated.*$',
r'^Try --help for options$',
AVOID_CROSS_ARCH_COMPARISON_RE,
]
# List of pairs (<flag string>, <regexp string>). If the regexp matches the
# test content, the flag is dropped from the command line of any run.
# If the flag is part of the third run, the entire run is dropped, as it
# in many cases would just be redundant to the second default run.
DROP_FLAGS_ON_CONTENT = [
('--jitless', r'\%Wasm(Struct|Array|GenerateRandomModule)\('),
]
###############################################################################
# Implementation - you should not need to change anything below this point.
# We'll match the linebreak explicitly to remove it as well.
assert all(exp.endswith('$') for exp in IGNORE_LINES)
assert not any(exp.endswith('\n$') for exp in IGNORE_LINES)
IGNORE_LINES = [exp[:-1] + '\n' for exp in IGNORE_LINES]
# Compile regular expressions.
IGNORE_LINES = [re.compile(exp.encode('utf-8'), re.M) for exp in IGNORE_LINES]
ORIGINAL_SOURCE_PREFIX = 'v8-foozzie source: '
SMOKE_TEST_SOURCE = 'foozzie smoke test'
SMOKE_TEST_END_TOKEN = '___foozzie___smoke_test_end___'
SMOKE_TEST_OUTPUT_RE = f'(.*?){SMOKE_TEST_END_TOKEN}'
SMOKE_TEST_INVERSE_OUTPUT_RE = f'.*?{SMOKE_TEST_END_TOKEN}\\s(.*)'
def get_output_capped(output1, output2):
"""Returns a pair of stdout byte arrays.
The arrays are safely capped if at least one run has crashed.
"""
# No length difference or no crash -> no capping.
if (len(output1.stdout_bytes) == len(output2.stdout_bytes) or
(not output1.HasCrashed() and not output2.HasCrashed())):
return output1.stdout_bytes, output2.stdout_bytes
# Both runs have crashed, cap by the shorter output.
if output1.HasCrashed() and output2.HasCrashed():
cap = min(len(output1.stdout_bytes), len(output2.stdout_bytes))
# Only the first run has crashed, cap by its output length.
elif output1.HasCrashed():
cap = len(output1.stdout_bytes)
# Similar if only the second run has crashed.
else:
cap = len(output2.stdout_bytes)
return output1.stdout_bytes[0:cap], output2.stdout_bytes[0:cap]
def short_line_output(line):
if len(line) <= MAX_LINE_LENGTH:
# Avoid copying.
return line
return line[0:MAX_LINE_LENGTH] + '...'
def diff_output(lines1, lines2):
"""Returns a tuple (difference, source).
The difference is None if there's no difference, otherwise a string
with a readable diff.
The source is the last source output within the test case, or None if no
such output existed.
"""
# This keeps track where we are in the original source file of the fuzz
# test case. We always start with the smoke-test part.
source = SMOKE_TEST_SOURCE
for line1, line2 in zip_longest(lines1, lines2, fillvalue=None):
# Only one of the two iterators should run out.
assert not (line1 is None and line2 is None)
# One iterator ends earlier.
if line1 is None:
return f'+ {short_line_output(line2)}', source
if line2 is None:
return f'- {short_line_output(line1)}', source
# Lines are equal.
if line1 == line2:
# Instrumented original-source-file output must be equal in both
# versions. It only makes sense to update it here when both lines
# are equal.
if source == SMOKE_TEST_SOURCE and line1 == SMOKE_TEST_END_TOKEN:
source = None
elif line1.startswith(ORIGINAL_SOURCE_PREFIX):
source = line1[len(ORIGINAL_SOURCE_PREFIX):]
continue
# Lines are different.
short_line1 = short_line_output(line1)
short_line2 = short_line_output(line2)
return f'- {short_line1}\n+ {short_line2}', source
# No difference found.
return None, source
def get_suppression(skip=False):
return V8Suppression(skip)
def decode(output):
try:
return output.decode('utf-8')
except UnicodeDecodeError:
return output.decode('latin-1')
def ignore_lines(text):
result = text
for line_re in IGNORE_LINES:
result = line_re.sub(b'', result)
return result
class V8Suppression(object):
def __init__(self, skip):
if skip:
self.ignore_output = {}
self.drop_flags_on_content = []
else:
self.ignore_output = IGNORE_OUTPUT
self.drop_flags_on_content = DROP_FLAGS_ON_CONTENT
def diff(self, output1, output2):
"""Diff capped lines in the presence of crashes."""
# We remove ignored lines in the raw bytes before output capping, as such
# a line might be present in the non-crash run. The crashy run however
# determines the byte length and might cap the to-be-ignored line right
# in the middle otherwise (https://crbug.com/381384408).
output1.stdout_bytes = ignore_lines(output1.stdout_bytes)
output2.stdout_bytes = ignore_lines(output2.stdout_bytes)
# We need to cap raw bytes before decoding, since bytes in the capped part
# could lead to decoding errors and an encoding change of the bytes in the
# non-capped part (https://crbug.com/40914456).
output1_capped, output2_capped = get_output_capped(output1, output2)
lines1 = decode(output1_capped).splitlines()
lines2 = decode(output2_capped).splitlines()
return self.diff_lines(lines1, lines2)
def diff_lines(self, output1_lines, output2_lines):
return diff_output(output1_lines, output2_lines)
def reduced_output(self, output, source):
"""Return output reduced by the source's origin, either only smoke-test
output or only non-smoke-test output.
"""
if source == SMOKE_TEST_SOURCE:
exp = SMOKE_TEST_OUTPUT_RE
else:
exp = SMOKE_TEST_INVERSE_OUTPUT_RE
match = re.search(exp, output, re.S)
if match:
return match.group(1)
return output
def ignore_by_output(self, output):
def check(mapping):
for bug, exp in mapping.items():
if exp.search(output):
return bug
return None
bug = check(self.ignore_output)
if bug:
return bug
return None
def _remove_config_flag(self, config, flag):
if config.has_config_flag(flag):
config.remove_config_flag(flag)
return [
f'Dropped {flag} from {config.label} config based on content rule.'
]
return []
def adjust_configs_by_content(self, execution_configs, testcase):
"""Modifies the execution configs if the testcase content matches a
regular expression defined in DROP_FLAGS_ON_CONTENT above.
The specified flag is dropped from the first two configs. Further
configs are dropped entirely if the specified flag is used.
Returns: A changelog as a list of strings.
"""
logs = []
assert len(execution_configs) > 1
for flag, regexp in self.drop_flags_on_content:
# Check first if the flag we need to drop appears anywhere. This is
# faster than processing the content.
if not any(config.has_config_flag(flag) for config in execution_configs):
continue
if not re.search(regexp, testcase):
continue
logs += self._remove_config_flag(execution_configs[0], flag)
logs += self._remove_config_flag(execution_configs[1], flag)
for config in execution_configs[2:]:
if config.has_config_flag(flag):
execution_configs.remove(config)
logs.append(f'Dropped {config.label} config using '
f'{flag} based on content rule.')
return logs
def adjust_configs_by_output(self, execution_configs, output):
"""Modifies the execution configs if the baseline output contains a
certain directive.
Currently this only searches for one particular directive, after which
we ensure to compare on the same architecture.
Returns: A changelog as a list of strings.
"""
# Cross-arch configs have a same-arch fallback. Check first if there
# are any, as this check is very cheap.
logs = []
if not any(config.fallback for config in execution_configs):
return []
if not re.search(AVOID_CROSS_ARCH_COMPARISON_RE, output, re.M):
return []
for i, config in enumerate(execution_configs):
if config.fallback:
logs.append(
f'Running the {config.label} config on the same architecture.')
execution_configs[i] = config.fallback
return logs

View File

@ -0,0 +1,22 @@
// Copyright 2020 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.
module.exports = {
"env": {
"node": true,
"commonjs": true,
"es6": true,
"mocha": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
}
};

View File

@ -0,0 +1,6 @@
/node_modules
/ochang_js_fuzzer*
/db/
/output.zip
/output/
/workdir/

View File

@ -0,0 +1,11 @@
# Metadata information for this directory.
#
# For more information on DIR_METADATA files, see:
# https://source.chromium.org/chromium/infra/infra/+/master:go/src/infra/tools/dirmd/README.md
#
# For the schema of this file, see Metadata message:
# https://source.chromium.org/chromium/infra/infra/+/master:go/src/infra/tools/dirmd/proto/dir_metadata.proto
monorail {
component: "Infra>Client>V8"
}

View File

@ -0,0 +1,3 @@
file:../../../INFRA_OWNERS
ochang@chromium.org

View File

@ -0,0 +1,122 @@
# JS-Fuzzer
Javascript fuzzer for stand-alone shells like D8, Chakra, JSC or Spidermonkey.
Original author: Oliver Chang
# Building
This fuzzer may require versions of node that are newer than available on
ClusterFuzz, so we use [pkg](https://github.com/zeit/pkg) to create a self
contained binary) out of this.
## Prereqs
You need to intall nodejs and npm. Run `npm install` in this directory.
## Fuzzing DB
This fuzzer requires a fuzzing DB. To build one, get the latest `web_tests.zip`
from [gs://clusterfuzz-data/web_tests.zip](
https://storage.cloud.google.com/clusterfuzz-data/web_tests.zip) and unzip it
(note https://crbug.com/v8/10891 for making this data publicly available).
Then run:
```bash
$ mkdir db
$ node build_db.js -i /path/to/web_tests -o db chakra v8 spidermonkey WebKit/JSTests
```
This may take a while. Optionally test the fuzzing DB with:
```bash
$ node test_db.js -i db
```
## Building fuzzer
Then, to build the fuzzer,
```bash
$ ./node_modules/.bin/pkg -t node18-linux-x64 .
```
Replace "linux" with either "win" or "macos" for those platforms.
This builds a binary named `ochang_js_fuzzer` for Linux / macOS OR
`ochang_js_fuzzer.exe` for Windows.
## Packaging
Use `./package.sh`, `./package.sh win` or `./package.sh macos` to build and
create the `output.zip` archive or use these raw commands:
```bash
$ mkdir output
$ cd output
$ ln -s ../db db
$ ln -s ../ochang_js_fuzzer run
$ zip -r /path/output.zip *
```
**NOTE**: Add `.exe` to `ochang_js_fuzzer` and `run` filename above if archiving
for Windows platform.
# Development
Run the tests with:
```bash
$ npm test
```
When test expectations change, generate them with:
```bash
$ GENERATE=1 npm test
```
# Generating exceptional configurations
Tests that fail to parse or show very bad performance can be automatically
skipped or soft-skipped with the following script (takes >1h):
```bash
$ WEB_TESTS=/path/to/web_tests OUTPUT=/path/to/output/folder ./gen_exceptions.sh
```
# Experimenting (limited to differential fuzzing)
To locally evaluate the fuzzer, setup a work directory as follows:
```bash
$ workdir/
$ workdir/app_dir
$ workdir/fuzzer
$ workdir/input
$ workdir/output
```
The `app_dir` folder can be a symlink or should contain the bundled
version of `d8` with all files required for execution.
Copy the packaged `ochang_js_fuzzer` executable and the `db` folder
to the `fuzzer` directory or use a symlink.
The `input` directory is the root folder of the corpus, i.e. pointing
to the unzipped data of `gs://clusterfuzz-data/web_tests.zip`.
The `output` directory is expected to be empty. It'll contain all
output of the fuzzing session. Start the experiments with:
```bash
$ # Around ~40000 corresponds to 24h of fuzzing on a workstation.
$ NUM_RUNS = 40000
$ python tools/workbench.py $NUM_RUNS
```
You can check current stats with:
```bash
$ cat workdir/output/stats.json | python -m json.tool
```
When failures are found, you can forge minimization command lines with:
```bash
$ MINIMIZER_PATH = path/to/minimizer
$ python tools/minimize.py $MINIMIZER_PATH
```
The path should point to a local checkout of the [minimizer](https://chrome-internal.googlesource.com/chrome/tools/clusterfuzz/+/refs/heads/master/src/python/bot/minimizer/).

View File

@ -0,0 +1,65 @@
// Copyright 2020 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.
/**
* @fileoverview Collect JS nodes.
*/
const program = require('commander');
const corpus = require('./corpus.js');
const db = require('./db.js');
const path = require('path');
const sourceHelpers = require('./source_helpers.js');
function main() {
Error.stackTraceLimit = Infinity;
program
.version('0.0.1')
.option('-i, --input_dir <path>', 'Input directory.')
.option('-o, --output_dir <path>', 'Output directory.')
.parse(process.argv);
if (!program.args.length) {
console.log('Need to specify corpora.');
return;
}
if (!program.output_dir) {
console.log('Need to specify output dir.');
return;
}
const mutateDb = new db.MutateDbWriter(program.output_dir);
const inputDir = path.resolve(program.input_dir);
for (const corpusName of program.args) {
const curCorpus = corpus.create(inputDir, corpusName);
for (const relPath of curCorpus.relFiles()) {
let source;
try {
source = sourceHelpers.loadSource(curCorpus, relPath);
} catch (e) {
console.log(e);
continue;
}
if (!source) {
continue;
}
try{
mutateDb.process(source);
} catch (e) {
console.log(e);
}
}
}
mutateDb.writeIndex();
}
main();

View File

@ -0,0 +1,303 @@
// Copyright 2020 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.
/**
* @fileoverview Corpus
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const program = require('commander');
const exceptions = require('./exceptions.js');
const random = require('./random.js');
const sourceHelpers = require('./source_helpers.js');
// We drop files containing dropped flags with a high probability.
const DROP_DISCOURAGED_FILES_PROB = 0.8;
const WASM_MODULE_BUILDER = 'test/mjsunit/wasm/wasm-module-builder.js';
const WASM_LOAD_LINE = `d8.file.execute("${WASM_MODULE_BUILDER}")`;
function* walkDirectory(directory, filter) {
// Generator for recursively walk a directory.
for (const filePath of fs.readdirSync(directory)) {
const currentPath = path.join(directory, filePath);
const stat = fs.lstatSync(currentPath);
if (stat.isFile()) {
if (!filter || filter(currentPath)) {
yield currentPath;
}
continue;
}
if (stat.isDirectory()) {
for (let childFilePath of walkDirectory(currentPath, filter)) {
yield childFilePath;
}
}
}
}
/**
* Returns true if the test file loads the wasm module builder.
*
* Note this is an approximation which might slightly underestimate (missing
* files that e.g. conditionally depend on the wasm-module-builder) and
* slightly overestimate (in case the load line appears in a multi-line
* comment).
*/
function needsWasmModuleBuilder(absPath) {
// TODO(machenbach): This could be optimized with caching or an
// input-stream version.
const data = fs.readFileSync(absPath, 'utf-8');
for (const [index, line] of data.split('\n').entries()) {
if (line.startsWith(WASM_LOAD_LINE)) {
return true;
}
}
return false;
}
class Corpus extends sourceHelpers.BaseCorpus {
// Input corpus.
constructor(inputDir, corpusName, extraStrict=false) {
super(inputDir);
this.extraStrict = extraStrict;
// Filter for permitted JS files.
function isPermittedJSFile(absPath) {
return (absPath.endsWith('.js') &&
!exceptions.isTestSkippedAbs(absPath));
}
// Cache relative paths of all files in corpus.
this.skippedFiles = [];
this.softSkippedFiles = [];
this.permittedFiles = [];
const directory = path.join(inputDir, corpusName);
for (const absPath of walkDirectory(directory, isPermittedJSFile)) {
const relPath = path.relative(this.inputDir, absPath);
if (exceptions.isTestSkippedRel(relPath) ||
this.isTestSkippedInCorpus(relPath)) {
this.skippedFiles.push(relPath);
} else if (exceptions.isTestSoftSkippedAbs(absPath) ||
exceptions.isTestSoftSkippedRel(relPath)) {
this.softSkippedFiles.push(relPath);
} else {
this.permittedFiles.push(relPath);
}
}
random.shuffle(this.softSkippedFiles);
random.shuffle(this.permittedFiles);
}
/**
* Enable subclasses to decide on more skipped files.
*/
isTestSkippedInCorpus(relPath) {
return false;
}
// Relative paths of all files in corpus.
*relFiles() {
for (const relPath of this.permittedFiles) {
yield relPath;
}
for (const relPath of this.softSkippedFiles) {
yield relPath;
}
}
// Relative paths of all files in corpus including generated skipped.
*relFilesForGenSkipped() {
for (const relPath of this.relFiles()) {
yield relPath;
}
for (const relPath of this.skippedFiles) {
yield relPath;
}
}
/**
* Returns "count" relative test paths, randomly selected from soft-skipped
* and permitted files. Permitted files have a 4 times higher chance to
* be chosen.
*/
getRandomTestcasePaths(count) {
return random.twoBucketSample(
this.softSkippedFiles, this.permittedFiles, 4, count);
}
loadTestcase(relPath, strict, label) {
const start = Date.now();
try {
const source = sourceHelpers.loadSource(this, relPath, strict);
if (program.verbose) {
const duration = Date.now() - start;
console.log(`Parsing ${relPath} ${label} took ${duration} ms.`);
}
if (exceptions.hasFlagsDiscouragingFiles(source.flags) &&
random.choose(module.exports.DROP_DISCOURAGED_FILES_PROB)) {
return undefined
}
return source;
} catch (e) {
console.log(`WARNING: failed to ${label} parse ${relPath}`);
console.log(e);
}
return undefined;
}
*loadTestcases(relPaths) {
for (const relPath of relPaths) {
if (this.extraStrict) {
// When re-generating the files marked sloppy, we additionally test if
// the file parses in strict mode.
this.loadTestcase(relPath, true, 'strict');
}
const source = this.loadTestcase(relPath, false, 'sloppy');
if (source) {
yield source;
}
}
}
getRandomTestcases(count) {
return Array.from(this.loadTestcases(this.getRandomTestcasePaths(count)));
}
getAllTestcases() {
return this.loadTestcases(this.relFilesForGenSkipped());
}
}
class FuzzilliCorpus extends Corpus {
constructor(inputDir, corpusName, extraStrict=false, v8Corpus=undefined) {
super(inputDir, 'fuzzilli', extraStrict);
this.flagMap = new Map();
// We require a V8 corpus side-by-side to cross-load resources.
this.v8Corpus = v8Corpus;
assert(this.v8Corpus);
}
get diffFuzzLabel() {
// Sources from Fuzzilli use the same universal label for differential
// fuzzing because the input file path is random and volatile. It can't
// be used to map to particular content.
return "fuzzilli_source";
}
loadFlags(relPath, data) {
// Create the settings path based on the location of the test file, e.g.
// .../fuzzilli/fuzzdir-1/settings.json for
// .../fuzzilli/fuzzdir-1/corpus/program_x.js
const pathComponents = relPath.split(path.sep);
assert(pathComponents.length > 1);
assert(pathComponents[0] == 'fuzzilli');
const settingsPath = path.join(
this.inputDir, pathComponents[0], pathComponents[1], 'settings.json');
// Use cached flags if already loaded.
let flags = this.flagMap.get(settingsPath);
if (flags == undefined) {
assert(fs.existsSync(settingsPath));
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
flags = settings["processArguments"];
this.flagMap.set(settingsPath, flags);
}
return flags;
}
// Fuzzilli is able to load resources relative to the V8 corpus, which is
// expected to be side-by-side.
loadDependency(relPath) {
const pathComponents = relPath.split(path.sep);
assert(pathComponents.length > 1);
if (pathComponents[0] != 'fuzzilli') {
assert(pathComponents[0] == 'v8');
return this.v8Corpus.loadDependency(relPath);
}
return super.loadDependency(relPath);
}
}
// As above, but skipping files from the crashes directories.
class FuzzilliNoCrashCorpus extends FuzzilliCorpus {
isTestSkippedInCorpus(relPath) {
const pathComponents = relPath.split(path.sep);
if (pathComponents.length < 3) {
return false;
}
assert(pathComponents[0] == 'fuzzilli');
return pathComponents[2] == 'crashes';
}
}
// Fuzzilli corpus that only contains the files depending on the
// wasm-module-builder.
class FuzzilliWasmCorpus extends FuzzilliCorpus {
isTestSkippedInCorpus(relPath) {
return !needsWasmModuleBuilder(
path.resolve(path.join(this.inputDir, relPath)));
}
}
class V8Corpus extends Corpus {
constructor(inputDir, corpusName, extraStrict=false) {
super(inputDir, 'v8', extraStrict);
}
loadFlags(relPath, data) {
const result = [];
let count = 0;
for (const line of data.split('\n')) {
if (count++ > 40) {
// No need to process the whole file. Flags are always added after the
// copyright header.
break;
}
const match = line.match(/\/\/ Flags:\s*(.*)\s*/);
if (!match) {
continue;
}
for (const flag of match[1].split(/\s+/)) {
result.push(flag);
}
}
return result;
}
}
// V8 corpus that only contains the files depending on the
// wasm-module-builder.
class V8WasmCorpus extends V8Corpus {
isTestSkippedInCorpus(relPath) {
return !needsWasmModuleBuilder(
path.resolve(path.join(this.inputDir, relPath)));
}
}
const CORPUS_CLASSES = {
'fuzzilli': FuzzilliCorpus,
'fuzzilli_no_crash': FuzzilliNoCrashCorpus,
'fuzzilli_wasm': FuzzilliWasmCorpus,
'v8': V8Corpus,
'v8_wasm': V8WasmCorpus,
};
function create(inputDir, corpusName, ...args) {
const cls = CORPUS_CLASSES[corpusName] || Corpus;
return new cls(inputDir, corpusName, ...args);
}
module.exports = {
DROP_DISCOURAGED_FILES_PROB: DROP_DISCOURAGED_FILES_PROB,
create: create,
walkDirectory: walkDirectory,
}

View File

@ -0,0 +1,508 @@
// Copyright 2020 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.
/**
* @fileoverview Mutation Db.
*/
const crypto = require('crypto');
const fs = require('fs');
const fsPath = require('path');
const babelGenerator = require('@babel/generator').default;
const babelTemplate = require('@babel/template').default;
const babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
const globals = require('globals');
const random = require('./random.js');
const sourceHelpers = require('./source_helpers.js');
// The probabiliy to choose a snippet with the super keyword in places
// where it can be inserted.
const CHOOSE_SUPER_PROB = 0.2;
const globalIdentifiers = new Set(Object.keys(globals.builtin));
const propertyNames = new Set([
// Parsed from https://github.com/tc39/ecma262/blob/master/spec.html
'add',
'anchor',
'apply',
'big',
'bind',
'blink',
'bold',
'buffer',
'byteLength',
'byteOffset',
'BYTES_PER_ELEMENT',
'call',
'catch',
'charAt',
'charCodeAt',
'clear',
'codePointAt',
'compile',
'concat',
'constructor',
'copyWithin',
'__defineGetter__',
'__defineSetter__',
'delete',
'endsWith',
'entries',
'every',
'exec',
'fill',
'filter',
'find',
'findIndex',
'fixed',
'flags',
'fontcolor',
'fontsize',
'forEach',
'get',
'getDate',
'getDay',
'getFloat32',
'getFloat64',
'getFullYear',
'getHours',
'getInt16',
'getInt32',
'getInt8',
'getMilliseconds',
'getMinutes',
'getMonth',
'getSeconds',
'getTime',
'getTimezoneOffset',
'getUint16',
'getUint32',
'getUint8',
'getUTCDate',
'getUTCDay',
'getUTCFullYear',
'getUTCHours',
'getUTCMilliseconds',
'getUTCMinutes',
'getUTCMonth',
'getUTCSeconds',
'getYear',
'global',
'has',
'hasInstance',
'hasOwnProperty',
'ignoreCase',
'includes',
'indexOf',
'isConcatSpreadable',
'isPrototypeOf',
'italics',
'iterator',
'join',
'keys',
'lastIndexOf',
'length',
'link',
'localeCompare',
'__lookupGetter__',
'__lookupSetter__',
'map',
'match',
'match',
'message',
'multiline',
'name',
'next',
'normalize',
'padEnd',
'padStart',
'pop',
'propertyIsEnumerable',
'__proto__',
'prototype',
'push',
'reduce',
'reduceRight',
'repeat',
'replace',
'replace',
'return',
'reverse',
'search',
'search',
'set',
'set',
'setDate',
'setFloat32',
'setFloat64',
'setFullYear',
'setHours',
'setInt16',
'setInt32',
'setInt8',
'setMilliseconds',
'setMinutes',
'setMonth',
'setSeconds',
'setTime',
'setUint16',
'setUint32',
'setUint8',
'setUTCDate',
'setUTCFullYear',
'setUTCHours',
'setUTCMilliseconds',
'setUTCMinutes',
'setUTCMonth',
'setUTCSeconds',
'setYear',
'shift',
'size',
'slice',
'slice',
'small',
'some',
'sort',
'source',
'species',
'splice',
'split',
'split',
'startsWith',
'sticky',
'strike',
'sub',
'subarray',
'substr',
'substring',
'sup',
'test',
'then',
'throw',
'toDateString',
'toExponential',
'toFixed',
'toGMTString',
'toISOString',
'toJSON',
'toLocaleDateString',
'toLocaleLowerCase',
'toLocaleString',
'toLocaleTimeString',
'toLocaleUpperCase',
'toLowerCase',
'toPrecision',
'toPrimitive',
'toString',
'toStringTag',
'toTimeString',
'toUpperCase',
'toUTCString',
'trim',
'unicode',
'unscopables',
'unshift',
'valueOf',
'values',
]);
const MAX_DEPENDENCIES = 2;
class Expression {
constructor(type, source, isStatement, originalPath,
dependencies, needsSuper) {
this.type = type;
this.source = source;
this.isStatement = isStatement;
this.originalPath = originalPath;
this.dependencies = dependencies;
this.needsSuper = needsSuper;
}
}
function dedupKey(expression) {
if (!expression.dependencies) {
return expression.source;
}
let result = expression.source;
for (let dependency of expression.dependencies) {
result = result.replace(new RegExp(dependency, 'g'), 'ID');
}
return result;
}
function _markSkipped(path) {
while (path) {
path.node.__skipped = true;
path = path.parentPath;
}
}
/**
* Returns true if an expression can be applied or false otherwise.
*/
function isValid(expression) {
const expressionTemplate = babelTemplate(
expression.source,
sourceHelpers.BABYLON_REPLACE_VAR_OPTIONS);
const dependencies = {};
if (expression.dependencies) {
for (const dependency of expression.dependencies) {
dependencies[dependency] = babelTypes.identifier('__v_0');
}
}
try {
expressionTemplate(dependencies);
} catch (e) {
return false;
}
return true;
}
class MutateDbWriter {
constructor(outputDir) {
this.seen = new Set();
this.outputDir = fsPath.resolve(outputDir);
this.index = {
statements: [],
superStatements: [],
all: [],
};
}
process(source) {
let self = this;
let varIndex = 0;
// First pass to collect dependency information.
babelTraverse(source.ast, {
Super(path) {
while (path) {
path.node.__needsSuper = true;
path = path.parentPath;
}
},
YieldExpression(path) {
// Don't include yield expressions in DB.
_markSkipped(path);
},
Identifier(path) {
if (globalIdentifiers.has(path.node.name) &&
path.node.name != 'eval') {
// Global name.
return;
}
if (propertyNames.has(path.node.name) &&
path.parentPath.isMemberExpression() &&
path.parentKey !== 'object') {
// Builtin property name.
return;
}
if (path.parentPath.isMemberExpression() &&
path.parent.property == path.node &&
babelTypes.isIdentifier(path.parent.object) &&
globalIdentifiers.has(path.parent.object.name)) {
// Property access on a global name.
return;
}
let binding = path.scope.getBinding(path.node.name);
if (!binding) {
// Unknown dependency. Don't handle this.
_markSkipped(path);
return;
}
let newName;
if (path.node.name.startsWith('VAR_')) {
newName = path.node.name;
} else if (babelTypes.isFunctionDeclaration(binding.path.node) ||
babelTypes.isFunctionExpression(binding.path.node) ||
babelTypes.isDeclaration(binding.path.node) ||
babelTypes.isFunctionExpression(binding.path.node)) {
// Unknown dependency. Don't handle this.
_markSkipped(path);
return;
} else {
newName = 'VAR_' + varIndex++;
path.scope.rename(path.node.name, newName);
}
// Mark all parents as having a dependency.
while (path) {
path.node.__idDependencies = path.node.__idDependencies || [];
if (path.node.__idDependencies.length <= MAX_DEPENDENCIES) {
path.node.__idDependencies.push(newName);
}
path = path.parentPath;
}
}
});
babelTraverse(source.ast, {
Expression(path) {
if (!path.parentPath.isExpressionStatement()) {
return;
}
if (path.node.__skipped ||
(path.node.__idDependencies &&
path.node.__idDependencies.length > MAX_DEPENDENCIES)) {
return;
}
if (path.isIdentifier() || path.isMemberExpression() ||
path.isConditionalExpression() ||
path.isBinaryExpression() || path.isDoExpression() ||
path.isLiteral() ||
path.isObjectExpression() || path.isArrayExpression()) {
// Skip:
// - Identifiers.
// - Member expressions (too many and too context dependent).
// - Conditional expressions (too many and too context dependent).
// - Binary expressions (too many).
// - Literals (too many).
// - Object/array expressions (too many).
return;
}
if (path.isAssignmentExpression()) {
if (!babelTypes.isMemberExpression(path.node.left)) {
// Skip assignments that aren't to properties.
return;
}
if (babelTypes.isIdentifier(path.node.left.object)) {
if (babelTypes.isNumericLiteral(path.node.left.property)) {
// Skip VAR[\d+] = ...;
// There are too many and they generally aren't very useful.
return;
}
if (babelTypes.isStringLiteral(path.node.left.property) &&
!propertyNames.has(path.node.left.property.value)) {
// Skip custom properties. e.g.
// VAR["abc"] = ...;
// There are too many and they generally aren't very useful.
return;
}
}
}
if (path.isCallExpression() &&
babelTypes.isIdentifier(path.node.callee) &&
!globalIdentifiers.has(path.node.callee.name)) {
// Skip VAR(...) calls since there's too much context we're missing.
return;
}
if (path.isUnaryExpression() && path.node.operator == '-') {
// Skip -... since there are too many.
return;
}
// Make the template.
let generated = babelGenerator(path.node, { concise: true }).code;
let expression = new Expression(
path.node.type,
generated,
path.parentPath.isExpressionStatement(),
source.relPath,
path.node.__idDependencies,
Boolean(path.node.__needsSuper));
// Try to de-dupe similar expressions.
let key = dedupKey(expression);
if (self.seen.has(key)) {
return;
}
// Test results.
if (!isValid(expression)) {
return;
}
// Write results.
let dirPath = fsPath.join(self.outputDir, expression.type);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
let sha1sum = crypto.createHash('sha1');
sha1sum.update(key);
let filePath = fsPath.join(dirPath, sha1sum.digest('hex') + '.json');
fs.writeFileSync(filePath, JSON.stringify(expression));
let relPath = fsPath.relative(self.outputDir, filePath);
// Update index.
self.seen.add(key);
self.index.all.push(relPath);
if (expression.needsSuper) {
self.index.superStatements.push(relPath);
} else {
self.index.statements.push(relPath);
}
}
});
}
writeIndex() {
this.index.all.sort();
this.index.statements.sort();
this.index.superStatements.sort();
fs.writeFileSync(
fsPath.join(this.outputDir, 'index.json'),
JSON.stringify(this.index));
}
}
class MutateDb {
constructor(outputDir) {
this.outputDir = fsPath.resolve(outputDir);
this.index = JSON.parse(
fs.readFileSync(fsPath.join(outputDir, 'index.json'), 'utf-8'));
}
getRandomStatement({canHaveSuper=false} = {}) {
let choices;
if (canHaveSuper) {
choices = random.choose(CHOOSE_SUPER_PROB) ?
this.index.superStatements : this.index.all;
} else {
choices = this.index.statements;
}
let path = fsPath.join(
this.outputDir, choices[random.randInt(0, choices.length - 1)]);
return JSON.parse(fs.readFileSync(path), 'utf-8');
}
*iterateStatements() {
for (const exp of this.index.all) {
const path = fsPath.join(this.outputDir, exp);
yield JSON.parse(fs.readFileSync(path), 'utf-8');
}
}
}
module.exports = {
MutateDb: MutateDb,
MutateDbWriter: MutateDbWriter,
}

View File

@ -0,0 +1,216 @@
// Copyright 2020 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.
/**
* @fileoverview Script mutator for differential fuzzing.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const common = require('./mutators/common.js');
const random = require('./random.js');
const runner = require('./runner.js');
const sourceHelpers = require('./source_helpers.js');
const { filterDifferentialFuzzFlags } = require('./exceptions.js');
const { DifferentialFuzzMutator, DifferentialFuzzSuppressions } = require(
'./mutators/differential_fuzz_mutator.js');
const { ScriptMutator } = require('./script_mutator.js');
const USE_ORIGINAL_FLAGS_PROB = 0.5;
/**
* Randomly chooses a configuration from experiments. The configuration
* parameters are expected to be passed from a bundled V8 build. Constraints
* mentioned below are enforced by PRESUBMIT checks on the V8 side.
*
* @param {Object[]} experiments List of tuples (probability, first config name,
* second config name, second d8 name). The probabilities are integers in
* [0,100]. We assume the sum of all probabilities is 100.
* @param {Object[]} additionalFlags List of tuples (probability, flag strings).
* Probability is in [0,1).
* @return {string[]} List of flags for v8_foozzie.py.
*/
function chooseRandomFlags(experiments, additionalFlags) {
// Add additional flags to second config based on experiment percentages.
const extra_flags = [];
for (const [p, flags] of additionalFlags) {
if (random.choose(p)) {
for (const flag of flags.split(' ')) {
extra_flags.push('--second-config-extra-flags=' + flag);
}
}
}
// Calculate flags determining the experiment.
let acc = 0;
const threshold = random.random() * 100;
for (let [prob, first_config, second_config, second_d8] of experiments) {
acc += prob;
if (acc > threshold) {
return [
'--first-config=' + first_config,
'--second-config=' + second_config,
'--second-d8=' + second_d8,
].concat(extra_flags);
}
}
// Unreachable.
assert(false);
}
function loadJSONFromBuild(name) {
assert(process.env.APP_DIR);
const fullPath = path.join(path.resolve(process.env.APP_DIR), name);
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
}
function hasMjsunit(dependencies) {
return dependencies.some(dep => dep.relPath.endsWith('mjsunit.js'));
}
function hasJSTests(dependencies) {
return dependencies.some(dep => dep.relPath.endsWith('jstest_stubs.js'));
}
class DifferentialScriptMutator extends ScriptMutator {
constructor(settings, db_path) {
super(settings, db_path);
// Mutators for differential fuzzing.
this.differential = [
new DifferentialFuzzSuppressions(settings),
new DifferentialFuzzMutator(settings),
];
// Flag configurations from the V8 build directory.
this.experiments = loadJSONFromBuild('v8_fuzz_experiments.json');
this.additionalFlags = loadJSONFromBuild('v8_fuzz_flags.json');
}
get runnerClass() {
// Choose a setup with the Fuzzilli corpus in 1 of 3.
return random.single([
runner.RandomCorpusRunner,
runner.RandomCorpusRunner,
runner.RandomCorpusRunnerWithFuzzilli,
]);
}
/**
* Performes the high-level mutation and afterwards adds flags for the
* v8_foozzie.py harness.
*/
mutateMultiple(inputs) {
const result = super.mutateMultiple(inputs);
const originalFlags = [];
// Keep original JS flags in some cases. Let the harness pass them to
// baseline _and_ comparison run.
if (random.choose(USE_ORIGINAL_FLAGS_PROB)) {
for (const flag of filterDifferentialFuzzFlags(result.flags)) {
originalFlags.push('--first-config-extra-flags=' + flag);
originalFlags.push('--second-config-extra-flags=' + flag);
}
}
// Add flags for the differnetial-fuzzing settings.
const fuzzFlags = chooseRandomFlags(this.experiments, this.additionalFlags);
result.flags = fuzzFlags.concat(originalFlags);
return result;
}
/**
* Mutatates a set of inputs.
*
* Additionally we prepare inputs by tagging each with the original source
* path for later printing. The mutated sources are post-processed by the
* differential-fuzz mutators, adding extra printing and other substitutions.
*/
mutateInputs(inputs) {
inputs.forEach(input => common.setOriginalPath(input, input.diffFuzzPath));
const result = super.mutateInputs(inputs);
this.differential.forEach(mutator => mutator.mutate(result));
return result;
}
/**
* Adds extra dependencies for differential fuzzing.
*/
resolveDependencies(inputs) {
const dependencies = super.resolveDependencies(inputs);
// The suppression file neuters functions not working with differential
// fuzzing. It can also be used to temporarily silence some functionality
// leading to dupes of an active bug.
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_suppressions.js'));
// Extra printing and tracking functionality.
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_library.js'));
// Make Chakra tests print more.
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_chakra.js'));
if (hasMjsunit(dependencies)) {
// Make V8 tests print more. We guard this as the functionality
// relies on mjsunit.js.
dependencies.push(sourceHelpers.loadResource('differential_fuzz_v8.js'));
}
if (hasJSTests(dependencies)) {
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_jstest.js'));
}
return dependencies;
}
}
/**
* Script mutator that only outputs single Fuzzilli cases without random
* mutations and without try-catch. I.e. mostly a pass-through of Fuzzilli
* cases for differential fuzzing, but with random fuzz experiments and flags.
*/
class FuzzilliDifferentialScriptMutator extends DifferentialScriptMutator {
constructor(settings, db_path) {
super(settings, db_path);
// Slightly higher probabilities for extra printing and exception
// tracking, since we don't do any other mutations.
this.settings.DIFF_FUZZ_EXTRA_PRINT = 0.2;
this.settings.DIFF_FUZZ_TRACK_CAUGHT = 0.6;
}
get runnerClass() {
return runner.RandomFuzzilliNoCrashCorpusRunner;
}
// Pure pass-through behavior without mutations.
mutate() {}
// We don't require any test-suite dependencies like mjsunit, nor their
// stubs and replacements. We also don't need the standard fuzz library
// that's used by mutators that don't run here. We add extra printing
// for differential fuzzing and a stub for the absence of mjsunit for
// differential fuzzing.
resolveDependencies(inputs) {
const dependencies = this.resolveInputDependencies(inputs);
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_mjsunit.js'));
dependencies.push(
sourceHelpers.loadResource('differential_fuzz_library.js'));
return dependencies;
}
}
module.exports = {
DifferentialScriptMutator: DifferentialScriptMutator,
FuzzilliDifferentialScriptMutator: FuzzilliDifferentialScriptMutator,
};

View File

@ -0,0 +1,347 @@
// Copyright 2020 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.
/**
* @fileoverview Blacklists for fuzzer.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const random = require('./random.js');
const {generatedSloppy, generatedSoftSkipped, generatedSkipped} = require(
'./generated/exceptions.js');
const SKIPPED_FILES = [
// Disabled for unexpected test behavior, specific to d8 shell.
'd8-os.js',
'd8-readbuffer.js',
// Passes JS flags.
'd8-arguments.js',
// Slow tests or tests that are too large to be used as input.
/numops-fuzz-part.*.js/,
'regexp-pcre.js',
'unicode-test.js',
'unicodelctest.js',
'unicodelctest-no-optimization.js',
// Unsupported modules.
/^modules.*\.js/,
// Unsupported property escapes.
/^regexp-property-.*\.js/,
// Bad testcases that just loads a script that always throws errors.
'regress-444805.js',
'regress-crbug-489597.js',
'regress-crbug-620253.js',
// Just recursively loads itself.
'regress-8510.js',
// Contains an expected SyntaxError. From:
// spidermonkey/non262/Unicode/regress-352044-02-n.js
'regress-352044-02-n.js',
// Very slow pattern that just makes fuzz tests time out.
'regress-crbug-400504688.js',
// Empty test that just sets flags.
'regress-392928803.js',
];
const SKIPPED_DIRECTORIES = [
// Slow tests or tests that are too large to be used as input.
'embenchen',
'poppler',
'sqlite',
// Causes lots of failures.
'test262',
// Unavailable debug.Debug.
'v8/test/debugger',
'v8/test/inspector',
// Unsupported modules.
'v8/test/js-perf-test/Modules',
// Contains tests expected to error out on parsing.
'v8/test/message',
// Needs specific dependencies for load of various tests.
'v8/test/mjsunit/tools',
// Unsupported e4x standard.
'mozilla/data/e4x',
// Bails out fast without ReadableStream support.
'spidermonkey/non262/ReadableStream',
];
// Files used with a lower probability.
const SOFT_SKIPPED_FILES = [
// Tests with large binary content.
/^binaryen.*\.js/,
// Tests slow to parse.
// CrashTests:
/^jquery.*\.js/,
/^js-test-pre\.js/,
// Spidermonkey:
'regress-308085.js',
'regress-74474-002.js',
'regress-74474-003.js',
// V8:
'object-literal.js',
'regress-390568195.js',
];
// Used with a lower probability if paths match.
const SOFT_SKIPPED_PATHS = [
/webgl/,
];
// Files that can't be combined with `use strict`.
const SLOPPY_FILES = new Set([
"chakra/UnitTestFramework/UnitTestFramework.js",
]);
// Flags that lead to false positives. Furthermore choose files using these
// flags with a lower probability, as the absence of the flags typically
// renders the tests useless.
const DISALLOWED_FLAGS_WITH_DISCOURAGED_FILES = [
// Disallowed due to noise. We explicitly add --harmony and --js-staging
// to trials, and all of these features are staged before launch.
/^--harmony-(?!shipping)/,
/^--js-(?!staging|shipping)/,
/^--icu-data-file.*/,
// Disallowed due to false positives.
'--correctness-fuzzer-suppressions',
'--expose-trigger-failure',
// Doesn't make much sense without the memory-corruption API. In the future
// we might want to enable the latter only on builds with the API
// available. Using tests that need one of these flags is also not
// resulting in useful cases.
'--sandbox-testing',
'--sandbox-fuzzing',
];
// Flags that lead to false positives or that are already passed by default.
const DISALLOWED_FLAGS = [
// Disallowed because features prefixed with "experimental" are not
// stabilized yet and would cause too much noise when enabled.
/^--experimental-.*/,
// Disallowed because they are passed explicitly on the command line.
'--allow-natives-syntax',
'--debug-code',
'--disable-abortjs',
'--enable-slow-asserts',
'--expose-gc',
'--expose_gc',
'--fuzzing',
'--omit-quit',
'--disable-in-process-stack-traces',
'--invoke-weak-callbacks',
'--verify-heap',
/^--random-seed.*/,
// Disallowed due to false positives.
'--check-handle-count',
'--expose-debug-as',
'--expose-natives-as',
'--mock-arraybuffer-allocator',
/^--trace-path.*/,
/.*\.mjs$/,
];
// Flags only used with 25% probability.
const LOW_PROB_FLAGS_PROB = 0.25;
const LOW_PROB_FLAGS = [
// Flags that lead to slow test performance.
/^--gc-interval.*/,
/^--deopt-every-n-times.*/,
];
// Flags printing data, leading to false positives in differential fuzzing.
const DISALLOWED_DIFFERENTIAL_FUZZ_FLAGS = [
/^--gc-interval.*/,
/^--perf.*/,
/^--print.*/,
/^--stress-runs.*/,
/^--trace.*/,
'--expose-externalize-string',
'--interpreted-frames-native-stack',
'--validate-asm',
];
// Pairs of flags that shouldn't be used together.
const CONTRADICTORY_FLAGS = [
['--assert-types', '--stress-concurrent-inlining'],
['--assert-types', '--stress-concurrent-inlining-attach-code'],
['--jitless', '--maglev'],
['--jitless', '--maglev-future'],
['--jitless', '--stress-maglev'],
['--jitless', '--stress-concurrent-inlining'],
['--jitless', '--stress-concurrent-inlining-attach-code'],
]
const MAX_FILE_SIZE_BYTES = 128 * 1024; // 128KB
const MEDIUM_FILE_SIZE_BYTES = 32 * 1024; // 32KB
function _findMatch(iterable, candidate) {
for (const entry of iterable) {
if (typeof entry === 'string') {
if (entry === candidate) {
return true;
}
} else {
if (entry.test(candidate)) {
return true;
}
}
}
return false;
}
function _doesntMatch(iterable, candidate) {
return !_findMatch(iterable, candidate);
}
// Convert Windows path separators.
function normalize(testPath) {
return path.normalize(testPath).replace(/\\/g, '/');
}
function isTestSkippedAbs(absPath) {
const basename = path.basename(absPath);
if (_findMatch(SKIPPED_FILES, basename)) {
return true;
}
const normalizedTestPath = normalize(absPath);
for (const entry of SKIPPED_DIRECTORIES) {
if (normalizedTestPath.includes(entry)) {
return true;
}
}
// Avoid OOM/hangs through huge inputs.
const stat = fs.statSync(absPath);
return (stat && stat.size >= MAX_FILE_SIZE_BYTES);
}
function isTestSkippedRel(relPath) {
return generatedSkipped.has(normalize(relPath));
}
// For testing.
function getSoftSkipped() {
return SOFT_SKIPPED_FILES;
}
// For testing.
function getSoftSkippedPaths() {
return SOFT_SKIPPED_PATHS;
}
// For testing.
function getGeneratedSoftSkipped() {
return generatedSoftSkipped;
}
// For testing.
function getGeneratedSloppy() {
return generatedSloppy;
}
function isTestSoftSkippedAbs(absPath) {
if (_findMatch(this.getSoftSkippedPaths(), absPath)) {
return true;
}
const basename = path.basename(absPath);
if (_findMatch(this.getSoftSkipped(), basename)) {
return true;
}
// Graylist medium size files.
const stat = fs.statSync(absPath);
return (stat && stat.size >= MEDIUM_FILE_SIZE_BYTES);
}
function isTestSoftSkippedRel(relPath) {
return this.getGeneratedSoftSkipped().has(normalize(relPath));
}
function isTestSloppyRel(relPath) {
const path = normalize(relPath);
return this.getGeneratedSloppy().has(path) || SLOPPY_FILES.has(path);
}
function filterFlags(flags) {
return flags.filter(flag => {
return (
flag.startsWith('--') &&
_doesntMatch(DISALLOWED_FLAGS_WITH_DISCOURAGED_FILES, flag) &&
_doesntMatch(DISALLOWED_FLAGS, flag) &&
(_doesntMatch(LOW_PROB_FLAGS, flag) ||
random.choose(LOW_PROB_FLAGS_PROB)));
});
}
function hasFlagsDiscouragingFiles(flags) {
return flags.some(flag => {
return _findMatch(DISALLOWED_FLAGS_WITH_DISCOURAGED_FILES, flag);
});
}
/**
* Randomly drops flags to resolve contradicions defined by
* `CONTRADICTORY_FLAGS`.
*/
function resolveContradictoryFlags(flags) {
const flagSet = new Set(flags);
for (const [flag1, flag2] of this.CONTRADICTORY_FLAGS) {
if (flagSet.has(flag1) && flagSet.has(flag2)) {
flagSet.delete(random.single([flag1, flag2]));
}
}
return Array.from(flagSet.values());
}
function filterDifferentialFuzzFlags(flags) {
return flags.filter(
flag => _doesntMatch(DISALLOWED_DIFFERENTIAL_FUZZ_FLAGS, flag));
}
module.exports = {
CONTRADICTORY_FLAGS: CONTRADICTORY_FLAGS,
filterDifferentialFuzzFlags: filterDifferentialFuzzFlags,
filterFlags: filterFlags,
getGeneratedSoftSkipped: getGeneratedSoftSkipped,
getGeneratedSloppy: getGeneratedSloppy,
getSoftSkipped: getSoftSkipped,
getSoftSkippedPaths: getSoftSkippedPaths,
hasFlagsDiscouragingFiles: hasFlagsDiscouragingFiles,
isTestSkippedAbs: isTestSkippedAbs,
isTestSkippedRel: isTestSkippedRel,
isTestSoftSkippedAbs: isTestSoftSkippedAbs,
isTestSoftSkippedRel: isTestSoftSkippedRel,
isTestSloppyRel: isTestSloppyRel,
resolveContradictoryFlags: resolveContradictoryFlags,
}

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
# Copyright 2020 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.
"""
Launcher for the foozzie differential-fuzzing harness. Wraps foozzie
with Python2 for backwards-compatibility when bisecting.
Obsolete now after switching to Python3 entirely. We keep the launcher
for a transition period.
"""
import re
import shutil
import subprocess
import sys
def find_harness_code(args):
for arg in args:
if arg.endswith('v8_foozzie.py'):
with open(arg) as f:
return f.read()
assert False, 'Foozzie harness not found'
if __name__ == '__main__':
# In some cases or older versions, the python executable is passed as
# first argument. Let's be robust either way, with or without full
# path or version.
if re.match(r'.*python.*', sys.argv[1]):
args = sys.argv[2:]
else:
args = sys.argv[1:]
python_exe = 'python3'
# To ease bisection of really old bugs, attempt to use Python2 as long
# as it is supported. This enables bisection before the point where the
# harness switched to Python3.
script = find_harness_code(args)
use_python3 = script.startswith('#!/usr/bin/env python3')
if not use_python3 and shutil.which('python2'):
python_exe = 'python2'
process = subprocess.Popen([python_exe] + args)
process.communicate()
sys.exit(process.returncode)

View File

@ -0,0 +1,196 @@
// Copyright 2020 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.
/**
* @fileoverview Generate exceptions from full corpus test report.
*/
const program = require('commander');
const assert = require('assert');
const babelGenerator = require('@babel/generator').default;
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const fs = require('fs');
const p = require('path');
const prettier = require("prettier");
const SPLIT_LINES_RE = /^.*([\n\r]+|$)/gm;
const PARSE_RE = /^Parsing (.*) sloppy took (\d+) ms\.\n$/;
const MUTATE_RE = /^Mutating (.*) took (\d+) ms\.\n$/;
const PARSE_FAILED_RE = /^WARNING: failed to sloppy parse (.*)\n$/;
const PARSE_STRICT_FAILED_RE = /^WARNING: failed to strict parse (.*)\n$/;
const MUTATE_FAILED_RE = /^ERROR: Exception during mutate: (.*)\n$/;
// Add tests matching error regexp to result array.
function matchError(regexp, line, resultArray){
const match = line.match(regexp);
if (!match) return false;
const relPath = match[1];
assert(relPath);
resultArray.push(relPath);
return true;
}
// Sum up total duration of tests matching the duration regexp and
// map test -> duration in result map.
function matchDuration(regexp, line, resultMap){
const match = line.match(regexp);
if (!match) return false;
const relPath = match[1];
assert(relPath);
resultMap[relPath] = (resultMap[relPath] || 0) + parseInt(match[2]);
return true;
}
// Create lists of failed and slow tests from stdout of a fuzzer run.
function processFuzzOutput(outputFile){
const text = fs.readFileSync(outputFile, 'utf-8');
const lines = text.match(SPLIT_LINES_RE);
const failedParse = [];
const failedParseStrict = [];
const failedMutate = [];
const durationsMap = {};
for (const line of lines) {
if (matchError(PARSE_FAILED_RE, line, failedParse))
continue;
if (matchError(PARSE_STRICT_FAILED_RE, line, failedParseStrict))
continue;
if (matchError(MUTATE_FAILED_RE, line, failedMutate))
continue;
if (matchDuration(PARSE_RE, line, durationsMap))
continue;
if (matchDuration(MUTATE_RE, line, durationsMap))
continue;
}
// Tuples (absPath, duration).
const total = Object.entries(durationsMap);
// Tuples (absPath, duration) with 2s < duration <= 10s.
const slow = total.filter(t => t[1] > 2000 && t[1] <= 10000);
// Tuples (absPath, duration) with 10s < duration.
const verySlow = total.filter(t => t[1] > 10000);
// Assert there's nothing horribly wrong with the results.
// We have at least 2500 tests in the output.
assert(total.length > 2500);
// No more than 5% parse/mutation errors.
assert(failedParse.length + failedMutate.length < total.length / 20);
// No more than 10% slow tests
assert(slow.length < total.length / 10);
// No more than 2% very slow tests.
assert(verySlow.length < total.length / 50);
// Sort everything.
failedParse.sort();
failedParseStrict.sort();
failedMutate.sort();
function slowestFirst(a, b) {
return b[1] - a[1];
}
slow.sort(slowestFirst);
verySlow.sort(slowestFirst);
return [failedParse, failedParseStrict, failedMutate, slow, verySlow];
}
// List of string literals of failed tests.
function getLiteralsForFailed(leadingComment, failedList) {
const result = failedList.map(path => babelTypes.stringLiteral(path));
if (result.length) {
babelTypes.addComment(result[0], 'leading', leadingComment);
}
return result;
}
// List of string literals of slow tests with duration comments.
function getLiteralsForSlow(leadingComment, slowList) {
const result = slowList.map(([path, duration]) => {
const literal = babelTypes.stringLiteral(path);
babelTypes.addComment(
literal, 'trailing', ` ${duration / 1000}s`, true);
return literal;
});
if (result.length) {
babelTypes.addComment(result[0], 'leading', leadingComment);
}
return result;
}
function main() {
program
.version('0.0.1')
.parse(process.argv);
if (!program.args.length) {
console.log('Need to specify stdout reports of fuzz runs.');
return;
}
let skipped = [];
let softSkipped = [];
let sloppy = [];
for (const outputFile of program.args) {
const [failedParse, failedParseStrict, failedMutate, slow, verySlow] = (
processFuzzOutput(outputFile));
const name = p.basename(outputFile, p.extname(outputFile));
// Skip tests that fail to parse/mutate or are very slow.
skipped = skipped.concat(getLiteralsForFailed(
` Tests with parse errors from ${name} `, failedParse));
skipped = skipped.concat(getLiteralsForFailed(
` Tests with mutation errors from ${name} `, failedMutate));
skipped = skipped.concat(getLiteralsForSlow(
` Very slow tests from ${name} `, verySlow));
// Soft-skip slow but not very slow tests.
softSkipped = softSkipped.concat(getLiteralsForSlow(
` Slow tests from ${name} `, slow));
// Mark sloppy tests.
sloppy = sloppy.concat(getLiteralsForFailed(
` Tests requiring sloppy mode from ${name} `, failedParseStrict));
}
const fileTemplate = babelTemplate(`
/**
* @fileoverview Autogenerated exceptions. Created with gen_exceptions.js.
*/
'use strict';
const skipped = SKIPPED;
const softSkipped = SOFTSKIPPED;
const sloppy = SLOPPY;
module.exports = {
generatedSkipped: new Set(skipped),
generatedSoftSkipped: new Set(softSkipped),
generatedSloppy: new Set(sloppy),
}
`, {preserveComments: true});
const skippedArray = babelTypes.arrayExpression(skipped);
const softSkippedArray = babelTypes.arrayExpression(softSkipped);
const sloppyArray = babelTypes.arrayExpression(sloppy);
const statements = fileTemplate({
SKIPPED: skippedArray,
SOFTSKIPPED: softSkippedArray,
SLOPPY: sloppyArray,
});
const resultProgram = babelTypes.program(statements);
const code = babelGenerator(resultProgram, { comments: true }).code;
const prettyCode = prettier.format(code, { parser: "babel" });
fs.writeFileSync('generated/exceptions.js', prettyCode);
}
main();

View File

@ -0,0 +1,12 @@
#!/bin/bash
# Copyright 2020 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.
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c chakra > chakra.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c v8 > v8.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c spidermonkey > spidermonkey.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c WebKit/JSTests > jstests.log
APP_NAME=d8 node run.js -i $WEB_TESTS -o $OUTPUT -z -v -e -c CrashTests > crashtests.log
node gen_exceptions.js v8.log spidermonkey.log chakra.log jstests.log crashtests.log

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,257 @@
// Copyright 2025 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.
/**
* @fileoverview Analyzer.
* Mutator that acquires additional information used in subsequent mutators.
* This mutator doesn't change the structure, but only marks nodes with
* meta information.
*/
'use strict';
const assert = require('assert');
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
// Additional resource needed if the AsyncIterator ID is found in the code.
const ASYNC_ITERATOR_ID = 'AsyncIterator'
const ASYNC_ITERATOR_RESOURCE = 'async_iterator.js'
// State for keeping track of loops and loop bodies during traversal.
const LoopState = Object.freeze({
LOOP: Symbol("loop"),
BLOCK: Symbol("block"),
});
// We consider loops with more iterations than this as large. This can be used
// to reduce some mutations in such loops.
const LARGE_LOOP_THRESHOLD = 1000;
/**
* Combine multiple enter/exit visitor pairs. Also single enter or exit
* methods are possible.
*/
function compound(...visitors) {
const enters = [];
const exits = [];
for (const visitor of visitors) {
if ("enter" in visitor) enters.push(visitor.enter);
if ("exit" in visitor) exits.push(visitor.exit);
}
return {
enter: enters,
// Exit in reverse order so that the corresponding enter/exit pairs
// operate on a stack.
exit: exits.reverse(),
};
}
/**
* Returns true for numeric values with or without an unary minus compound.
*/
function isNumber(node) {
return (
(
// Direct valid number literal.
babelTypes.isLiteral(node) &&
!isNaN(node.value)
) ||
(
// A number literal wrapped with an unary minus.
babelTypes.isUnaryExpression(node) &&
node.operator === '-' &&
babelTypes.isLiteral(node.argument) &&
!isNaN(node.argument.value)
)
);
}
/**
* Extracts the numeric value from a node. We assume it is a number as
* determined by the function above.
*/
function getNumber(node) {
// Direct valid number literal.
if (babelTypes.isLiteral(node)) {
assert(!isNaN(node.value));
return node.value;
}
// A number literal wrapped with an unary minus.
assert(babelTypes.isUnaryExpression(node));
assert(node.operator === '-');
assert(babelTypes.isLiteral(node.argument));
assert(!isNaN(node.argument.value));
return -node.argument.value;
}
/**
* Resolve two nodes in any order as [identifier, number] if possible,
* [undefined, undefined] otherwise.
*/
function idAndNumber(node1, node2) {
if(!node1 || !node2) {
return [undefined, undefined];
}
if (babelTypes.isIdentifier(node1) && isNumber(node2)) {
return [node1.name, getNumber(node2)];
}
if (isNumber(node1) && babelTypes.isIdentifier(node2)) {
return [node2.name, getNumber(node1)];
}
return [undefined, undefined];
}
/**
* Return true if we approximate this node as a large for-loop. We
* heuristically look at simple loop initializers and conditions only.
*
* As this is only an approximation, it will determine some large loops
* as small and vice versa. Hence the result of this should only be used
* for soft logical changes during fuzzing, e.g. behind a probability.
*/
function approximateLargeForLoop(node) {
assert(babelTypes.isForStatement(node));
// The initializer must be a single declaration of the form:
// `{var|let|const} {decl}`.
if (!node.init ||
!babelTypes.isVariableDeclaration(node.init) ||
node.init.declarations.length != 1) {
return false;
}
// The declaration must assign a number to an identifier: `id = num`.
const decl = node.init.declarations[0];
if (!decl ||
!babelTypes.isVariableDeclarator(decl) ||
!babelTypes.isIdentifier(decl.id) ||
!decl.init ||
!isNumber(decl.init)) {
return false;
}
// The test part must be a single binary expression. We don't care which
// operator, e.g. `A < B`.
const initID = decl.id.name;
const initNumber = getNumber(decl.init);
if (!node.test || !babelTypes.isBinaryExpression(node.test)) {
return false;
}
// The two operants must be an identifier and a number in any order. E.g.:
// `id < num`.
const [testID, testNumber] = idAndNumber(node.test.left, node.test.right);
if (testID === undefined || testNumber === undefined) {
return false;
}
// If we test the same identifier we initialized and if the numeric
// difference is large enough, we approximate this as large. We ignore
// increments.
return (
initID === testID &&
Math.abs(initNumber - testNumber) >= LARGE_LOOP_THRESHOLD);
}
class ContextAnalyzer extends mutator.Mutator {
constructor() {
super();
this.loopStack = [];
}
/**
* Return true if the traversal is currently within a loop test part.
*/
isLoopTest() {
return this.loopStack.at(-1) === LoopState.LOOP;
}
get visitor() {
const thisMutator = this;
const loopStack = this.loopStack;
// Maintain the state if we're in a loop.
const loopStatement = {
enter(path) {
loopStack.push(LoopState.LOOP);
},
exit(path) {
assert(loopStack.pop() === LoopState.LOOP);
}
};
// Mark functions that contain empty infinite loops.
const emptyInfiniteLoop = {
enter(path) {
if (common.isInfiniteLoop(path.node) &&
path.node.body &&
babelTypes.isBlockStatement(path.node.body) &&
!path.node.body.body.length) {
const fun = path.findParent((p) => p.isFunctionDeclaration());
if (fun && fun.node && fun.node.id &&
babelTypes.isIdentifier(fun.node.id) &&
common.isFunctionIdentifier(fun.node.id.name)) {
thisMutator.context.infiniteFunctions.add(fun.node.id.name);
}
}
},
};
const largeForLoop = {
enter(path) {
if (approximateLargeForLoop(path.node)) {
common.setLargeLoop(path.node);
}
},
};
return {
Identifier(path) {
if (path.node.name === ASYNC_ITERATOR_ID) {
thisMutator.context.extraResources.add(ASYNC_ITERATOR_RESOURCE);
}
if (thisMutator.isLoopTest() &&
common.isVariableIdentifier(path.node.name)) {
thisMutator.context.loopVariables.add(path.node.name);
}
},
FunctionDeclaration(path) {
if (path.node.id && babelTypes.isIdentifier(path.node.id)) {
if (common.isFunctionIdentifier(path.node.id.name) &&
path.node.async) {
thisMutator.context.asyncFunctions.add(path.node.id.name);
}
}
},
WhileStatement: compound(loopStatement, emptyInfiniteLoop),
DoWhileStatement: compound(loopStatement, emptyInfiniteLoop),
ForStatement: compound(loopStatement, largeForLoop),
BlockStatement: {
enter(path) {
loopStack.push(LoopState.BLOCK);
},
exit(path) {
assert(loopStack.pop() === LoopState.BLOCK);
}
},
// Mark nodes containing a yield expression up to the encolsing
// generator function declaration.
YieldExpression(path) {
for (let parent = path.parentPath;
parent && !parent.isFunctionDeclaration();
parent = parent.parentPath) {
common.setContainsYield(parent.node);
}
},
};
}
}
module.exports = {
ContextAnalyzer: ContextAnalyzer,
};

View File

@ -0,0 +1,111 @@
// Copyright 2020 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.
/**
* @fileoverview Mutator for array expressions.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
// Blueprint for choosing the maximum number of mutations. Bias towards
// performing only one mutation.
const MUTATION_CHOICES = [1, 1, 1, 1, 1, 2, 2, 2, 3];
const MAX_ARRAY_LENGTH = 50;
class ArrayMutator extends mutator.Mutator {
get visitor() {
const thisMutator = this;
return {
ArrayExpression(path) {
const elements = path.node.elements;
if (!random.choose(thisMutator.settings.MUTATE_ARRAYS) ||
elements.length > module.exports.MAX_ARRAY_LENGTH) {
return;
}
// Annotate array expression with the action taken, indicating
// if we also replaced elements.
function annotate(message, replace) {
if (replace) message += ' (replaced)';
thisMutator.annotate(path.node, message);
}
// Add or replace elements at a random index.
function randomSplice(replace, ...args) {
// Choose an index that's small enough to replace all desired items.
const index = random.randInt(0, elements.length - replace);
elements.splice(index, replace, ...args);
}
function duplicateElement(replace) {
const element = random.single(elements);
if (!element || common.isLargeNode(element)) {
return;
}
annotate('Duplicate an element', replace);
randomSplice(replace, babelTypes.cloneDeep(element));
}
function insertRandomValue(replace) {
annotate('Insert a random value', replace);
randomSplice(replace, common.randomValue(path));
}
function insertHole(replace) {
annotate('Insert a hole', replace);
randomSplice(replace, null);
}
function removeElements(count) {
annotate('Remove elements');
randomSplice(random.randInt(1, count));
}
function shuffle() {
annotate('Shuffle array');
random.shuffle(elements);
}
// Mutation options. Repeated mutations have a higher probability.
const mutations = [
() => duplicateElement(1),
() => duplicateElement(1),
() => duplicateElement(1),
() => duplicateElement(0),
() => duplicateElement(0),
() => insertRandomValue(1),
() => insertRandomValue(1),
() => insertRandomValue(0),
() => insertHole(1),
() => insertHole(0),
() => removeElements(1),
() => removeElements(elements.length),
shuffle,
];
// Perform several mutations.
const count = random.single(MUTATION_CHOICES);
for (let i = 0; i < count; i++) {
random.single(mutations)();
}
// Don't recurse on nested arrays.
path.skip();
},
}
}
}
module.exports = {
MAX_ARRAY_LENGTH: MAX_ARRAY_LENGTH,
ArrayMutator: ArrayMutator,
};

View File

@ -0,0 +1,145 @@
// Copyright 2025 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.
/**
* @fileoverview Closure remover.
*
* This transforms top-level parameterless closures and inlines their code
* into the enclosing program scope. Such code is more accessible to subsequent
* mutations.
*/
const assert = require('assert');
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
/**
* Returns true if this function expression can be transformed.
*/
function canTransformFunctionExpression(node, referencedFunctionIds) {
return (
// Needs to be a kind of function expression.
(babelTypes.isFunctionExpression(node) ||
babelTypes.isArrowFunctionExpression(node)) &&
// Can't contain return statements. We mark these during the first
// pass of the analysis.
!node.__returns &&
// We just don't support these.
!node.async &&
!node.expression &&
!node.generator &&
// Can't have parameters.
node.params.length == 0 &&
// Is either anonymous or doesn't reference its identifier. We track
// references during the first pass of the analysis.
(!node.id ||
(babelTypes.isIdentifier(node.id) &&
!referencedFunctionIds.has(node.id.name))) &&
// Has a block body.
node.body &&
babelTypes.isBlockStatement(node.body) &&
// The block is not empty.
node.body.body &&
node.body.body.length
);
}
/**
* Returns true if this closure can be transformed.
*/
function canTransformClosure(path, referencedFunctionIds) {
return (
// We only transform top-level nodes.
path.parentPath.isProgram() &&
// Expect a call-expression statement.
path.node.expression &&
babelTypes.isCallExpression(path.node.expression) &&
// Can't pass arguments.
path.node.expression.arguments.length == 0 &&
// Must call an inlined function expression.
path.node.expression.callee &&
canTransformFunctionExpression(
path.node.expression.callee, referencedFunctionIds)
);
}
// We access the body assuming it has been checked before with
// `isRemovableFunctionExpression`.
function getFunctionBody(path) {
return path.node.expression.callee.body.body;
}
class ClosureRemover extends mutator.Mutator {
get visitor() {
if (!random.choose(this.settings.TRANSFORM_CLOSURES)) {
return [];
}
const thisMutator = this;
const referencedFunctionIds = new Set();
const functionStack = [];
let transformationCount = 0;
// Maintain function expression stack. During the traversal, return
// statements belong to the function on the top of the stack.
const functionExpression = {
enter(path) {
functionStack.push(path.node);
},
exit(path) {
const node = functionStack.pop()
assert(babelTypes.isFunctionExpression(node) ||
babelTypes.isArrowFunctionExpression(node));
}
};
return [{
// First pass to mark functions with return statements and to mark
// referenced function identifiers.
ArrowFunctionExpression: functionExpression,
FunctionExpression: functionExpression,
Identifier(path) {
if (path.isReferencedIdentifier() &&
common.isFunctionIdentifier(path.node.name)) {
referencedFunctionIds.add(path.node.name);
}
},
ReturnStatement(path) {
const currentFunction = functionStack.at(-1);
if (currentFunction) {
currentFunction.__returns = true;
}
path.skip();
}
}, {
// Second pass to transform closures.
ExpressionStatement(path) {
if (!canTransformClosure(path, referencedFunctionIds)) {
path.skip();
return;
}
for (const node of getFunctionBody(path)) {
path.insertBefore(node);
}
path.remove();
transformationCount++;
},
Program: {
exit(path) {
thisMutator.annotate(
path.node,
`Transformed ${transformationCount} closures.`);
}
}
}];
}
}
module.exports = {
ClosureRemover: ClosureRemover,
}

View File

@ -0,0 +1,487 @@
// Copyright 2020 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.
/**
* @fileoverview Common mutator utilities.
*/
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const babylon = require('@babel/parser');
const sourceHelpers = require('../source_helpers.js');
const random = require('../random.js');
const INTERESTING_NUMBER_VALUES = [
-1, -0.0, 0, 1,
// Float values.
-0.000000000000001, 0.000000000000001,
// Special values.
NaN, +Infinity, -Infinity,
// Boundaries of int, signed, unsigned, SMI (near +/- 2^(30, 31, 32).
0x03fffffff, 0x040000000, 0x040000001,
-0x03fffffff, -0x040000000, -0x040000001,
0x07fffffff, 0x080000000, 0x080000001,
-0x07fffffff, -0x080000000, -0x080000001,
0x0ffffffff, 0x100000000, 0x100000001,
-0x0ffffffff, -0x100000000, -0x100000001,
// Boundaries of maximum safe integer (near +/- 2^53).
9007199254740990, 9007199254740991, 9007199254740992,
-9007199254740990, -9007199254740991, -9007199254740992,
// Boundaries of double.
5e-324, 1.7976931348623157e+308,
-5e-324,-1.7976931348623157e+308,
]
const INTERESTING_NON_NUMBER_VALUES = [
// Simple arrays.
'[]',
'Array(0x8000).fill("a")',
// Simple object.
'{}',
'{a: "foo", b: 10, c: {}}',
// Simple strings.
'"foo"',
'""',
// Simple regex.
'/0/',
'"/0/"',
// Simple symbol.
'Symbol("foo")',
// Long string.
'Array(0x8000).join("a")',
// Math.PI
'Math.PI',
// Others.
'false',
'true',
'undefined',
'null',
'this',
'this[0]',
'this[1]',
// Empty function.
'(function() {return 0;})',
// Objects with functions.
'({toString:function(){return "0";}})',
'({valueOf:function(){return 0;}})',
'({valueOf:function(){return "0";}})',
// Objects for primitive types created using new.
'(new Boolean(false))',
'(new Boolean(true))',
'(new String(""))',
'(new Number(0))',
'(new Number(-0))',
]
const LARGE_NODE_SIZE = 100;
const MAX_ARGUMENT_COUNT = 10;
// If chosen, a strict execution order is used for finding available
// variables, which however limits the choices. The probability is
// rather low than high, as violations can only occur if calls of
// enclosing functions are also moved up. Most of these rare violations
// would be wrapped by the try-catch mutator.
const STRICT_EXECUTION_ORDER_PROB = 0.3;
function _identifier(identifier) {
return babelTypes.identifier(identifier);
}
function _numericLiteral(number) {
return babelTypes.numericLiteral(number);
}
function _unwrapExpressionStatement(value) {
if (babelTypes.isExpressionStatement(value)) {
return value.expression;
}
return value;
}
function isVariableIdentifier(name) {
return /__v_[0-9]+/.test(name);
}
function isFunctionIdentifier(name) {
return /__f_[0-9]+/.test(name);
}
function isInForLoopCondition(path) {
// Return whether if we're in the init/test/update parts of a for loop (but
// not the body). Mutating variables in the init/test/update will likely
// modify loop variables and cause infinite loops.
const forStatementChild = path.find(
p => p.parent && babelTypes.isForStatement(p.parent));
return (forStatementChild && forStatementChild.parentKey !== 'body');
}
function isInWhileLoop(path) {
// Return whether if we're in a while loop.
const whileStatement = path.find(p => babelTypes.isWhileStatement(p));
return Boolean(whileStatement);
}
function isInfiniteLoop(node) {
// Approximates if node is a trivial infinite while loop. Assumes that node
// is either a while or dowhile statement.
return (node.test &&
babelTypes.isLiteral(node.test) &&
node.test.value === true);
}
/**
* Return if `currentPath` might execute before `otherPath`.
*
* This is more permissive for function expressions, whose lexical location
* has more available identifiers without leading to wrong code when those
* are used within the body.
*
* Behind a probabiliy, this is also more permissive for function declarations.
* There, however, depending on where the function call might happen,
* the resulting code can cause reference errors.
*
* The permissive logic for functions and function expressions is only
* implemented for one level. It could also be implemented for further nested
* levels.
*
* Example 1:
* foo();
* let a;
* function foo() { using `a` here might cause a reference error}
*
* Example 2:
* let a;
* (function () { using `a` here probably can't cause a reference error})();
*/
function executesBefore(currentPath, otherPath) {
if (!currentPath.willIMaybeExecuteBefore(otherPath)) {
return false;
}
// TODO(machenbach): The enclosing function info could be cached across
// several calls to executesBefore in the loop below.
const enclosingFunExpr = currentPath.findParent(
x => x.isFunctionExpression() || x.isArrowFunctionExpression());
if (enclosingFunExpr &&
!enclosingFunExpr.willIMaybeExecuteBefore(otherPath)) {
return false;
}
if (random.choose(module.exports.STRICT_EXECUTION_ORDER_PROB)) {
return true;
}
const enclosingFunDecl = currentPath.findParent(
x => x.isFunctionDeclaration());
if (enclosingFunDecl &&
!enclosingFunDecl.willIMaybeExecuteBefore(otherPath)) {
return false;
}
return true;
}
function* _availableIdentifierNamesGen(path, filter) {
// TODO(ochang): Consider globals that aren't declared with let/var etc.
const allBindings = path.scope.getAllBindings();
for (const key of Object.keys(allBindings)) {
if (!filter(key)) {
continue;
}
if (filter === isVariableIdentifier &&
executesBefore(path, allBindings[key].path)) {
continue;
}
yield key;
}
}
function _availableIdentifiers(path, filter) {
const available = new Array();
for (const name of _availableIdentifierNamesGen(path, filter)) {
available.push(_identifier(name));
}
return available;
}
function availableVariables(path) {
return _availableIdentifiers(path, isVariableIdentifier);
}
function availableFunctions(path) {
return _availableIdentifiers(path, isFunctionIdentifier);
}
function availableFunctionNames(path) {
const available = new Set([]);
for (const name of _availableIdentifierNamesGen(
path, isFunctionIdentifier)) {
available.add(name);
}
return available;
}
function randomVariable(path) {
return random.single(availableVariables(path));
}
function randomFunction(path) {
return random.single(availableFunctions(path));
}
function randomSeed() {
return random.randInt(0, 2**20);
}
function randomObject(seed) {
if (seed === undefined) {
seed = randomSeed();
}
const template = babelTemplate('__getRandomObject(SEED)');
return template({
SEED: _numericLiteral(seed),
}).expression;
}
function randomProperty(identifier, seed) {
if (seed === undefined) {
seed = randomSeed();
}
const template = babelTemplate('__getRandomProperty(IDENTIFIER, SEED)');
return template({
IDENTIFIER: identifier,
SEED: _numericLiteral(seed),
}).expression;
}
function randomArguments(path) {
const numArgs = random.randInt(0, MAX_ARGUMENT_COUNT);
const args = [];
for (let i = 0; i < numArgs; i++) {
args.push(randomValue(path));
}
return args.map(_unwrapExpressionStatement);
}
function randomValue(path) {
const probability = random.random();
if (probability < 0.01) {
const randomFunc = randomFunction(path);
if (randomFunc) {
return randomFunc;
}
}
if (probability < 0.25) {
const randomVar = randomVariable(path);
if (randomVar) {
return randomVar;
}
}
if (probability < 0.5) {
return randomInterestingNumber();
}
if (probability < 0.75) {
return randomInterestingNonNumber();
}
return randomObject();
}
function callRandomFunction(path, identifier, seed) {
if (seed === undefined) {
seed = randomSeed();
}
let args = [
identifier,
_numericLiteral(seed)
];
args = args.map(_unwrapExpressionStatement);
args = args.concat(randomArguments(path));
return babelTypes.callExpression(
babelTypes.identifier('__callRandomFunction'),
args);
}
function nearbyRandomNumber(value) {
const probability = random.random();
if (probability < 0.9) {
return _numericLiteral(value + random.randInt(-0x10, 0x10));
} else if (probability < 0.95) {
return _numericLiteral(value + random.randInt(-0x100, 0x100));
} else if (probability < 0.99) {
return _numericLiteral(value + random.randInt(-0x1000, 0x1000));
}
return _numericLiteral(value + random.randInt(-0x10000, 0x10000));
}
function randomInterestingNumber() {
const value = random.single(INTERESTING_NUMBER_VALUES);
if (random.choose(0.05)) {
return nearbyRandomNumber(value);
}
return _numericLiteral(value);
}
function randomInterestingNonNumber() {
return babylon.parseExpression(random.single(INTERESTING_NON_NUMBER_VALUES));
}
function concatFlags(inputs) {
const flags = new Set();
for (const input of inputs) {
for (const flag of input.flags || []) {
flags.add(flag);
}
}
return Array.from(flags.values());
}
function concatPrograms(inputs) {
// Concatentate programs.
const resultProgram = babelTypes.program([]);
const result = babelTypes.file(resultProgram, [], null);
for (const input of inputs) {
const ast = input.ast.program;
resultProgram.body = resultProgram.body.concat(ast.body);
resultProgram.directives = resultProgram.directives.concat(ast.directives);
}
// TODO(machenbach): Concat dependencies here as soon as they are cached.
const combined = new sourceHelpers.ParsedSource(
result, '', '', concatFlags(inputs));
// If any input file is sloppy, the combined result is sloppy.
combined.sloppy = inputs.some(input => input.isSloppy());
return combined;
}
function setSourceLoc(source, index, total) {
const noop = babelTypes.noop();
noop.__loc = index / total;
noop.__self = noop;
source.ast.program.body.unshift(noop);
}
function getSourceLoc(node) {
// Source location is invalid in cloned nodes.
if (node !== node.__self) {
return undefined;
}
return node.__loc;
}
function setOriginalPath(source, originalPath) {
const noop = babelTypes.noop();
noop.__path = originalPath;
noop.__self = noop;
source.ast.program.body.unshift(noop);
}
function getOriginalPath(node) {
// Original path is invalid in cloned nodes.
if (node !== node.__self) {
return undefined;
}
return node.__path;
}
function getOriginalPath(node) {
// Original path is invalid in cloned nodes.
if (node !== node.__self) {
return undefined;
}
return node.__path;
}
function setLargeLoop(node) {
node.__is_large_loop = true;
}
function isLargeLoop(node) {
return Boolean(node.__is_large_loop);
}
function setContainsYield(node) {
node.__contains_yield = true;
}
function containsYield(node) {
return Boolean(node.__contains_yield);
}
// Estimate the size of a node in raw source characters.
function isLargeNode(node) {
// Ignore array holes inserted by us (null) or previously cloned nodes
// (they have no start/end).
if (!node || node.start === undefined || node.end === undefined ) {
return false;
}
return node.end - node.start > LARGE_NODE_SIZE;
}
module.exports = {
STRICT_EXECUTION_ORDER_PROB: STRICT_EXECUTION_ORDER_PROB,
callRandomFunction: callRandomFunction,
concatFlags: concatFlags,
concatPrograms: concatPrograms,
containsYield: containsYield,
availableVariables: availableVariables,
availableFunctions: availableFunctions,
availableFunctionNames: availableFunctionNames,
randomFunction: randomFunction,
randomVariable: randomVariable,
isInForLoopCondition: isInForLoopCondition,
isInWhileLoop: isInWhileLoop,
isInfiniteLoop: isInfiniteLoop,
isLargeLoop: isLargeLoop,
isLargeNode: isLargeNode,
isVariableIdentifier: isVariableIdentifier,
isFunctionIdentifier: isFunctionIdentifier,
nearbyRandomNumber: nearbyRandomNumber,
randomArguments: randomArguments,
randomInterestingNonNumber: randomInterestingNonNumber,
randomInterestingNumber: randomInterestingNumber,
randomObject: randomObject,
randomProperty: randomProperty,
randomSeed: randomSeed,
randomValue: randomValue,
setContainsYield: setContainsYield,
setLargeLoop: setLargeLoop,
getOriginalPath: getOriginalPath,
setOriginalPath: setOriginalPath,
getSourceLoc: getSourceLoc,
setSourceLoc: setSourceLoc,
}

View File

@ -0,0 +1,182 @@
// Copyright 2020 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.
/**
* @fileoverview Expression mutator.
*/
'use strict';
const babelTemplate = require('@babel/template').default;
const babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
const babylon = require('@babel/parser');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
const sourceHelpers = require('../source_helpers.js');
const tryCatch = require('./try_catch.js');
const BABYLON_RELAXED_SUPER_OPTIONS = Object.assign(
{}, sourceHelpers.BABYLON_OPTIONS);
BABYLON_RELAXED_SUPER_OPTIONS['allowSuperOutsideMethod'] = true;
// Drop desired try-catch wrapping only with a small probability.
const WRAP_TC_IF_NEEDED_PROB = 0.8;
/**
* Check if a snippet containing `super` can be inserted at `path`
* without creating a syntax error.
*
* Syntax errors would be raised if super is used within a class that
* doesn't have a superclass or if a super() call is used outside of
* a constructor. Member expressions like super.x are allowed.
*/
function validateSuper(path, source) {
const ast = babylon.parse(source, BABYLON_RELAXED_SUPER_OPTIONS);
// TODO(389069288): The DB currently only stores if a snippet contains
// super. We could move this further differentiation into the DB.
// But since it's rarely used, we just re-parse the snippet here on the fly
// for now.
// Furthermore: Some snippets might actually provide a self-contained
// `super`, properly enclosed by an extending class. For these we could be
// more permissive, but they are rare.
let call = false;
babelTraverse(ast, {
Super(path) {
if (babelTypes.isCallExpression(path.parent)) {
call = true;
}
},
});
const surroundingMethod = path.findParent(x => x.isClassMethod());
const surroundingClass = surroundingMethod.findParent(x => x.isClass());
return surroundingClass && surroundingClass.node.superClass &&
(!call || surroundingMethod.node.kind == 'constructor');
}
/**
* Checks if the expression contains a member expression with an identifier.
*
* These get extra try-catch wrapping as such accesses are often wrong out of
* context. The try-catch mutator doesn't always wrap all expressions.
*/
function needsTryCatch(source) {
const ast = babylon.parse(source, BABYLON_RELAXED_SUPER_OPTIONS);
// TODO(389069288): We could precalculate this and store it in the DB
// together with the snippets.
let member = false;
babelTraverse(ast, {
MemberExpression(path) {
if (babelTypes.isIdentifier(path.node.property)) {
member = true;
}
},
});
return member;
}
class CrossOverMutator extends mutator.Mutator {
constructor(settings, db) {
super(settings);
this._db = db;
}
// For testing.
db() {
return this._db;
}
createInsertion(path, expression) {
if (expression.needsSuper &&
!validateSuper(path, expression.source)) {
return undefined;
}
// Insert the statement.
let toInsert = babelTemplate(
expression.source,
sourceHelpers.BABYLON_REPLACE_VAR_OPTIONS);
const dependencies = {};
const expressionDependencies = expression.dependencies;
if (expressionDependencies) {
const variables = common.availableVariables(path);
if (variables.length < expressionDependencies.length) {
return undefined;
}
const chosenVariables = random.sample(
variables, expressionDependencies.length);
for (const [index, dependency] of expressionDependencies.entries()) {
dependencies[dependency] = chosenVariables[index];
}
}
try {
toInsert = toInsert(dependencies);
} catch (e) {
if (this.settings.testing) {
// Fail early in tests.
throw e;
}
console.log('ERROR: Failed to parse:', expression.source);
console.log(e);
return undefined;
}
if (random.choose(WRAP_TC_IF_NEEDED_PROB) &&
needsTryCatch(expression.source)) {
toInsert = tryCatch.wrapTryCatch(toInsert);
}
this.annotate(
toInsert,
'Crossover from ' + expression.originalPath);
return toInsert;
}
get visitor() {
const thisMutator = this;
return [{
ForStatement(path) {
// Cross-over insertions in large loops often lead to timeouts.
if (common.isLargeLoop(path.node) &&
random.choose(mutator.SKIP_LARGE_LOOP_MUTATION_PROB)) {
path.skip();
}
},
ExpressionStatement(path) {
if (!random.choose(thisMutator.settings.MUTATE_CROSSOVER_INSERT)) {
return;
}
const canHaveSuper = Boolean(path.findParent(x => x.isClassMethod()));
const randomExpression = thisMutator.db().getRandomStatement(
{canHaveSuper: canHaveSuper});
const toInsert = thisMutator.createInsertion(path, randomExpression);
if (!toInsert) return;
if (random.choose(0.5)) {
thisMutator.insertBeforeSkip(path, toInsert);
} else {
thisMutator.insertAfterSkip(path, toInsert);
}
path.skip();
},
}, {
}];
}
}
module.exports = {
CrossOverMutator: CrossOverMutator,
};

View File

@ -0,0 +1,225 @@
// Copyright 2020 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.
/**
* @fileoverview Mutator for differential fuzzing.
*/
'use strict';
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
// Templates for various statements.
const incCaught = babelTemplate('__caught++;');
const printValue = babelTemplate('print(VALUE);');
const printCaught = babelTemplate('print("Caught: " + __caught);');
const printHash = babelTemplate('print("Hash: " + __hash);');
const prettyPrint = babelTemplate('__prettyPrint(ID);');
const prettyPrintExtra = babelTemplate('__prettyPrintExtra(ID);');
// This section prefix is expected by v8_foozzie.py. Existing prefixes
// (e.g. from CrashTests) are cleaned up with CLEANED_PREFIX.
const SECTION_PREFIX = 'v8-foozzie source: ';
const CLEANED_PREFIX = 'v***************e: ';
/**
* Babel statement for calling deep printing from the fuzz library.
*/
function prettyPrintStatement(variable) {
return prettyPrint({ ID: babelTypes.cloneDeep(variable) });
}
/**
* As above, but using the "extra" variant, which will reduce printing
* after too many calls to prevent I/O flooding.
*/
function prettyPrintExtraStatement(variable) {
return prettyPrintExtra({ ID: babelTypes.cloneDeep(variable) });
}
/**
* Mutator for suppressing known and/or unfixable issues.
*/
class DifferentialFuzzSuppressions extends mutator.Mutator {
get visitor() {
let thisMutator = this;
return {
// Clean up strings containing the magic section prefix. Those can come
// e.g. from CrashTests and would confuse the deduplication in
// v8_foozzie.py.
StringLiteral(path) {
if (path.node.value.startsWith(SECTION_PREFIX)) {
const postfix = path.node.value.substring(SECTION_PREFIX.length);
path.node.value = CLEANED_PREFIX + postfix;
thisMutator.annotate(path.node, 'Replaced magic string');
}
},
// Known precision differences: https://crbug.com/380147861
BinaryExpression(path) {
if (path.node.operator == '**') {
path.node.operator = '+';
thisMutator.annotate(path.node, 'Replaced **');
}
},
// Unsupported language feature: https://crbug.com/1020573
MemberExpression(path) {
if (path.node.property.name == "arguments") {
let replacement = common.randomVariable(path);
if (!replacement) {
replacement = babelTypes.thisExpression();
}
thisMutator.annotate(replacement, 'Replaced .arguments');
thisMutator.replaceWithSkip(path, replacement);
}
},
};
}
}
/**
* Mutator for tracking original input files and for extra printing.
*/
class DifferentialFuzzMutator extends mutator.Mutator {
constructor(settings) {
super();
this.settings = settings;
}
/**
* Looks for the dummy node that marks the beginning of an input file
* from the corpus.
*/
isSectionStart(path) {
return !!common.getOriginalPath(path.node);
}
/**
* Create print statements for printing the magic section prefix that's
* expected by v8_foozzie.py to differentiate different source files.
*/
getSectionHeader(path) {
const orig = common.getOriginalPath(path.node);
return printValue({
VALUE: babelTypes.stringLiteral(SECTION_PREFIX + orig),
});
}
/**
* Create statements for extra printing at the end of a section. We print
* the number of caught exceptions, a generic hash of all observed values
* and the contents of all variables in scope.
*/
getSectionFooter(path) {
const variables = common.availableVariables(path);
const statements = variables.map(prettyPrintStatement);
statements.unshift(printCaught());
statements.unshift(printHash());
const statement = babelTypes.tryStatement(
babelTypes.blockStatement(statements),
babelTypes.catchClause(
babelTypes.identifier('e'),
babelTypes.blockStatement([])));
this.annotate(statement, 'Print variables and exceptions from section');
return statement;
}
/**
* Helper for printing the contents of several variables.
*/
printVariables(path, nodes) {
const statements = [];
for (const node of nodes) {
if (!babelTypes.isIdentifier(node) ||
!common.isVariableIdentifier(node.name))
continue;
statements.push(prettyPrintExtraStatement(node));
}
if (statements.length) {
this.annotate(statements[0], 'Extra variable printing');
this.insertAfterSkip(path, statements);
}
}
get visitor() {
const thisMutator = this;
const settings = this.settings;
return {
// Replace existing normal print statements with deep printing.
CallExpression(path) {
if (babelTypes.isIdentifier(path.node.callee) &&
path.node.callee.name == 'print') {
path.node.callee = babelTypes.identifier('__prettyPrintExtra');
thisMutator.annotate(path.node, 'Pretty printing');
}
},
// Either print or track caught exceptions, guarded by a probability.
CatchClause(path) {
const probability = random.random();
if (probability < settings.DIFF_FUZZ_EXTRA_PRINT &&
path.node.param &&
babelTypes.isIdentifier(path.node.param)) {
const statement = prettyPrintExtraStatement(path.node.param);
path.node.body.body.unshift(statement);
} else if (probability < settings.DIFF_FUZZ_TRACK_CAUGHT) {
path.node.body.body.unshift(incCaught());
}
},
// Insert section headers and footers between the contents of two
// original source files. We detect the dummy no-op nodes that were
// previously tagged with the original path of the file.
Noop(path) {
if (!thisMutator.isSectionStart(path)) {
return;
}
const header = thisMutator.getSectionHeader(path);
const footer = thisMutator.getSectionFooter(path);
thisMutator.insertBeforeSkip(path, footer);
thisMutator.insertBeforeSkip(path, header);
},
// Additionally we print one footer in the end.
Program: {
exit(path) {
const footer = thisMutator.getSectionFooter(path);
path.node.body.push(footer);
},
},
// Print contents of variables after assignments, guarded by a
// probability.
ExpressionStatement(path) {
if (!babelTypes.isAssignmentExpression(path.node.expression) ||
!random.choose(settings.DIFF_FUZZ_EXTRA_PRINT)) {
return;
}
const left = path.node.expression.left;
if (babelTypes.isMemberExpression(left)) {
thisMutator.printVariables(path, [left.object]);
} else {
thisMutator.printVariables(path, [left]);
}
},
// Print contents of variables after declaration, guarded by a
// probability.
VariableDeclaration(path) {
if (babelTypes.isLoop(path.parent) ||
!random.choose(settings.DIFF_FUZZ_EXTRA_PRINT)) {
return;
}
const identifiers = path.node.declarations.map(decl => decl.id);
thisMutator.printVariables(path, identifiers);
},
};
}
}
module.exports = {
DifferentialFuzzMutator: DifferentialFuzzMutator,
DifferentialFuzzSuppressions: DifferentialFuzzSuppressions,
};

View File

@ -0,0 +1,61 @@
// Copyright 2020 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.
/**
* @fileoverview Expression mutator.
*/
'use strict';
const babelTypes = require('@babel/types');
const random = require('../random.js');
const mutator = require('./mutator.js');
class ExpressionMutator extends mutator.Mutator {
get visitor() {
const thisMutator = this;
return {
ExpressionStatement(path) {
if (!random.choose(thisMutator.settings.MUTATE_EXPRESSIONS)) {
return;
}
const probability = random.random();
if (probability < 0.7) {
const repeated = babelTypes.cloneDeep(path.node);
thisMutator.annotate(repeated, 'Repeated');
thisMutator.insertBeforeSkip(path, repeated);
} else if (path.key > 0) {
// Get a random previous sibling.
const prev = path.getSibling(random.randInt(0, path.key - 1));
if (!prev || !prev.node) {
return;
}
// Either select a previous or the current node to clone. Bias
// towards the former, as the other way can lead to use of undefined
// variables.
const [selected, destination] = random.single(
[[prev, path], [prev, path], [prev, path], [path, prev]]);
if (selected.isDeclaration()) {
return;
}
const cloned = babelTypes.cloneDeep(selected.node);
thisMutator.annotate(cloned, 'Cloned sibling');
if (random.choose(0.5)) {
thisMutator.insertBeforeSkip(destination, cloned);
} else {
thisMutator.insertAfterSkip(destination, cloned);
}
}
},
};
}
}
module.exports = {
ExpressionMutator: ExpressionMutator,
};

View File

@ -0,0 +1,181 @@
// Copyright 2020 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.
/**
* @fileoverview Function calls mutator.
*/
'use strict';
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
const REPLACE_CROSS_ASYNC_PROB = 0.2;
function _liftExpressionsToStatements(path, nodes) {
// If the node we're replacing is an expression in an expression statement,
// lift the replacement nodes into statements too.
if (!babelTypes.isExpressionStatement(path.parent)) {
return nodes;
}
return nodes.map(n => babelTypes.expressionStatement(n));
}
/**
* All functions in the path's scope that keep the same async property.
* A different property (e.g. sync to async) is used only with a small
* probability. Additionally this doesn't include the current function.
*/
function availableReplacementFunctionNames(path, context) {
const available = common.availableFunctionNames(path);
const oldFunc = path.node.callee.name;
// The available set is a fresh set which we can mutate here.
available.delete(oldFunc);
// Disregard infinitely running functions.
for (const name of context.infiniteFunctions.values()) {
available.delete(name);
}
// If we don't care to maintain the async property, just use all functions.
if (random.choose(module.exports.REPLACE_CROSS_ASYNC_PROB)) {
return Array.from(available.values());
}
// Return an array with functions of the same async property as the current.
const oldIsAsync = context.asyncFunctions.has(oldFunc);
let result = new Array();
for (const name of available.values()) {
if (oldIsAsync == context.asyncFunctions.has(name)) {
result.push(name);
}
}
return result;
}
class FunctionCallMutator extends mutator.Mutator {
get visitor() {
const thisMutator = this;
return {
CallExpression(path) {
if (!babelTypes.isIdentifier(path.node.callee)) {
return;
}
if (!common.isFunctionIdentifier(path.node.callee.name)) {
return;
}
if (!random.choose(thisMutator.settings.MUTATE_FUNCTION_CALLS)) {
return;
}
const probability = random.random();
if (probability < 0.3) {
const replacement = random.single(availableReplacementFunctionNames(
path, thisMutator.context));
if (replacement) {
thisMutator.annotate(
path.node,
`Replaced ${path.node.callee.name} with ${replacement}`);
path.node.callee = babelTypes.identifier(replacement);
}
} else if (probability < 0.7 && thisMutator.settings.engine == 'v8') {
const prepareTemplate = babelTemplate(
'__V8BuiltinPrepareFunctionForOptimization(ID)');
const optimizationMode = random.choose(0.7) ? 'Function' : 'Maglev';
const optimizeTemplate = babelTemplate(
`__V8BuiltinOptimize${optimizationMode}OnNextCall(ID)`);
const nodes = [
prepareTemplate({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
babelTypes.cloneDeep(path.node),
babelTypes.cloneDeep(path.node),
optimizeTemplate({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
];
thisMutator.annotate(
path.node,
`Optimizing ${path.node.callee.name}`);
if (!babelTypes.isExpressionStatement(path.parent)) {
nodes.push(path.node);
thisMutator.replaceWithSkip(
path, babelTypes.sequenceExpression(nodes));
} else {
thisMutator.insertBeforeSkip(
path, _liftExpressionsToStatements(path, nodes));
}
} else if (probability < 0.8 && thisMutator.settings.engine == 'v8') {
const template = babelTemplate(
'__V8BuiltinCompileBaseline(ID)');
const nodes = [
template({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
];
thisMutator.annotate(
nodes[0],
`Compiling baseline ${path.node.callee.name}`);
if (!babelTypes.isExpressionStatement(path.parent)) {
nodes.push(path.node);
thisMutator.replaceWithSkip(
path, babelTypes.sequenceExpression(nodes));
} else {
thisMutator.insertBeforeSkip(
path, _liftExpressionsToStatements(path, nodes));
}
} else if (probability < 0.9 &&
thisMutator.settings.engine == 'v8') {
const template = babelTemplate(
'__V8BuiltinDeoptimizeFunction(ID)');
const insert = _liftExpressionsToStatements(path, [
template({
ID: babelTypes.cloneDeep(path.node.callee),
}).expression,
]);
thisMutator.annotate(
path.node,
`Deoptimizing ${path.node.callee.name}`);
thisMutator.insertAfterSkip(path, insert);
} else {
const template = babelTemplate(
'runNearStackLimit(() => { return CALL });');
thisMutator.annotate(
path.node,
`Run to stack limit ${path.node.callee.name}`);
thisMutator.replaceWithSkip(
path,
template({
CALL: path.node,
}).expression);
}
path.skip();
},
}
}
}
module.exports = {
REPLACE_CROSS_ASYNC_PROB: REPLACE_CROSS_ASYNC_PROB,
FunctionCallMutator: FunctionCallMutator,
};

View File

@ -0,0 +1,125 @@
// Copyright 2020 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.
/**
* @fileoverview Mutator
*/
'use strict';
const babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
const SKIP_LARGE_LOOP_MUTATION_PROB = 0.75;
/**
* Container for any state that lives throughout one fuzz-test output
* generation. Can be used to collect information during parsing and
* use it later in mutators.
*/
class MutationContext {
constructor () {
this.asyncFunctions = new Set();
this.infiniteFunctions = new Set();
this.extraResources = new Set();
this.loopVariables = new Set();
}
}
const EMPTY_DEFAULT_CONTEXT = new MutationContext();
class Mutator {
constructor(settings) {
this.settings = settings;
this.context = EMPTY_DEFAULT_CONTEXT;
}
get visitor() {
return null;
}
_traverse(ast, visitor) {
let oldEnter = null;
if (Object.prototype.hasOwnProperty.call(visitor, 'enter')) {
oldEnter = visitor['enter'];
}
// Transparently skip nodes that are marked.
visitor['enter'] = (path) => {
if (this.shouldSkip(path.node)) {
path.skip();
return;
}
if (oldEnter) {
oldEnter(path);
}
}
babelTraverse(ast, visitor);
}
mutate(source, context=EMPTY_DEFAULT_CONTEXT) {
this.context = context;
const result = this.visitor;
if (Array.isArray(result)) {
for (const visitor of result) {
this._traverse(source.ast, visitor);
}
} else {
this._traverse(source.ast, result);
}
}
get _skipPropertyName() {
return '__skip' + this.constructor.name;
}
shouldSkip(node) {
return Boolean(node[this._skipPropertyName]);
}
skipMutations(node) {
// Mark a node to skip further mutations of the same kind.
if (Array.isArray(node)) {
for (const item of node) {
item[this._skipPropertyName] = true;
}
} else {
node[this._skipPropertyName] = true;
}
return node;
}
insertBeforeSkip(path, node) {
this.skipMutations(node);
path.insertBefore(node);
}
insertAfterSkip(path, node) {
this.skipMutations(node);
path.insertAfter(node);
}
replaceWithSkip(path, node) {
this.skipMutations(node);
path.replaceWith(node);
}
replaceWithMultipleSkip(path, node) {
this.skipMutations(node);
path.replaceWithMultiple(node);
}
annotate(node, message) {
babelTypes.addComment(
node, 'leading', ` ${this.constructor.name}: ${message} `);
}
}
module.exports = {
SKIP_LARGE_LOOP_MUTATION_PROB: SKIP_LARGE_LOOP_MUTATION_PROB,
Mutator: Mutator,
MutationContext: MutationContext,
}

View File

@ -0,0 +1,122 @@
// Copyright 2020 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.
/**
* @fileoverview Normalizer.
* This renames variables so that we don't have collisions when combining
* different files. It also simplifies other logic when e.g. determining the
* type of an identifier. This also normalizes all loop bodies with blocks.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
class NormalizerContext {
constructor() {
this.funcIndex = 0;
this.varIndex = 0;
this.classIndex = 0;
}
}
class IdentifierNormalizer extends mutator.Mutator {
constructor() {
super();
this.normalizerContext = new NormalizerContext();
}
get visitor() {
const normalizerContext = this.normalizerContext;
const renamed = new WeakSet();
const globalMappings = new Map();
const loopStatement = {
enter(path) {
if (path.node.body && !babelTypes.isBlockStatement(path.node.body)) {
path.node.body = babelTypes.blockStatement([path.node.body]);
}
}
};
return [{
Scope(path) {
for (const [name, binding] of Object.entries(path.scope.bindings)) {
if (renamed.has(binding.identifier)) {
continue;
}
renamed.add(binding.identifier);
// Prefix with __JSF in the first pass to avoid collisions with
// leftover identifier names from old fuzzer output. A collision with
// __JSF would be extremely rare, hence we don't add extra logic.
if (babelTypes.isClassDeclaration(binding.path.node) ||
babelTypes.isClassExpression(binding.path.node)) {
path.scope.rename(name, '__JSF__c_' + normalizerContext.classIndex++);
} else if (babelTypes.isFunctionDeclaration(binding.path.node) ||
babelTypes.isFunctionExpression(binding.path.node)) {
path.scope.rename(name, '__JSF__f_' + normalizerContext.funcIndex++);
} else {
path.scope.rename(name, '__JSF__v_' + normalizerContext.varIndex++);
}
}
},
AssignmentExpression(path) {
// Find assignments for which we have no binding in the scope. We assume
// that these are globals which are local to our script (which weren't
// declared with var/let/const etc).
const ids = path.getBindingIdentifiers();
for (const name in ids) {
if (!path.scope.getBinding(name)) {
globalMappings.set(name, '__v_' + normalizerContext.varIndex++);
}
}
},
WhileStatement: loopStatement,
DoWhileStatement: loopStatement,
ForStatement: loopStatement,
}, {
// Second pass to re-normalize already renamed identifiers to their final
// JS_Fuzzer name pattern.
Scope(path) {
for (const [name, binding] of Object.entries(path.scope.bindings)) {
if (name.startsWith('__JSF__')) {
path.scope.rename(name, name.substring(5));
}
}
},
// Second pass to rename globals that weren't declared with
// var/let/const etc.
Identifier(path) {
if (!globalMappings.has(path.node.name)) {
return;
}
if (path.scope.getBinding(path.node.name)) {
// Don't rename if there is a binding that hides the global.
return;
}
if (path.node.name === "constructor" &&
babelTypes.isClassMethod(path.parent) &&
path.parent.key == path.node) {
// Don't touch constructors.
return;
}
path.node.name = globalMappings.get(path.node.name);
},
}];
}
}
module.exports = {
IdentifierNormalizer: IdentifierNormalizer,
};

View File

@ -0,0 +1,128 @@
// Copyright 2020 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.
/**
* @fileoverview Numbers mutator.
*/
'use strict';
const babelTypes = require('@babel/types');
const arrayMutator = require('./array_mutator.js');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
const MIN_SAFE_INTEGER = -9007199254740991;
const MAX_SAFE_INTEGER = 9007199254740991;
// Large arrays cause too many mutations, which also wastes time in this
// visitor. Often, large arrays are output from Binaryen, which gets
// invalidated easily.
const SKIP_LARGE_ARRAYS_PROB = 0.9;
function isMethodOrObjectKey(path) {
return (path.parent &&
(babelTypes.isObjectMember(path.parent) ||
babelTypes.isClassMethod(path.parent) ||
babelTypes.isClassProperty(path.parent)) &&
path.parent.key === path.node);
}
function isExponentiationBase(path) {
return (path.parent &&
babelTypes.isBinaryExpression(path.parent) &&
path.parent.operator === '**' &&
path.parent.left === path.node);
}
function createRandomNumber(value) {
// TODO(ochang): Maybe replace with variable.
const probability = random.random();
if (probability < 0.01) {
return babelTypes.numericLiteral(
random.randInt(MIN_SAFE_INTEGER, MAX_SAFE_INTEGER));
} else if (probability < 0.06) {
return common.randomInterestingNumber();
} else {
return common.nearbyRandomNumber(value);
}
}
class NumberMutator extends mutator.Mutator {
ignore(path) {
return !random.choose(this.settings.MUTATE_NUMBERS) ||
common.isInForLoopCondition(path) ||
common.isInWhileLoop(path);
}
randomReplace(path, value, forcePositive=false) {
const randomNumber = createRandomNumber(value);
if (forcePositive) {
randomNumber.value = Math.abs(randomNumber.value);
}
this.annotate(
path.node,
`Replaced ${value} with ${randomNumber.value}`);
this.replaceWithSkip(path, randomNumber);
}
get visitor() {
const thisMutator = this;
return {
ArrayExpression(path) {
if (path.node.elements.length > arrayMutator.MAX_ARRAY_LENGTH &&
random.choose(module.exports.SKIP_LARGE_ARRAYS_PROB)) {
path.skip();
}
},
NumericLiteral(path) {
if (thisMutator.ignore(path)) {
return;
}
// We handle negative unary expressions separately to replace the whole
// expression below. E.g. -5 is UnaryExpression(-, NumericLiteral(5)).
if (path.parent && babelTypes.isUnaryExpression(path.parent) &&
path.parent.operator === '-') {
return;
}
// Enfore positive numbers if the literal is the key of an object
// property or method. Negative keys cause syntax errors.
// TODO(389069288): We also enforce a positive base for
// exponentiations as stand-alone negative numbers cause syntax errors.
// We could support this case by constructing a negative number as an
// UnaryExpression (with -) wrapping a number, which should get
// Babel to parenthesize the expression.
const forcePositive = (
isMethodOrObjectKey(path) || isExponentiationBase(path));
thisMutator.randomReplace(path, path.node.value, forcePositive);
},
UnaryExpression(path) {
if (thisMutator.ignore(path)) {
return;
}
// Handle the case we ignore above.
if (path.node.operator === '-' &&
babelTypes.isNumericLiteral(path.node.argument)) {
thisMutator.randomReplace(path, -path.node.argument.value);
}
}
};
}
}
module.exports = {
SKIP_LARGE_ARRAYS_PROB: SKIP_LARGE_ARRAYS_PROB,
NumberMutator: NumberMutator,
};

View File

@ -0,0 +1,130 @@
// Copyright 2020 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.
/**
* @fileoverview Mutator for object expressions.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
const MAX_PROPERTIES = 50;
/**
* Turn the key of an object property into a string literal.
*/
function keyToString(key) {
if (babelTypes.isNumericLiteral(key)) {
return babelTypes.stringLiteral(key.value.toString());
}
if (babelTypes.isIdentifier(key)) {
return babelTypes.stringLiteral(key.name);
}
// Already a string literal.
return key;
}
class ObjectMutator extends mutator.Mutator {
get visitor() {
const thisMutator = this;
return {
ObjectExpression(path) {
const properties = path.node.properties;
if (!random.choose(thisMutator.settings.MUTATE_OBJECTS) ||
properties.length > MAX_PROPERTIES) {
return;
}
// Use the indices of object properties for mutations. We ignore
// getters and setters.
const propertyIndicies = [];
for (const [index, property] of properties.entries()) {
if (babelTypes.isObjectProperty(property)) {
propertyIndicies.push(index);
}
}
// The mutations below require at least one property.
if (!propertyIndicies.length) {
return;
}
// Annotate object expression with the action taken.
function annotate(message) {
thisMutator.annotate(path.node, message);
}
function getOneRandomProperty() {
return properties[random.single(propertyIndicies)];
}
function getTwoRandomProperties() {
const [a, b] = random.sampleOfTwo(propertyIndicies);
return [properties[a], properties[b]];
}
function swapPropertyValues() {
if (propertyIndicies.length > 1) {
annotate('Swap properties');
const [a, b] = getTwoRandomProperties();
[a.value, b.value] = [b.value, a.value];
}
}
function duplicatePropertyValue() {
if (propertyIndicies.length > 1) {
const [a, b] = random.shuffle(getTwoRandomProperties());
if (common.isLargeNode(b.value)) {
return;
}
annotate('Duplicate a property value');
a.value = babelTypes.cloneDeep(b.value);
}
}
function insertRandomValue() {
annotate('Insert a random value');
const property = getOneRandomProperty();
property.value = common.randomValue(path);
}
function stringifyKey() {
annotate('Stringify a property key');
const property = getOneRandomProperty();
property.key = keyToString(property.key);
}
function removeProperty() {
annotate('Remove a property');
properties.splice(random.single(propertyIndicies), 1);
}
// Mutation options. Repeated mutations have a higher probability.
const mutations = [
swapPropertyValues,
swapPropertyValues,
duplicatePropertyValue,
duplicatePropertyValue,
insertRandomValue,
insertRandomValue,
removeProperty,
stringifyKey,
];
// Perform mutation.
random.single(mutations)();
},
}
}
}
module.exports = {
ObjectMutator: ObjectMutator,
};

View File

@ -0,0 +1,269 @@
// Copyright 2020 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.
/**
* @fileoverview Try catch wrapper.
*/
const assert = require('assert');
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const common = require('./common.js');
const mutator = require('./mutator.js');
const random = require('../random.js');
// Default target probability for skipping try-catch completely.
const DEFAULT_SKIP_PROB = 0.05;
// Default target probability to wrap only on toplevel, i.e. to not nest
// try-catch.
const DEFAULT_TOPLEVEL_PROB = 0.5;
// Probability to deviate from defaults and use extreme cases.
const IGNORE_DEFAULT_PROB = 0.03;
// We don't support 'using' and 'async using'. We wrap var with try-catch.
const WRAPPABLE_DECL_KINDS = new Set(['let', 'const']);
// This function is defined in resources/fuzz_library.js.
const WRAP_FUN = babelTemplate('__wrapTC(() => ID)');
function isFunction(node) {
return (babelTypes.isArrowFunctionExpression(node) ||
babelTypes.isFunctionExpression(node) ||
babelTypes.isFunctionDeclaration(node));
}
function isFunctionBody(path) {
const parent = path.parent;
return parent && isFunction(parent) && parent.body == path.node;
}
function hasFunctionDeclaration(body) {
return body.some((node) => babelTypes.isFunctionDeclaration(node));
}
function _rawTryCatch(node, catchBlock=[]) {
return babelTypes.tryStatement(
node,
babelTypes.catchClause(
babelTypes.identifier('e'),
babelTypes.blockStatement(catchBlock)));
}
function wrapTryCatch(node) {
return _rawTryCatch(babelTypes.blockStatement([node]));
}
function wrapBlockWithTryCatch(block) {
assert(block.body);
assert(block.body.length); // We don't wrap empty blocks.
const lastStatement = block.body.at(-1);
// If the block ended with the return of an object expression,
// we also return an empty object expression in the catch block,
// which reduces problems if the returned value is destructured.
let catchBlock = [];
if (babelTypes.isReturnStatement(lastStatement) &&
lastStatement.argument &&
babelTypes.isObjectExpression(lastStatement.argument)) {
catchBlock.push(
babelTypes.returnStatement(babelTypes.objectExpression([])));
}
return babelTypes.blockStatement([_rawTryCatch(block, catchBlock)]);
}
function skipReplaceVariableDeclarator(path) {
return (
// Uninitialized variable.
!path.node.init ||
// Simple initialization with a literal.
babelTypes.isLiteral(path.node.init) ||
// Wrapping a yield expression with an arrow function is
// syntactically wrong.
common.containsYield(path.node) ||
// Initialization with undefined.
(babelTypes.isIdentifier(path.node.init) &&
path.node.init.name == 'undefined') ||
// Consistency check.
!babelTypes.isVariableDeclaration(path.parent) ||
// Don't wrap variables in loop declarations.
babelTypes.isLoop(path.parentPath.parent) ||
// Only wrap supported kinds.
!WRAPPABLE_DECL_KINDS.has(path.parent.kind))
}
function replaceVariableDeclarator(path) {
let wrapped;
if (babelTypes.isAwaitExpression(path.node.init)) {
// The await can't remain in the inner arrow function of WRAP_FUN,
// we pull it outside of the wrapper instead. E.g.
// "await x" becomes "await __wrapTC(() => x)".
wrapped = babelTypes.AwaitExpression(
WRAP_FUN({ID: path.node.init.argument}).expression);
} else {
wrapped = WRAP_FUN({ID: path.node.init}).expression;
}
path.replaceWith(babelTypes.variableDeclarator(path.node.id, wrapped));
path.skip();
}
function replaceAndSkip(path) {
if (!babelTypes.isLabeledStatement(path.parent) ||
!babelTypes.isLoop(path.node)) {
// Don't wrap loops with labels as it makes continue
// statements syntactically invalid. We wrap the label
// instead below.
path.replaceWith(wrapTryCatch(path.node));
}
// Prevent infinite looping.
path.skip();
}
function replaceBlockStatementAndSkip(path) {
path.replaceWith(wrapBlockWithTryCatch(path.node));
path.skip();
}
class AddTryCatchMutator extends mutator.Mutator {
callWithProb(path, fun) {
const probability = random.random();
if (probability < this.skipProb * this.loc) {
// Entirely skip try-catch wrapper.
path.skip();
} else if (probability < (this.skipProb + this.toplevelProb)) {
// Only wrap on top-level.
fun(path);
}
}
get visitor() {
const thisMutator = this;
const accessStatement = {
enter(path) {
thisMutator.callWithProb(path, replaceAndSkip);
},
exit(path) {
// Apply nested wrapping (is only executed if not skipped above).
replaceAndSkip(path);
}
};
const handleInfiniteLoops = {
enter(path) {
// Just replace the outer loop and don't progress on the inside if
// it's an infinite loop, since sometimes exceptions are used to
// break out of the loop.
if (common.isInfiniteLoop(path.node)) {
replaceAndSkip(path);
}
},
};
return {
Program: {
enter(path) {
// Track original source location fraction in [0, 1).
thisMutator.loc = 0;
// Target probability for skipping try-catch.
thisMutator.skipProb = module.exports.DEFAULT_SKIP_PROB;
// Target probability for not nesting try-catch.
thisMutator.toplevelProb = module.exports.DEFAULT_TOPLEVEL_PROB;
// Maybe deviate from target probability for the entire test.
if (random.choose(module.exports.IGNORE_DEFAULT_PROB)) {
thisMutator.skipProb = random.uniform(0, 0.5);
thisMutator.toplevelProb = random.uniform(0, 1);
thisMutator.annotate(
path.node,
'Target skip probability ' + thisMutator.skipProb +
' and toplevel probability ' + thisMutator.toplevelProb);
}
}
},
Noop: {
enter(path) {
if (common.getSourceLoc(path.node)) {
thisMutator.loc = common.getSourceLoc(path.node);
}
},
},
BlockStatement(path) {
// It'd be superfluous to wrap existing try statements.
if (path.parent && babelTypes.isTryStatement(path.parent)) {
path.skip();
return;
}
// Wrap the entire body of a function instead of descending into all
// the function's nodes. This also includes return statements.
// We only do this with the lower top-level wrapping probability and
// don't provide an exit() function here. I.e. if we did descent into
// the child nodes, we don't additionally wrap the body.
// Don't wrap if there's a nested function declaration in the body as
// this might break legacy scope compatibility rules.
if (isFunctionBody(path) &&
path.node.body &&
path.node.body.length &&
!hasFunctionDeclaration(path.node.body)) {
thisMutator.callWithProb(path, replaceBlockStatementAndSkip);
}
},
DoWhileStatement: handleInfiniteLoops,
ExpressionStatement: accessStatement,
IfStatement: accessStatement,
LabeledStatement: {
enter(path) {
// Apply an extra try-catch around the label of a loop, since we
// ignore the loop itself if it has a label.
if (babelTypes.isLoop(path.node.body)) {
thisMutator.callWithProb(path, replaceAndSkip);
}
},
exit(path) {
// Apply nested wrapping (is only executed if not skipped above).
if (babelTypes.isLoop(path.node.body)) {
replaceAndSkip(path);
}
},
},
// This covers {While|DoWhile|ForIn|ForOf|For}Statement.
Loop: accessStatement,
SwitchStatement: accessStatement,
VariableDeclaration: {
enter(path) {
if (path.node.kind !== 'var' || babelTypes.isLoop(path.parent))
return;
thisMutator.callWithProb(path, replaceAndSkip);
},
exit(path) {
if (path.node.kind !== 'var' || babelTypes.isLoop(path.parent))
return;
// Apply nested wrapping (is only executed if not skipped above).
replaceAndSkip(path);
}
},
VariableDeclarator: {
enter(path) {
if (skipReplaceVariableDeclarator(path))
return;
thisMutator.callWithProb(path, replaceVariableDeclarator);
},
exit(path) {
if (skipReplaceVariableDeclarator(path))
return;
// Apply nested wrapping (is only executed if not skipped above).
replaceVariableDeclarator(path);
}
},
WhileStatement: handleInfiniteLoops,
WithStatement: accessStatement,
};
}
}
module.exports = {
DEFAULT_SKIP_PROB: DEFAULT_SKIP_PROB,
DEFAULT_TOPLEVEL_PROB: DEFAULT_TOPLEVEL_PROB,
IGNORE_DEFAULT_PROB: IGNORE_DEFAULT_PROB,
AddTryCatchMutator: AddTryCatchMutator,
wrapTryCatch: wrapTryCatch,
}

View File

@ -0,0 +1,104 @@
// Copyright 2020 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.
/**
* @fileoverview Variables mutator.
*/
'use strict';
const babelTypes = require('@babel/types');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
function _isInFunctionParam(path) {
const child = path.find(p => p.parent && babelTypes.isFunction(p.parent));
return child && child.parentKey === 'params';
}
// Skip possible loop-variable mutations with a high probability.
const SKIP_LOOP_VAR_PROB = 0.95;
/**
* Returns true if path appears on the left-hand side of a variable declarator.
*
* This also includes nesting:
* let lhs_var = rhs_var;
* let {prop: lhs_var} = rhs_var;
*/
function _isBeingDeclared(path) {
const child = path.find(
p => p.parent && babelTypes.isVariableDeclarator(p.parent));
return child && child.parent.id == child.node;
}
class VariableMutator extends mutator.Mutator {
get visitor() {
const thisMutator = this;
return {
AssignmentExpression(path) {
// Don't change assignments to loop variables, also outside of loop
// conditions.
if (babelTypes.isIdentifier(path.node.left) &&
common.isVariableIdentifier(path.node.left.name) &&
thisMutator.context.loopVariables.has(path.node.left.name) &&
random.choose(module.exports.SKIP_LOOP_VAR_PROB)) {
path.skip();
}
},
Identifier(path) {
if (!random.choose(thisMutator.settings.MUTATE_VARIABLES)) {
return;
}
if (!common.isVariableIdentifier(path.node.name)) {
return;
}
// Don't mutate variables on the left-hand side of a declaration.
if (_isBeingDeclared(path)) {
return;
}
// Don't mutate function params.
if (_isInFunctionParam(path)) {
return;
}
if (common.isInForLoopCondition(path) ||
common.isInWhileLoop(path)) {
return;
}
const randVar = common.randomVariable(path);
if (!randVar) {
return;
}
const newName = randVar.name;
// Don't assign to loop variables.
if (babelTypes.isAssignmentExpression(path.parent) &&
path.parent.left == path.node &&
thisMutator.context.loopVariables.has(newName) &&
random.choose(module.exports.SKIP_LOOP_VAR_PROB)) {
return;
}
thisMutator.annotate(
path.node,
`Replaced ${path.node.name} with ${newName}`);
path.node.name = newName;
}
};
}
}
module.exports = {
SKIP_LOOP_VAR_PROB: SKIP_LOOP_VAR_PROB,
VariableMutator: VariableMutator,
};

View File

@ -0,0 +1,183 @@
// Copyright 2020 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.
/**
* @fileoverview Variables mutator.
*/
'use strict';
const babelTemplate = require('@babel/template').default;
const babelTypes = require('@babel/types');
const common = require('./common.js');
const random = require('../random.js');
const mutator = require('./mutator.js');
const variableMutator = require('../mutators/variable_mutator.js');
const MAX_MUTATION_RECURSION_DEPTH = 5;
const CHOOSE_MAJOR_GC_PROB = 0.7;
// Stub for testing.
function chooseCallGC() {
return random.choose(0.3);
}
/**
* Append a GC call to an expression with a probability.
*/
function maybeGCTemplate(expression) {
let templ = expression;
if (module.exports.chooseCallGC()) {
if (random.choose(CHOOSE_MAJOR_GC_PROB)){
templ += ', __callGC(true)';
} else {
templ += ', __callGC(false)';
}
}
return babelTemplate(templ);
}
class VariableOrObjectMutator extends mutator.Mutator {
_randomVariableOrObject(path) {
const randomVar = common.randomVariable(path);
if (random.choose(0.05) || !randomVar) {
return common.randomObject();
}
return randomVar;
}
_randomVariableOrObjectMutations(path, recurseDepth=0) {
if (recurseDepth >= MAX_MUTATION_RECURSION_DEPTH) {
return new Array();
}
const probability = random.random();
if (probability < 0.3) {
const first = this._randomVariableOrObjectMutations(path, recurseDepth + 1);
const second = this._randomVariableOrObjectMutations(
path, recurseDepth + 1);
return first.concat(second);
}
const randVarOrObject = this._randomVariableOrObject(path);
const randProperty = common.randomProperty(randVarOrObject);
let newRandVarOrObject = randVarOrObject;
if (random.choose(0.2)) {
newRandVarOrObject = this._randomVariableOrObject(path);
}
const mutations = new Array();
if (probability < 0.4) {
const template = maybeGCTemplate('delete IDENTIFIER[PROPERTY]')
mutations.push(template({
IDENTIFIER: randVarOrObject,
PROPERTY: randProperty
}));
} else if (probability < 0.5) {
const template = maybeGCTemplate('IDENTIFIER[PROPERTY]')
mutations.push(template({
IDENTIFIER: randVarOrObject,
PROPERTY: randProperty
}));
} else if (probability < 0.6) {
const template = maybeGCTemplate('IDENTIFIER[PROPERTY] = RANDOM')
mutations.push(template({
IDENTIFIER: randVarOrObject,
PROPERTY: randProperty,
RANDOM: common.randomValue(path),
}));
} else if (probability < 0.7) {
mutations.push(
babelTypes.expressionStatement(
common.callRandomFunction(path, randVarOrObject)));
} else if (probability < 0.8) {
const template = maybeGCTemplate('VAR = IDENTIFIER')
var randomVar = common.randomVariable(path);
if (!randomVar) {
return mutations;
}
// Don't assign to loop variables.
if (this.context.loopVariables.has(randomVar.name) &&
random.choose(variableMutator.SKIP_LOOP_VAR_PROB)) {
return mutations;
}
mutations.push(template({
VAR: randomVar,
IDENTIFIER: randVarOrObject,
}));
} else if (probability < 0.9) {
const template = babelTemplate(
'if (IDENTIFIER != null && typeof(IDENTIFIER) == "object") ' +
'Object.defineProperty(IDENTIFIER, PROPERTY, {value: VALUE})')
mutations.push(template({
IDENTIFIER: newRandVarOrObject,
PROPERTY: randProperty,
VALUE: common.randomValue(path),
}));
} else {
const template = babelTemplate(
'if (IDENTIFIER != null && typeof(IDENTIFIER) == "object") ' +
'Object.defineProperty(IDENTIFIER, PROPERTY, {' +
'get: function() { GETTER_MUTATION ; return VALUE; },' +
'set: function(value) { SETTER_MUTATION; }' +
'})');
mutations.push(template({
IDENTIFIER: newRandVarOrObject,
PROPERTY: randProperty,
GETTER_MUTATION: this._randomVariableOrObjectMutations(
path, recurseDepth + 1),
SETTER_MUTATION: this._randomVariableOrObjectMutations(
path, recurseDepth + 1),
VALUE: common.randomValue(path),
}));
}
return mutations;
}
get visitor() {
const settings = this.settings;
const thisMutator = this;
return {
ForStatement(path) {
// Var/obj mutations in large loops often lead to timeouts.
if (common.isLargeLoop(path.node) &&
random.choose(mutator.SKIP_LARGE_LOOP_MUTATION_PROB)) {
path.skip();
}
},
ExpressionStatement(path) {
if (!random.choose(settings.ADD_VAR_OR_OBJ_MUTATIONS)) {
return;
}
const mutations = thisMutator._randomVariableOrObjectMutations(path);
thisMutator.annotate(mutations[0], 'Random mutation');
if (random.choose(0.5)) {
thisMutator.insertBeforeSkip(path, mutations);
} else {
thisMutator.insertAfterSkip(path, mutations);
}
path.skip();
}
};
}
}
module.exports = {
VariableOrObjectMutator: VariableOrObjectMutator,
chooseCallGC: chooseCallGC,
};

3742
deps/v8/tools/clusterfuzz/js_fuzzer/package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
{
"name": "ochang_js_fuzzer",
"version": "1.0.0",
"description": "",
"main": "run.js",
"scripts": {
"test": "APP_NAME=d8 mocha"
},
"bin": "run.js",
"author": "ochang@google.com",
"license": "ISC",
"dependencies": {
"@babel/generator": "^7.26.10",
"@babel/template": "^7.26.9",
"@babel/traverse": "^7.6.0",
"@babel/types": "^7.26.10",
"@babel/parser": "^7.26.10",
"commander": "^2.11.0",
"dir-compare": "^5.0.0",
"globals": "^10.1.0",
"tempfile": "^3.0.0",
"tempy": "^0.5.0"
},
"devDependencies": {
"eslint": "^6.8.0",
"mocha": "^3.5.3",
"pkg": "^5.8.1",
"prettier": "2.0.5",
"sinon": "^4.0.0"
},
"pkg": {
"assets": "resources/**/*"
}
}

View File

@ -0,0 +1,32 @@
#!/bin/bash
# Copyright 2020 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.
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
OS="linux"
OS_LABEL="Linux"
SUFFIX=""
if [[ -n "$1" && $1 == "win" ]]; then
OS="win"
OS_LABEL="Windows"
SUFFIX=".exe"
elif [[ -n "$1" && $1 == "macos" ]]; then
OS="macos"
OS_LABEL="MacOS"
fi
echo "Building and packaging for $OS_LABEL..."
(set -x; $DIR/node_modules/.bin/pkg -t node18-$OS-x64 $DIR)
rm -rf $DIR/output > /dev/null 2>&1 || true
rm $DIR/output.zip > /dev/null 2>&1 || true
mkdir $DIR/output
cd $DIR/output
ln -s ../db db
ln -s ../ochang_js_fuzzer$SUFFIX run$SUFFIX
ln -s ../foozzie_launcher.py foozzie_launcher.py
echo "Creating $DIR/output.zip"
(set -x; zip -r $DIR/output.zip * > /dev/null)

View File

@ -0,0 +1,128 @@
// Copyright 2020 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.
/**
* @fileoverview Random helpers.
*/
'use strict';
const assert = require('assert');
function randInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function choose(probability) {
return Math.random() < probability;
}
function random() {
return Math.random();
}
function uniform(min, max) {
return Math.random() * (max - min) + min;
}
function swap(array, p1, p2) {
[array[p1], array[p2]] = [array[p2], array[p1]];
}
/**
* Returns "count" elements, randomly selected from "highProbArray" and
* "lowProbArray". Elements from highProbArray have a "factor" times
* higher chance to be chosen. As a side effect, this swaps the chosen
* elements to the end of the respective input arrays. The complexity is
* O(count).
*/
function twoBucketSample(lowProbArray, highProbArray, factor, count) {
// Track number of available elements for choosing.
let low = lowProbArray.length;
let high = highProbArray.length;
assert(low + high >= count);
const result = [];
for (let i = 0; i < count; i++) {
// Map a random number to the summarized indices of both arrays. Give
// highProbArray elements a "factor" times higher probability.
const p = random();
const index = Math.floor(p * (high * factor + low));
if (index < low) {
// If the index is in the low part, draw the element and discard it.
result.push(lowProbArray[index]);
swap(lowProbArray, index, --low);
} else {
// Same as above but for a highProbArray element. The index is first
// mapped back to the array's range.
const highIndex = Math.floor((index - low) / factor);
result.push(highProbArray[highIndex]);
swap(highProbArray, highIndex, --high);
}
}
return result;
}
/**
* Returns a single random element from an array.
*/
function single(array) {
return array[randInt(0, array.length - 1)];
}
/**
* Returns an array with a sample of two distinct elements from an array.
*/
function sampleOfTwo(array) {
assert(array.length >= 2);
const first = randInt(0, array.length - 1);
let second = randInt(1, array.length - 1);
if (first == second) {
// On a collision, we simulate a swap.
second = 0;
}
return [array[first], array[second]];
}
/**
* As above for an arbitrary number of elements with a fast implementation
* for 1 or 2 elements.
*/
function sample(array, count) {
assert(count > 0);
assert(array.length >= count);
// Fast paths for 1 or 2 elements.
if (count == 1) {
return [single(array)];
}
if (count == 2) {
return sampleOfTwo(array);
}
const copy = Array.from(array);
shuffle(copy);
return copy.slice(0, count);
}
function shuffle(array) {
for (let i = 0; i < array.length - 1; i++) {
const j = randInt(i, array.length - 1);
swap(array, i, j);
}
return array;
}
module.exports = {
choose: choose,
randInt: randInt,
random: random,
sample: sample,
sampleOfTwo: sampleOfTwo,
shuffle: shuffle,
single: single,
twoBucketSample: twoBucketSample,
uniform: uniform,
}

View File

@ -0,0 +1,9 @@
// Copyright 2025 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.
try {
AsyncIterator;
} catch (e) {
this.AsyncIterator = class AsyncIterator extends Iterator {}
}

View File

@ -0,0 +1,11 @@
// Copyright 2025 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.
// WasmSpec tests in the chakra testsuite use a lot of test(() => ...)
// patterns.
function test(fun) {
try {
fun();
} catch(e) {}
}

View File

@ -0,0 +1,8 @@
// Copyright 2025 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.
let a = 0;
let b = 0;
console.log(42);

View File

@ -0,0 +1,16 @@
// Copyright 2025 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.
let a = 0;
let b = 0;
class A {
}
class B extends A {
constructor() {
console.log(42);
}
}

View File

@ -0,0 +1,16 @@
// Copyright 2025 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.
let a = 0;
let b = 0;
class A {
foo() {}
}
class B extends A {
foo() {
console.log(42);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2020 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.
// Adjust chakra behavior for differential fuzzing.
this.WScript = new Proxy({}, {
get(target, name) {
switch (name) {
case 'Echo':
return __prettyPrintExtra;
default:
return {};
}
}
});

View File

@ -0,0 +1,11 @@
// Copyright 2020 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.
function debug(msg) {
__prettyPrintExtra(msg);
}
function shouldBe(_a) {
__prettyPrintExtra((typeof _a == "function" ? _a() : eval(_a)));
}

View File

@ -0,0 +1,122 @@
// Copyright 2020 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.
// Helpers for printing in correctness fuzzing.
// Global helper functions for printing.
var __prettyPrint;
var __prettyPrintExtra;
// Track caught exceptions.
var __caught = 0;
// Track a hash of all printed values - printing is cut off after a
// certain size.
var __hash = 0;
(function() {
const charCodeAt = String.prototype.charCodeAt;
const join = Array.prototype.join;
const map = Array.prototype.map;
const substring = String.prototype.substring;
const toString = Object.prototype.toString;
// Same as in mjsunit.js.
const classOf = function(object) {
// Argument must not be null or undefined.
const string = toString.call(object);
// String has format [object <ClassName>].
return substring.call(string, 8, string.length - 1);
};
// For standard cases use original prettyPrinted from mjsunit.
const origPrettyPrinted = prettyPrinted;
// Override prettyPrinted with a version that also recusively prints objects
// and arrays with a depth of 4. We don't track circles, but we'd cut off
// after a depth of 4 if there are any.
prettyPrinted = function prettyPrinted(value, depth=4) {
if (depth <= 0) {
return "...";
}
switch (typeof value) {
case "object":
if (value === null) return "null";
switch (classOf(value)) {
case "Array":
return prettyPrintedArray(value, depth);
case "Object":
return prettyPrintedObject(value, depth);
}
}
// Fall through to original version for all other types.
return origPrettyPrinted(value);
}
// Helper for pretty array with depth.
function prettyPrintedArray(array, depth) {
const result = map.call(array, (value, index, array) => {
if (value === undefined && !(index in array)) return "";
return prettyPrinted(value, depth - 1);
});
return `[${join.call(result, ", ")}]`;
}
// Helper for pretty objects with depth.
function prettyPrintedObject(object, depth) {
const keys = Object.keys(object);
const prettyValues = map.call(keys, (key) => {
return `${key}: ${prettyPrinted(object[key], depth - 1)}`;
});
const content = join.call(prettyValues, ", ");
return `${object.constructor.name || "Object"}{${content}}`;
}
// Helper for calculating a hash code of a string.
function hashCode(str) {
let hash = 0;
if (str.length == 0) {
return hash;
}
for (let i = 0; i < str.length; i++) {
const char = charCodeAt.call(str, i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}
// Upper limit for calling extra printing. When reached, hashes of
// strings are tracked and printed instead.
let maxExtraPrinting = 100;
// Helper for pretty printing.
__prettyPrint = function(value, extra=false) {
let str = prettyPrinted(value);
// Change __hash with the contents of the full string to
// keep track of differences also when we don't print.
const hash = hashCode(str);
__hash = hashCode(hash + __hash.toString());
if (extra && maxExtraPrinting-- <= 0) {
return;
}
// Cut off long strings to prevent overloading I/O. We still track
// the hash of the full string.
if (str.length > 64) {
const head = substring.call(str, 0, 54);
const tail = substring.call(str, str.length - 10, str.length - 1);
str = `${head}[...]${tail}`;
}
print(str);
};
__prettyPrintExtra = function (value) {
__prettyPrint(value, true);
}
})();

View File

@ -0,0 +1,8 @@
// Copyright 2020 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.
// Substitute for mjsunit. We reuse prettyPrinted from mjsunit, but only if
// it is loaded. If not, we use this substitute instead.
let prettyPrinted = value => value;

View File

@ -0,0 +1,12 @@
// Copyright 2020 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.
// Don't breach stack limit in differential fuzzing as it leads to
// early bailout.
runNearStackLimit = function(f) {
try {
f();
} catch (e) {}
};

View File

@ -0,0 +1,29 @@
// Copyright 2020 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.
// Adjust mjsunit behavior for differential fuzzing.
// We're not interested in stack traces.
MjsUnitAssertionError = () => {};
// Do more printing in assertions for more correctness coverage.
failWithMessage = message => { __prettyPrint(message); };
assertSame = (expected, found, name_opt) => { __prettyPrint(found); };
assertNotSame = (expected, found, name_opt) => { __prettyPrint(found); };
assertEquals = (expected, found, name_opt) => { __prettyPrint(found); };
assertNotEquals = (expected, found, name_opt) => { __prettyPrint(found); };
assertNull = (value, name_opt) => { __prettyPrint(value); };
assertNotNull = (value, name_opt) => { __prettyPrint(value); };
// Suppress optimization status as it leads to false positives.
assertUnoptimized = () => {};
assertOptimized = () => {};
isNeverOptimize = () => {};
isAlwaysOptimize = () => {};
isInterpreted = () => {};
isBaseline = () => {};
isUnoptimized = () => {};
isOptimized = () => {};
isTurboFanned = () => {};

View File

@ -0,0 +1,140 @@
// Copyright 2020 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.
// Slightly modified variants from http://code.fitness/post/2016/01/javascript-enumerate-methods.html.
function __isPropertyOfType(obj, name, type) {
let desc;
try {
desc = Object.getOwnPropertyDescriptor(obj, name);
} catch(e) {
return false;
}
if (!desc)
return false;
return typeof type === 'undefined' || typeof desc.value === type;
}
function __getProperties(obj, type) {
if (typeof obj === "undefined" || obj === null)
return [];
let properties = [];
for (let name of Object.getOwnPropertyNames(obj)) {
if (__isPropertyOfType(obj, name, type))
properties.push(name);
}
let proto = Object.getPrototypeOf(obj);
while (proto && proto != Object.prototype) {
Object.getOwnPropertyNames(proto)
.forEach (name => {
if (name !== 'constructor') {
if (__isPropertyOfType(proto, name, type))
properties.push(name);
}
});
proto = Object.getPrototypeOf(proto);
}
return properties;
}
function* __getObjects(root = this, level = 0) {
if (level > 4)
return;
let obj_names = __getProperties(root, 'object');
for (let obj_name of obj_names) {
let obj = root[obj_name];
if (obj === root)
continue;
yield obj;
yield* __getObjects(obj, level + 1);
}
}
var __getRandomObject;
{
let count = 0;
__getRandomObject = function(seed) {
if (count++ > 50) return this;
let objects = [];
for (let obj of __getObjects()) {
objects.push(obj);
}
return objects[seed % objects.length];
};
}
var __getRandomProperty;
{
let count = 0;
__getRandomProperty = function(obj, seed) {
if (count++ > 50) return undefined;
let properties = __getProperties(obj);
if (!properties.length)
return undefined;
return properties[seed % properties.length];
};
}
var __callRandomFunction;
{
let count = 0;
__callRandomFunction = function(obj, seed, ...args)
{
if (count++ > 25) return;
let functions = __getProperties(obj, 'function');
if (!functions.length)
return;
let random_function = functions[seed % functions.length];
try {
obj[random_function](...args);
} catch(e) { }
};
}
function runNearStackLimit(f) {
function t() {
try {
return t();
} catch (e) {
return f();
}
};
try {
return t();
} catch (e) {}
}
// Limit number of times we cause GCs in tests to reduce hangs
// when called within larger loops.
let __callGC;
(function() {
let countGC = 0;
__callGC = function(major) {
const type = {type: major ? 'major' : 'minor'};
if (countGC++ < 20) {
gc(type);
}
};
})();
function __wrapTC(f) {
try {
return f();
} catch (e) {}
}
// Neuter common test functions.
try { this.fail = nop; } catch(e) { }
try { this.failWithMessage = nop; } catch(e) { }
try { this.triggerAssertFalse = nop; } catch(e) { }
try { this.quit = nop; } catch(e) { }

View File

@ -0,0 +1,41 @@
// Copyright 2020 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.
// Minimally stub out methods from JSTest's standalone-pre.js.
function description(msg) {}
function debug(msg) {}
function shouldBe(_a) {
print((typeof _a == "function" ? _a() : eval(_a)));
}
function shouldBeTrue(_a) { shouldBe(_a); }
function shouldBeFalse(_a) { shouldBe(_a); }
function shouldBeNaN(_a) { shouldBe(_a); }
function shouldBeNull(_a) { shouldBe(_a); }
function shouldNotThrow(_a) { shouldBe(_a); }
function shouldThrow(_a) { shouldBe(_a); }
function noInline() {}
function finishJSTest() {}
// Stub out $vm.
try {
$vm;
} catch(e) {
const handler = {
get: function(x, prop) {
if (prop == Symbol.toPrimitive) {
return function() { return undefined; };
}
return dummy;
},
};
const dummy = new Proxy(function() { return dummy; }, handler);
this.$vm = dummy;
}
// Other functions.
function ensureArrayStorage() {}
function transferArrayBuffer() {}

View File

@ -0,0 +1,28 @@
// Copyright 2025 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.
// Stub for V8's memory-corruption API.
try {
Sandbox;
} catch (e) {
this.Sandbox = {
MemoryView: class MemoryView extends ArrayBuffer{},
base: undefined,
byteLength: 42,
getAddressOf: () => 0,
getObjectAt: () => undefined,
getSizeOf: () => 42,
getSizeOfObjectAt: () => 42,
isValidObjectAt: () => true,
isWritable: () => true,
isWritableObjectAt: () => true,
getInstanceTypeOf: () => undefined,
getInstanceTypeOfObjectAt: () => undefined,
getInstanceTypeIdOf: () => undefined,
getInstanceTypeIdOfObjectAt: () => undefined,
getInstanceTypeIdFor: () => 0,
getFieldOffset: () => 31,
};
}

View File

@ -0,0 +1,7 @@
// Copyright 2025 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.
// Required to make a spidermonkey dependency more permissive:
// spidermonkey/test/expected/import/files/local/smTempBranch/shell.js
var document = {all: undefined};

View File

@ -0,0 +1,39 @@
// Copyright 2020 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.
// Helper neuter function.
function nop() { return false; }
// Stubs for non-standard functions.
try { gc; } catch(e) {
this.gc = function () {
for (let i = 0; i < 10000; i++) {
let s = new String("AAAA" + Math.random());
}
}
}
try { uneval; } catch(e) { this.uneval = this.nop; }
// For Spidermonkey shell.
try { gcparam; } catch(e) { this.gcparam = this.nop; }
try {
// For Chakra tests.
WScript;
} catch(e) {
this.WScript = new Proxy({}, {
get(target, name) {
switch (name) {
case 'Echo':
return print;
default:
return {};
}
}
});
}
try { this.alert = console.log; } catch(e) { }
try { this.print = console.log; } catch(e) { }

View File

@ -0,0 +1,154 @@
// Copyright 2020 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.
/**
* @fileoverview Description of this file.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const program = require('commander');
const differentialScriptMutator = require('./differential_script_mutator.js');
const random = require('./random.js');
const runner = require('./runner.js');
const scriptMutator = require('./script_mutator.js');
const sourceHelpers = require('./source_helpers.js');
// Base implementations for default or differential fuzzing.
const SCRIPT_MUTATORS = {
db: scriptMutator.CrossScriptMutator,
default: scriptMutator.ScriptMutator,
foozzie: differentialScriptMutator.DifferentialScriptMutator,
foozzie_fuzzilli: differentialScriptMutator.FuzzilliDifferentialScriptMutator,
wasm: scriptMutator.WasmScriptMutator,
};
function collect(value, total) {
total.push(value);
return total;
}
function overrideSettings(settings, settingOverrides) {
for (const setting of settingOverrides) {
const parts = setting.split('=');
settings[parts[0]] = parseFloat(parts[1]);
}
}
function main() {
Error.stackTraceLimit = Infinity;
program
.version('0.0.1')
.option('-i, --input_dir <path>', 'Input directory.')
.option('-o, --output_dir <path>', 'Output directory.')
.option('-n, --no_of_files <n>', 'Output directory.', parseInt)
.option('-c, --mutate_corpus <name>', 'Mutate single files in a corpus.')
.option('-e, --extra_strict', 'Additionally parse files in strict mode.')
.option('-m, --mutate <path>', 'Mutate a file and output results.')
.option('-s, --setting [setting]', 'Settings overrides.', collect, [])
.option('-v, --verbose', 'More verbose printing.')
.option('-z, --zero_settings', 'Zero all settings.')
.parse(process.argv);
const settings = scriptMutator.defaultSettings();
if (program.zero_settings) {
for (const key of Object.keys(settings)) {
settings[key] = 0.0;
}
}
if (program.setting.length > 0) {
overrideSettings(settings, program.setting);
}
let app_name = process.env.APP_NAME;
if (app_name && app_name.endsWith('.exe')) {
app_name = app_name.substr(0, app_name.length - 4);
}
if (app_name === 'd8' ||
app_name === 'v8_simple_inspector_fuzzer' ||
app_name === 'v8_foozzie.py') {
// V8 supports running the raw d8 executable, the inspector fuzzer or
// the differential fuzzing harness 'foozzie'.
settings.engine = 'v8';
} else if (app_name === 'ch') {
settings.engine = 'chakra';
} else if (app_name === 'js') {
settings.engine = 'spidermonkey';
} else if (app_name === 'jsc') {
settings.engine = 'jsc';
} else {
console.log('ERROR: Invalid APP_NAME');
process.exit(1);
}
const mode = process.env.FUZZ_MODE || 'default';
assert(mode in SCRIPT_MUTATORS, `Unknown mode ${mode}`);
const mutator = new SCRIPT_MUTATORS[mode](settings);
if (program.mutate) {
const absPath = path.resolve(program.mutate);
const baseDir = path.dirname(absPath);
const fileName = path.basename(absPath);
const corpus = new sourceHelpers.BaseCorpus(baseDir);
const input = sourceHelpers.loadSource(
corpus, fileName, program.extra_strict);
const mutated = mutator.mutateMultiple([input]);
console.log(mutated.code);
return;
}
let testRunner;
if (program.mutate_corpus) {
testRunner = new runner.SingleCorpusRunner(
program.input_dir, program.mutate_corpus, program.extra_strict);
} else {
testRunner = new mutator.runnerClass(
program.input_dir, settings.engine, program.no_of_files);
}
for (const [i, inputs] of testRunner.enumerateInputs()) {
const outputPath = path.join(program.output_dir, 'fuzz-' + i + '.js');
const start = Date.now();
const paths = inputs.map(input => input.relPath);
try {
const mutated = mutator.mutateMultiple(inputs);
fs.writeFileSync(outputPath, mutated.code);
if (settings.engine === 'v8' && mutated.flags && mutated.flags.length > 0) {
const flagsPath = path.join(program.output_dir, 'flags-' + i + '.js');
fs.writeFileSync(flagsPath, mutated.flags.join(' '));
}
} catch (e) {
if (e.message.startsWith('ENOSPC')) {
console.log('ERROR: No space left. Bailing out...');
console.log(e);
return;
}
console.log(`ERROR: Exception during mutate: ${paths}`);
console.log(e);
continue;
} finally {
if (program.verbose) {
const duration = Date.now() - start;
console.log(`Mutating ${paths} took ${duration} ms.`);
}
}
if ((i + 1) % 10 == 0) {
console.log('Up to ', i + 1);
}
}
}
main();

View File

@ -0,0 +1,237 @@
// Copyright 2024 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.
/**
* @fileoverview Main execution flow.
*/
'use strict';
const path = require('path');
const db = require('./db.js');
const corpus = require('./corpus.js');
const random = require('./random.js');
const sourceHelpers = require('./source_helpers.js');
// Maximum number of test inputs to use for one fuzz test.
const MAX_TEST_INPUTS_PER_TEST = 10;
const MAX_WASM_TEST_INPUTS_PER_TEST = 5;
/**
* Returns an array of maxium `count` parsed input sources, randomly
* selected from a primary corpus and a list of secondary corpora.
*/
function getRandomInputs(primaryCorpus, secondaryCorpora, count) {
count = random.randInt(2, count);
// Choose 40%-80% of inputs from primary corpus.
const primaryCount = Math.floor(random.uniform(0.4, 0.8) * count);
count -= primaryCount;
let inputs = primaryCorpus.getRandomTestcases(primaryCount);
// Split remainder equally between the secondary corpora.
const secondaryCount = Math.floor(count / secondaryCorpora.length);
for (let i = 0; i < secondaryCorpora.length; i++) {
let currentCount = secondaryCount;
if (i == secondaryCorpora.length - 1) {
// Last one takes the remainder.
currentCount = count;
}
count -= currentCount;
if (currentCount) {
inputs = inputs.concat(
secondaryCorpora[i].getRandomTestcases(currentCount));
}
}
return random.shuffle(inputs);
}
class Runner {
*inputGen() {
throw new Error("Not implemented.");
}
*enumerateInputs() {
let i = 0;
for (const value of this.inputGen()) {
yield [i, value];
i++;
}
}
}
/**
* Runner that randomly selects a number of tests from all corpora.
*/
class RandomCorpusRunner extends Runner {
constructor(inputDir, primary, numFiles,
maxTestInputs=MAX_TEST_INPUTS_PER_TEST) {
super();
inputDir = path.resolve(inputDir);
this.primary = primary;
this.numFiles = numFiles;
this.maxTestInputs = maxTestInputs;
this.corpora = {
'v8': corpus.create(inputDir, 'v8'),
'chakra': corpus.create(inputDir, 'chakra'),
'spidermonkey': corpus.create(inputDir, 'spidermonkey'),
'jsc': corpus.create(inputDir, 'WebKit/JSTests'),
'crash': corpus.create(inputDir, 'CrashTests'),
};
}
*inputGen() {
const primary = this.corpora[this.primary];
const secondary = Object.values(this.corpora);
for (let i = 0; i < this.numFiles; i++) {
const inputs = getRandomInputs(
primary,
random.shuffle(secondary),
this.maxTestInputs);
if (inputs.length > 0) {
yield inputs;
}
}
}
}
/**
* Like above, including the Fuzzilli corpus.
*/
class RandomCorpusRunnerWithFuzzilli extends RandomCorpusRunner {
constructor(inputDir, primary, numFiles,
maxTestInputs=MAX_TEST_INPUTS_PER_TEST) {
super(inputDir, primary, numFiles, maxTestInputs);
this.corpora['fuzzilli'] = corpus.create(
inputDir, 'fuzzilli', false, this.corpora['v8']);
}
}
/**
* Runner that randomly selects Wasm cases from V8 and Fuzzilli.
*/
class RandomWasmCorpusRunner extends Runner {
constructor(inputDir, engine, numFiles,
maxTestInputs=MAX_WASM_TEST_INPUTS_PER_TEST) {
super();
this.numFiles = numFiles;
this.maxTestInputs = maxTestInputs;
// Bias a bit towards the V8 corpus.
const v8Corpus = corpus.create(inputDir, 'v8_wasm');
const fuzzilliCorpus = corpus.create(
inputDir, 'fuzzilli_wasm', false, v8Corpus);
this.corpora = [v8Corpus, v8Corpus, fuzzilliCorpus];
}
*inputGen() {
for (let i = 0; i < this.numFiles; i++) {
const count = random.randInt(1, this.maxTestInputs);
const inputs = [];
for (let j= 0; j < count; j++) {
inputs.push(...random.single(this.corpora).getRandomTestcases(1));
}
if (inputs.length > 0) {
yield inputs;
}
}
}
}
/**
* Runner that enumerates all tests from a particular corpus.
*/
class SingleCorpusRunner extends Runner {
constructor(inputDir, corpusName, extraStrict) {
super();
this.corpus = corpus.create(
path.resolve(inputDir), corpusName, extraStrict);
}
*inputGen() {
for (const input of this.corpus.getAllTestcases()) {
yield [input];
}
}
}
/**
* Runner that enumerates random cases from the Fuzzilli corpus without
* repeats and without cases from the crashes directory.
*/
class RandomFuzzilliNoCrashCorpusRunner extends Runner {
constructor(inputDir, engine, numFiles) {
super();
this.numFiles = numFiles;
// We need a V8 corpus placeholder only to cross-load dependencies
// from there, e.g. the wasm module builder.
const v8Corpus = corpus.create(inputDir, 'v8');
this.corpus = corpus.create(
inputDir, 'fuzzilli_no_crash', false, v8Corpus);
}
*inputGen() {
// The 'permittedFiles' are already shuffled. Just using the first x
// files is random enough.
for (const relPath of this.corpus.permittedFiles.slice(0, this.numFiles)) {
const source = this.corpus.loadTestcase(relPath, false, 'sloppy');
if (source) {
yield [source];
}
}
}
}
/**
* Runner that enumerates a fixture case combined with a snippet from the DB
* for validation.
*/
class FixtureRunner extends Runner {
constructor(inputDir, engine, numFiles) {
// This runner ignores the usual inputs and always iterates over all
// snippets from the DB.
super();
this.dbPath = path.join(process.cwd(), 'db');
}
*inputGen() {
const mutateDb = new db.MutateDb(this.dbPath);
for (const expression of mutateDb.iterateStatements()) {
let templates;
if (expression.needsSuper) {
// One template for an insertion in a constructor and one in a method.
templates = ['cross_over_template_2.js', 'cross_over_template_3.js'];
} else {
templates = ['cross_over_template_1.js'];
}
for (const tmplName of templates) {
const tmpl = sourceHelpers.loadSource(
sourceHelpers.BASE_CORPUS, path.join('resources', tmplName));
// We glue the expression to the source. It's otherwise rather hard to
// pass this through into the mutation phases.
tmpl.__expression = expression;
yield [tmpl];
}
}
}
}
module.exports = {
FixtureRunner: FixtureRunner,
RandomCorpusRunner: RandomCorpusRunner,
RandomCorpusRunnerWithFuzzilli: RandomCorpusRunnerWithFuzzilli,
RandomFuzzilliNoCrashCorpusRunner: RandomFuzzilliNoCrashCorpusRunner,
RandomWasmCorpusRunner: RandomWasmCorpusRunner,
SingleCorpusRunner: SingleCorpusRunner,
};

View File

@ -0,0 +1,425 @@
// Copyright 2020 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.
/**
* @fileoverview Script mutator.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
const common = require('./mutators/common.js');
const db = require('./db.js');
const exceptions = require('./exceptions.js');
const random = require('./random.js');
const runner = require('./runner.js');
const sourceHelpers = require('./source_helpers.js');
const { AddTryCatchMutator } = require('./mutators/try_catch.js');
const { ArrayMutator } = require('./mutators/array_mutator.js');
const { ClosureRemover } = require('./mutators/closure_remover.js');
const { ContextAnalyzer } = require('./mutators/analyzer.js');
const { CrossOverMutator } = require('./mutators/crossover_mutator.js');
const { ExpressionMutator } = require('./mutators/expression_mutator.js');
const { FunctionCallMutator } = require('./mutators/function_call_mutator.js');
const { IdentifierNormalizer } = require('./mutators/normalizer.js');
const { MutationContext } = require('./mutators/mutator.js');
const { NumberMutator } = require('./mutators/number_mutator.js');
const { ObjectMutator } = require('./mutators/object_mutator.js');
const { VariableMutator } = require('./mutators/variable_mutator.js');
const { VariableOrObjectMutator } = require('./mutators/variable_or_object_mutation.js');
const CHAKRA_WASM_MODULE_BUILDER_REL = 'chakra/WasmSpec/testsuite/harness/wasm-module-builder.js'
const CHAKRA_WASM_CONSTANTS_REL = 'chakra/WasmSpec/testsuite/harness/wasm-constants.js'
const V8_WASM_MODULE_BUILDER_REL = 'v8/test/mjsunit/wasm/wasm-module-builder.js';
const MAX_EXTRA_MUTATIONS = 5;
function defaultSettings() {
return {
ADD_VAR_OR_OBJ_MUTATIONS: 0.1,
DIFF_FUZZ_EXTRA_PRINT: 0.1,
DIFF_FUZZ_TRACK_CAUGHT: 0.4,
MUTATE_ARRAYS: 0.1,
MUTATE_CROSSOVER_INSERT: 0.05,
MUTATE_EXPRESSIONS: 0.1,
MUTATE_FUNCTION_CALLS: 0.1,
MUTATE_NUMBERS: 0.05,
MUTATE_OBJECTS: 0.1,
MUTATE_VARIABLES: 0.075,
SCRIPT_MUTATOR_EXTRA_MUTATIONS: 0.2,
SCRIPT_MUTATOR_SHUFFLE: 0.2,
// Probability to remove certain types of closures: Anonymous parameterless
// functions calling themselves, but not referencing themselves. These
// appear often appear in test input and subsequent mutations are more
// likely without these closures.
TRANSFORM_CLOSURES: 0.2,
};
}
/**
* Create a context with information, useful in subsequent analyses.
*/
function analyzeContext(source) {
const analyzer = new ContextAnalyzer();
const context = new MutationContext();
analyzer.mutate(source, context);
return context;
}
class Result {
constructor(code, flags) {
this.code = code;
this.flags = flags;
}
}
class ScriptMutator {
constructor(settings, db_path=undefined) {
// Use process.cwd() to bypass pkg's snapshot filesystem.
this.mutateDb = new db.MutateDb(db_path || path.join(process.cwd(), 'db'));
this.crossover = new CrossOverMutator(settings, this.mutateDb);
this.mutators = [
new ArrayMutator(settings),
new ObjectMutator(settings),
new VariableMutator(settings),
new NumberMutator(settings),
this.crossover,
new ExpressionMutator(settings),
new FunctionCallMutator(settings),
new VariableOrObjectMutator(settings),
];
this.closures = new ClosureRemover(settings);
this.trycatch = new AddTryCatchMutator(settings);
this.settings = settings;
}
/**
* Returns a runner class that decides the composition of tests from
* different corpora.
*/
get runnerClass() {
// Choose a setup with the Fuzzilli corpus with a 50% chance.
return random.single(
[runner.RandomCorpusRunner, runner.RandomCorpusRunnerWithFuzzilli]);
}
_addMjsunitIfNeeded(dependencies, input) {
if (dependencies.has('mjsunit')) {
return;
}
if (!input.absPath.includes('mjsunit')) {
return;
}
// Find mjsunit.js
let mjsunitPath = input.absPath;
while (path.dirname(mjsunitPath) != mjsunitPath &&
path.basename(mjsunitPath) != 'mjsunit') {
mjsunitPath = path.dirname(mjsunitPath);
}
if (path.basename(mjsunitPath) == 'mjsunit') {
mjsunitPath = path.join(mjsunitPath, 'mjsunit.js');
dependencies.set('mjsunit', sourceHelpers.loadDependencyAbs(
input.corpus, mjsunitPath));
return;
}
console.log('ERROR: Failed to find mjsunit.js');
}
_addSpiderMonkeyShellIfNeeded(dependencies, input) {
// Find shell.js files
const shellJsPaths = new Array();
let currentDir = path.dirname(input.absPath);
while (path.dirname(currentDir) != currentDir) {
const shellJsPath = path.join(currentDir, 'shell.js');
if (fs.existsSync(shellJsPath)) {
shellJsPaths.push(shellJsPath);
}
if (currentDir == 'spidermonkey') {
break;
}
currentDir = path.dirname(currentDir);
}
// Add shell.js dependencies in reverse to add ones that are higher up in
// the directory tree first.
for (let i = shellJsPaths.length - 1; i >= 0; i--) {
if (!dependencies.has(shellJsPaths[i])) {
const dependency = sourceHelpers.loadDependencyAbs(
input.corpus, shellJsPaths[i]);
dependencies.set(shellJsPaths[i], dependency);
}
}
}
_addStubsIfNeeded(dependencies, input, baseName, corpusDir) {
if (dependencies.has(baseName) || !input.absPath.includes(corpusDir)) {
return;
}
dependencies.set(baseName, sourceHelpers.loadResource(baseName + '.js'));
}
_addJSTestStubsIfNeeded(dependencies, input) {
this._addStubsIfNeeded(dependencies, input, 'jstest_stubs', 'JSTests');
}
_addChakraStubsIfNeeded(dependencies, input) {
this._addStubsIfNeeded(dependencies, input, 'chakra_stubs', 'chakra');
}
_addSpidermonkeyStubsIfNeeded(dependencies, input) {
this._addStubsIfNeeded(
dependencies, input, 'spidermonkey_stubs', 'spidermonkey');
}
mutate(source, context) {
let mutators = this.mutators.slice();
let annotations = [];
if (random.choose(this.settings.SCRIPT_MUTATOR_SHUFFLE)){
annotations.push(' Script mutator: using shuffled mutators');
random.shuffle(mutators);
}
if (random.choose(this.settings.SCRIPT_MUTATOR_EXTRA_MUTATIONS)){
for (let i = random.randInt(1, MAX_EXTRA_MUTATIONS); i > 0; i--) {
let mutator = random.single(this.mutators);
mutators.push(mutator);
annotations.push(` Script mutator: extra ${mutator.constructor.name}`);
}
}
// We always remove certain closures first.
mutators.unshift(this.closures);
// Try-catch wrapping should always be the last mutation.
mutators.push(this.trycatch);
for (const mutator of mutators) {
mutator.mutate(source, context);
}
for (const annotation of annotations.reverse()) {
sourceHelpers.annotateWithComment(source.ast, annotation);
}
}
/**
* Particular dependencies have precedence over others due to duplicate
* variable declarations in their sources.
*
* This is currently only implemented for the wasm-module-builder, which
* lives in V8 and in an older version in the Chakra test suite. It could
* be generalized for other cases.
*/
resolveCollisions(inputs) {
let hasWasmModuleBuilder = false;
inputs.forEach(input => {
hasWasmModuleBuilder |= input.dependentPaths.filter(
(x) => x.endsWith(V8_WASM_MODULE_BUILDER_REL)).length;
});
if (!hasWasmModuleBuilder) {
return;
}
inputs.forEach(input => {
input.dependentPaths = input.dependentPaths.filter(
(x) => !x.endsWith(CHAKRA_WASM_MODULE_BUILDER_REL) &&
!x.endsWith(CHAKRA_WASM_CONSTANTS_REL));
});
}
// Returns parsed dependencies for inputs.
resolveInputDependencies(inputs) {
const dependencies = new Map();
// Resolve test harness files.
inputs.forEach(input => {
try {
// TODO(machenbach): Some harness files contain load expressions
// that are not recursively resolved. We already remove them, but we
// also need to load the dependencies they point to.
this._addJSTestStubsIfNeeded(dependencies, input);
this._addChakraStubsIfNeeded(dependencies, input);
this._addMjsunitIfNeeded(dependencies, input);
this._addSpidermonkeyStubsIfNeeded(dependencies, input);
this._addSpiderMonkeyShellIfNeeded(dependencies, input);
} catch (e) {
console.log(
'ERROR: Failed to resolve test harness for', input.relPath);
throw e;
}
});
// Resolve dependencies loaded within the input files.
inputs.forEach(input => {
try {
input.loadDependencies(dependencies);
} catch (e) {
console.log(
'ERROR: Failed to resolve dependencies for', input.relPath);
throw e;
}
});
// Map.values() returns values in insertion order.
return Array.from(dependencies.values());
}
// Combines input dependencies with fuzzer resources.
resolveDependencies(inputs) {
this.resolveCollisions(inputs);
const dependencies = this.resolveInputDependencies(inputs);
// Add stubs for non-standard functions in the beginning.
dependencies.unshift(sourceHelpers.loadResource('stubs.js'));
// Add our fuzzing support helpers. This also overrides some common test
// functions from earlier dependencies that cause early bailouts.
dependencies.push(sourceHelpers.loadResource('fuzz_library.js'));
return dependencies;
}
concatInputs(inputs) {
return common.concatPrograms(inputs);
}
// Normalizes, combines and mutates multiple inputs.
mutateInputs(inputs, dependencies) {
const normalizerMutator = new IdentifierNormalizer();
for (const [index, input] of inputs.entries()) {
try {
normalizerMutator.mutate(input);
} catch (e) {
console.log('ERROR: Failed to normalize ', input.relPath);
throw e;
}
common.setSourceLoc(input, index, inputs.length);
}
// Combine ASTs into one. This is so that mutations have more context to
// cross over content between ASTs (e.g. variables).
const combinedSource = this.concatInputs(inputs);
// First pass for context information, then run other mutators.
const context = analyzeContext(combinedSource);
this.mutate(combinedSource, context);
// Add extra resources determined during mutation.
for (const resource of context.extraResources.values()) {
dependencies.push(sourceHelpers.loadResource(resource));
}
return combinedSource;
}
mutateMultiple(inputs) {
// High level operation:
// 1) Compute dependencies from inputs.
// 2) Normalize, combine and mutate inputs.
// 3) Generate code with dependency code prepended.
// 4) Combine and filter flags from inputs.
const dependencies = this.resolveDependencies(inputs);
const combinedSource = this.mutateInputs(inputs, dependencies);
const code = sourceHelpers.generateCode(combinedSource, dependencies);
const allFlags = common.concatFlags(dependencies.concat([combinedSource]));
const filteredFlags = exceptions.resolveContradictoryFlags(
exceptions.filterFlags(allFlags));
return new Result(code, filteredFlags);
}
}
/**
* Script mutator that only generates files depending on the
* wasm-module-builder with appropriate mutations.
*/
class WasmScriptMutator extends ScriptMutator {
constructor(settings, db_path) {
super(settings, db_path);
// Decrease cross-over and object mutations. Cross-over rarely
// works well with Wasm. Object mutations might easily invalidate the
// Wasm modules.
this.settings.MUTATE_CROSSOVER_INSERT = 0.01;
this.settings.MUTATE_OBJECTS = 0.05;
// Increase number, variable and function-call mutations, which often
// leave the underlying wasm-module-builder structures intact.
this.settings.MUTATE_NUMBERS = 0.1;
this.settings.MUTATE_VARIABLES = 0.1;
this.settings.MUTATE_FUNCTION_CALLS = 0.15;
// High likelihood to drop closures, as many wasm-module-builder cases
// are wrapped with those. After the transformation, subsequent
// mutations have more impact on the resulting code.
this.settings.TRANSFORM_CLOSURES = 0.5;
}
get runnerClass() {
return runner.RandomWasmCorpusRunner;
}
}
/**
* Script mutator that only inserts one cross-over expression from the DB to
* validate.
*/
class CrossScriptMutator extends ScriptMutator {
// We don't do any mutations except a deterministic insertion of one
// snippet into a predefined place in a template.
mutate(source, context) {
// The __expression was pinned to the expression in the FixtureRunner.
assert(source.__expression);
const crossover = this.crossover;
let done = false;
babelTraverse(source.ast, {
ExpressionStatement(path) {
if (done || !path.node.expression ||
!babelTypes.isCallExpression(path.node.expression)) {
return;
}
// Avoid infinite loops if there's an expression statement in the
// inserted expression.
done = true;
path.insertAfter(crossover.createInsertion(path, source.__expression));
}
});
}
// This mutator has only one input to which the __expression was pinned.
concatInputs(inputs) {
assert(inputs.length == 1);
return inputs[0];
}
// No dependencies needed for simple snippet evaluation.
resolveDependencies() {
return [];
}
get runnerClass() {
return runner.FixtureRunner;
}
}
module.exports = {
analyzeContext: analyzeContext,
defaultSettings: defaultSettings,
CrossScriptMutator: CrossScriptMutator,
ScriptMutator: ScriptMutator,
WasmScriptMutator: WasmScriptMutator,
};

View File

@ -0,0 +1,561 @@
// Copyright 2020 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.
/**
* @fileoverview Source loader.
*/
const assert = require('assert');
const fs = require('fs');
const fsPath = require('path');
const { EOL } = require('os');
const babelGenerator = require('@babel/generator').default;
const babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
const babylon = require('@babel/parser');
const exceptions = require('./exceptions.js');
const SCRIPT = Symbol('SCRIPT');
const MODULE = Symbol('MODULE');
const V8_BUILTIN_PREFIX = '__V8Builtin';
const V8_REPLACE_BUILTIN_REGEXP = new RegExp(
V8_BUILTIN_PREFIX + '(\\w+)\\(', 'g');
const V8_MJSUNIT_REL = fsPath.join('test', 'mjsunit');
const V8_MJSUNIT_HARNESS_REL = fsPath.join(V8_MJSUNIT_REL, 'mjsunit.js');
const BABYLON_OPTIONS = {
sourceType: 'script',
allowReturnOutsideFunction: true,
tokens: false,
ranges: false,
plugins: [
'doExpressions',
'explicitResourceManagement',
'exportDefaultFrom',
],
}
const BABYLON_REPLACE_VAR_OPTIONS = Object.assign({}, BABYLON_OPTIONS);
BABYLON_REPLACE_VAR_OPTIONS['placeholderPattern'] = /^VAR_[0-9]+$/;
function _idEquals(exp, name) {
return babelTypes.isIdentifier(exp) && exp.name == name;
}
function _isV8NewAPIExecute(exp) {
// exp is a member expression resolving to 'd8.file.execute'
return (_hasMemberProperty(exp, 'execute') &&
_hasMemberProperty(exp.object, 'file') &&
_idEquals(exp.object.object, 'd8'));
}
function _hasMemberProperty(exp, name) {
// 'exp' is a member expression with property <name>
return babelTypes.isMemberExpression(exp) && _idEquals(exp.property, name);
}
function _isV8OrSpiderMonkeyLoad(path) {
// 'load' and 'loadRelativeToScript' used by V8's old API and SpiderMonkey.
// 'd8.file.execute' used by V8's new API.
return ((_idEquals(path.node.callee, 'load') ||
_idEquals(path.node.callee, 'loadRelativeToScript') ||
_isV8NewAPIExecute(path.node.callee)) &&
path.node.arguments.length == 1 &&
babelTypes.isStringLiteral(path.node.arguments[0]));
}
function _isChakraLoad(path) {
// 'WScript.LoadScriptFile' used by Chakra.
// TODO(ochang): The optional second argument can change semantics ("self",
// "samethread", "crossthread" etc).
// Investigate whether if it still makes sense to include them.
return (babelTypes.isMemberExpression(path.node.callee) &&
babelTypes.isIdentifier(path.node.callee.property) &&
path.node.callee.property.name == 'LoadScriptFile' &&
path.node.arguments.length >= 1 &&
babelTypes.isStringLiteral(path.node.arguments[0]));
}
function _findPath(path, caseSensitive=true) {
// If the path exists, return the path. Otherwise return null. Used to handle
// case insensitive matches for Chakra tests.
if (caseSensitive) {
return fs.existsSync(path) ? path : null;
}
path = fsPath.normalize(fsPath.resolve(path));
const pathComponents = path.split(fsPath.sep);
let realPath = fsPath.resolve(fsPath.sep);
for (let i = 1; i < pathComponents.length; i++) {
// For each path component, do a directory listing to see if there is a case
// insensitive match.
const curListing = fs.readdirSync(realPath);
let realComponent = null;
for (const component of curListing) {
if (i < pathComponents.length - 1 &&
!fs.statSync(fsPath.join(realPath, component)).isDirectory()) {
continue;
}
if (component.toLowerCase() == pathComponents[i].toLowerCase()) {
realComponent = component;
break;
}
}
if (!realComponent) {
return null;
}
realPath = fsPath.join(realPath, realComponent);
}
return realPath;
}
function _findDependentCodePath(filePath, baseDirectory, caseSensitive=true) {
while (fsPath.dirname(baseDirectory) != baseDirectory) {
// Walk up the directory tree.
const testPath = fsPath.join(baseDirectory, filePath);
const realPath = _findPath(testPath, caseSensitive)
if (realPath) {
return realPath;
}
baseDirectory = fsPath.dirname(baseDirectory);
}
return null;
}
/**
* Resolves dependencies calculated from the sources and returns their string
* values in an array.
*
* Removes V8/Spidermonkey/Chakra load expressions in a source AST. Calculates
* if a Sandbox stub is needed.
*
* @param {string} originalFilePath Absolute path to file.
* @param {AST} ast Babel AST of the sources.
*/
function resolveDependencies(originalFilePath, ast) {
const dependencies = new Set();
const orphanedDeclarations = new Set();
babelTraverse(ast, {
CallExpression(path) {
const isV8OrSpiderMonkeyLoad = _isV8OrSpiderMonkeyLoad(path);
const isChakraLoad = _isChakraLoad(path);
if (!isV8OrSpiderMonkeyLoad && !isChakraLoad) {
return;
}
let loadValue = path.node.arguments[0].extra.rawValue;
// If the load we're removing initialized a variable, we need to also
// eliminate the entire declaration in the end.
if(path.parentPath.isVariableDeclarator() &&
path.parentPath.parentPath.isVariableDeclaration() &&
path.parentPath.node.init == path.node) {
// Using a set so that we don't attempt to remove a path twice.
orphanedDeclarations.add(path.parentPath.parentPath);
}
// Remove load call.
path.remove();
// Normalize Windows path separators.
loadValue = loadValue.replace(/\\/g, fsPath.sep);
// Ignore the mjsunit harness file as we already include an altered
// version for fuzzing.
if (loadValue == V8_MJSUNIT_HARNESS_REL) {
return;
}
// Locate mjsunit dependencies relative to the V8 corpus. Like that we
// resolve to a valid file even if the test file is from fuzzilli or
// crash tests.
if (loadValue.startsWith(V8_MJSUNIT_REL)) {
loadValue = fsPath.join('v8', loadValue);
}
const resolvedPath = _findDependentCodePath(
loadValue, fsPath.dirname(originalFilePath), !isChakraLoad);
if (!resolvedPath) {
console.log('ERROR: Could not find dependent path for', loadValue);
return;
}
if (exceptions.isTestSkippedAbs(resolvedPath)) {
// Dependency is skipped.
return;
}
// Add the dependency path.
dependencies.add(resolvedPath);
},
MemberExpression(path) {
const object = path.node.object;
if (babelTypes.isIdentifier(object) && object.name == 'Sandbox') {
dependencies.add(
fsPath.join(__dirname, 'resources', 'sandbox.js'));
}
}
});
// Remove declarations where we eliminated the initializers.
for (const decl of orphanedDeclarations) {
decl.remove();
}
return Array.from(dependencies);
}
function isStrictDirective(directive) {
return (directive.value &&
babelTypes.isDirectiveLiteral(directive.value) &&
directive.value.value === 'use strict');
}
function replaceV8Builtins(code) {
return code.replace(/%(\w+)\(/g, V8_BUILTIN_PREFIX + '$1(');
}
function restoreV8Builtins(code) {
return code.replace(V8_REPLACE_BUILTIN_REGEXP, '%$1(');
}
function maybeUseStict(code, useStrict) {
if (useStrict) {
return `'use strict';${EOL}${EOL}${code}`;
}
return code;
}
class BaseCorpus {
constructor(inputDir) {
assert(fsPath.dirname(inputDir) != inputDir,
`Require an absolute, non-root path to corpus. Got ${inputDir}`)
this.inputDir = inputDir;
}
/**
* If specified, sources from this corpus will use this as universal
* label annotating the code in differential fuzzing.
*/
get diffFuzzLabel() {
return undefined;
}
/**
* Load flags from meta data specific to a particular test case.
*/
loadFlags(relPath, data) {
return [];
}
/**
* By default load dependencies from within this corpus.
*/
loadDependency(relPath) {
return loadDependency(this, relPath);
}
}
const BASE_CORPUS = new BaseCorpus(__dirname);
class Source {
constructor(corpus, relPath, flags, dependentPaths) {
this.corpus = corpus;
this.relPath = relPath;
this.flags = flags;
this.dependentPaths = dependentPaths;
this.sloppy = exceptions.isTestSloppyRel(relPath);
}
get absPath() {
return fsPath.join(this.corpus.inputDir, this.relPath);
}
/**
* Specifies the path used to label sources in differential fuzzing.
*/
get diffFuzzPath() {
return this.corpus.diffFuzzLabel || this.relPath;
}
/**
* Specifies if the source isn't compatible with strict mode.
*/
isSloppy() {
return this.sloppy;
}
/**
* Specifies if the source has a top-level 'use strict' directive.
*/
isStrict() {
throw Error('Not implemented');
}
/**
* Generates the code as a string without any top-level 'use strict'
* directives. V8 natives that were replaced before parsing are restored.
*/
generateNoStrict() {
throw Error('Not implemented');
}
/**
* Recursively adds dependencies of a this source file.
*
* @param {Map} dependencies Dependency map to which to add new, parsed
* dependencies unless they are already in the map.
* @param {Map} visitedDependencies A set for avoiding loops.
*/
loadDependencies(dependencies, visitedDependencies) {
visitedDependencies = visitedDependencies || new Set();
for (const absPath of this.dependentPaths) {
if (dependencies.has(absPath) ||
visitedDependencies.has(absPath)) {
// Already added.
continue;
}
// Prevent infinite loops.
visitedDependencies.add(absPath);
// Recursively load dependencies.
const dependency = loadDependencyAbs(this.corpus, absPath);
dependency.loadDependencies(dependencies, visitedDependencies);
// Add the dependency.
dependencies.set(absPath, dependency);
}
}
}
/**
* Represents sources whose AST can be manipulated.
*/
class ParsedSource extends Source {
constructor(ast, corpus, relPath, flags, dependentPaths) {
super(corpus, relPath, flags, dependentPaths);
this.ast = ast;
this.sloppy |= hasSloppyCode(ast);
}
isStrict() {
return !!this.ast.program.directives.filter(isStrictDirective).length;
}
generateNoStrict() {
const allDirectives = this.ast.program.directives;
this.ast.program.directives = this.ast.program.directives.filter(
directive => !isStrictDirective(directive));
try {
const code = babelGenerator(this.ast.program, {comments: true}).code;
return restoreV8Builtins(code);
} finally {
this.ast.program.directives = allDirectives;
}
}
}
/**
* Represents sources with cached code.
*/
class CachedSource extends Source {
constructor(source) {
super(source.corpus, source.relPath, source.flags, source.dependentPaths);
this.use_strict = source.isStrict();
this.code = source.generateNoStrict();
}
isStrict() {
return this.use_strict;
}
generateNoStrict() {
return this.code;
}
}
/**
* Read file path into an AST.
*
* Post-processes the AST by replacing V8 natives and removing disallowed
* natives, as well as removing load expressions and adding the paths-to-load
* as meta data.
*/
function loadSource(corpus, relPath, parseStrict=false) {
const absPath = fsPath.resolve(fsPath.join(corpus.inputDir, relPath));
const data = fs.readFileSync(absPath, 'utf-8');
if (guessType(data) !== SCRIPT) {
return null;
}
const preprocessed = maybeUseStict(replaceV8Builtins(data), parseStrict);
const ast = babylon.parse(preprocessed, BABYLON_OPTIONS);
removeComments(ast);
cleanAsserts(ast);
annotateWithOriginalPath(ast, relPath);
const flags = corpus.loadFlags(relPath, data);
const dependentPaths = resolveDependencies(absPath, ast);
return new ParsedSource(ast, corpus, relPath, flags, dependentPaths);
}
function guessType(data) {
if (data.includes('// MODULE')) {
return MODULE;
}
return SCRIPT;
}
/**
* Remove existing comments.
*/
function removeComments(ast) {
babelTraverse(ast, {
enter(path) {
babelTypes.removeComments(path.node);
}
});
}
/**
* Replace all throw statements with no-ops to reduce bailouts from
* dependencies.
*/
function neuralizeThrows(ast) {
babelTraverse(ast, {
ThrowStatement(path) {
path.replaceWith(babelTypes.emptyStatement());
path.skip();
}
});
}
/**
* Return true if there's any code incompatible with strict mode.
*/
function hasSloppyCode(ast) {
let sloppy = false;
babelTraverse(ast, {
WithStatement(path) {
sloppy = true;
},
UnaryExpression(path) {
if (path.node.operator === 'delete' &&
babelTypes.isIdentifier(path.node.argument)) {
sloppy = true;
}
}
});
return sloppy;
}
/**
* Removes "Assert" from strings in spidermonkey shells or from older
* crash tests: https://crbug.com/1068268
*/
function cleanAsserts(ast) {
function replace(string) {
return string == null ? null : string.replace(/[Aa]ssert/g, '*****t');
}
babelTraverse(ast, {
StringLiteral(path) {
path.node.value = replace(path.node.value);
path.node.extra.raw = replace(path.node.extra.raw);
path.node.extra.rawValue = replace(path.node.extra.rawValue);
},
TemplateElement(path) {
path.node.value.cooked = replace(path.node.value.cooked);
path.node.value.raw = replace(path.node.value.raw);
},
});
}
/**
* Annotate code with top-level comment.
*/
function annotateWithComment(ast, comment) {
if (ast.program && ast.program.body && ast.program.body.length > 0) {
babelTypes.addComment(
ast.program.body[0], 'leading', comment, true);
}
}
/**
* Annotate code with original file path.
*/
function annotateWithOriginalPath(ast, relPath) {
annotateWithComment(ast, ' Original: ' + relPath);
}
const dependencyCache = new Map();
function loadDependency(corpus, relPath) {
const absPath = fsPath.join(corpus.inputDir, relPath);
let dependency = dependencyCache.get(absPath);
if (!dependency) {
const source = loadSource(corpus, relPath);
// Reduce bailouts from dependencies by removing throws.
neuralizeThrows(source.ast);
dependency = new CachedSource(source);
dependencyCache.set(absPath, dependency);
}
return dependency;
}
function loadDependencyAbs(corpus, absPath) {
return corpus.loadDependency(fsPath.relative(corpus.inputDir, absPath));
}
// Convenience helper to load a file from the resources directory.
function loadResource(fileName) {
return loadDependency(BASE_CORPUS, fsPath.join('resources', fileName));
}
function generateCode(source, dependencies=[]) {
const allSources = dependencies.concat([source]);
const codePieces = allSources.map(
source => source.generateNoStrict());
if (allSources.some(source => source.isStrict()) &&
!allSources.some(source => source.isSloppy())) {
codePieces.unshift('\'use strict\';');
}
return codePieces.join(EOL + EOL);
}
module.exports = {
BABYLON_OPTIONS: BABYLON_OPTIONS,
BABYLON_REPLACE_VAR_OPTIONS: BABYLON_REPLACE_VAR_OPTIONS,
BASE_CORPUS: BASE_CORPUS,
annotateWithComment: annotateWithComment,
BaseCorpus: BaseCorpus,
generateCode: generateCode,
loadDependencyAbs: loadDependencyAbs,
loadResource: loadResource,
loadSource: loadSource,
ParsedSource: ParsedSource,
}

View File

@ -0,0 +1,122 @@
// Copyright 2020 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.
/**
* @fileoverview Test helpers.
*/
'use strict';
const assert = require('assert');
const dircompare = require('dir-compare');
const path = require('path');
const fs = require('fs');
const corpus = require('../corpus.js');
const sourceHelpers = require('../source_helpers.js');
const BASE_DIR = path.join(path.dirname(__dirname), 'test_data');
const DB_DIR = path.join(BASE_DIR, 'fake_db');
const TEST_CORPUS = new sourceHelpers.BaseCorpus(BASE_DIR);
const V8_TEST_CORPUS = corpus.create(BASE_DIR, 'v8');
const FUZZILLI_TEST_CORPUS = corpus.create(
BASE_DIR, 'fuzzilli', false, V8_TEST_CORPUS);
const HEADER = `// Copyright 2025 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.
`;
/**
* Create a function that returns one of `probs` when called. It rotates
* through the values. Useful to replace `random.random()` in tests using
* the probabilities that trigger different interesting cases.
*/
function cycleProbabilitiesFun(probs) {
let index = 0;
return () => {
index = index % probs.length;
return probs[index++];
};
}
/**
* Replace Math.random with a deterministic pseudo-random function.
*/
function deterministicRandom(sandbox) {
let seed = 1;
function random() {
const x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
sandbox.stub(Math, 'random').callsFake(() => { return random(); });
}
function loadTestData(relPath) {
return sourceHelpers.loadSource(TEST_CORPUS, relPath);
}
function loadFuzzilliTestData(relPath) {
return sourceHelpers.loadSource(FUZZILLI_TEST_CORPUS, relPath);
}
function loadV8TestData(relPath) {
return sourceHelpers.loadSource(V8_TEST_CORPUS, relPath);
}
function assertExpectedPath(expectedPath, actualPath) {
const absPath = path.join(BASE_DIR, expectedPath);
if (process.env.GENERATE) {
if (fs.existsSync(absPath)) {
fs.rmSync(absPath, { recursive: true, force: true });
}
fs.cpSync(actualPath, absPath, {recursive: true});
}
const options = { compareSize: true };
const res = dircompare.compareSync(absPath, actualPath, options);
const message = (
`Equal: ${res.equal}, distinct: ${res.distinct}, ` +
`expected only: ${res.left}, actual only: ${res.right}, ` +
`differences: ${res.differences}`
);
assert.ok(res.same, message);
}
function assertExpectedResult(expectedPath, result) {
const absPath = path.join(BASE_DIR, expectedPath);
if (process.env.GENERATE) {
let header = HEADER;
if (fs.existsSync(absPath)) {
// Keep the old copyright header if the file already exists.
const previous = fs.readFileSync(absPath, 'utf-8').trim().split('\n');
previous.splice(3);
header = previous.join('\n') + '\n\n';
}
fs.writeFileSync(absPath, header + result.trim() + '\n');
return;
}
// Omit copyright header when comparing files.
const expected = fs.readFileSync(absPath, 'utf-8').trim().split('\n');
expected.splice(0, 4);
assert.strictEqual(expected.join('\n'), result.trim());
}
module.exports = {
BASE_DIR: BASE_DIR,
DB_DIR: DB_DIR,
FUZZILLI_TEST_CORPUS: FUZZILLI_TEST_CORPUS,
TEST_CORPUS: TEST_CORPUS,
assertExpectedPath: assertExpectedPath,
assertExpectedResult: assertExpectedResult,
cycleProbabilitiesFun: cycleProbabilitiesFun,
deterministicRandom: deterministicRandom,
loadFuzzilliTestData: loadFuzzilliTestData,
loadTestData: loadTestData,
loadV8TestData: loadV8TestData,
}

View File

@ -0,0 +1,67 @@
// Copyright 2020 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.
/**
* @fileoverview Tests for mutating variables
*/
'use strict';
const babelTraverse = require('@babel/traverse').default;
const sinon = require('sinon');
const common = require('../mutators/common.js');
const helpers = require('./helpers.js');
const sandbox = sinon.createSandbox();
describe('Available variables and functions', () => {
afterEach(() => {
sandbox.restore();
});
function testAvailableIdentifiers(input, expected) {
const source = helpers.loadTestData(input);
const result = new Array();
babelTraverse(source.ast, {
CallExpression(path) {
result.push({
variables: common.availableVariables(path),
functions: common.availableFunctions(path),
});
}
});
helpers.assertExpectedResult(
expected, JSON.stringify(result, null, 2));
}
it('test with functions and strict execution order', () => {
sandbox.stub(common, 'STRICT_EXECUTION_ORDER_PROB').value(1.0);
testAvailableIdentifiers(
'available_variables.js', 'available_variables_expected.js');
});
it('test with functions and sloppy execution order', () => {
sandbox.stub(common, 'STRICT_EXECUTION_ORDER_PROB').value(0.0);
testAvailableIdentifiers(
'available_variables.js',
'available_variables_sloppy_expected.js');
});
it('test with function expressions', () => {
sandbox.stub(common, 'STRICT_EXECUTION_ORDER_PROB').value(0.0);
testAvailableIdentifiers(
'available_variables_fun_exp.js',
'available_variables_fun_exp_expected.js');
});
it('test with arrow function expressions', () => {
sandbox.stub(common, 'STRICT_EXECUTION_ORDER_PROB').value(0.0);
testAvailableIdentifiers(
'available_variables_arrow_fun_exp.js',
'available_variables_arrow_fun_exp_expected.js');
});
});

View File

@ -0,0 +1,64 @@
// Copyright 2025 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.
/**
* @fileoverview Tests for analyzing the mutation context.
*/
'use strict';
const babelTraverse = require('@babel/traverse').default;
const babelTypes = require('@babel/types');
const common = require('../mutators/common.js');
const helpers = require('./helpers.js');
const sourceHelpers = require('../source_helpers.js');
const { analyzeContext } = require('../script_mutator.js');
const { IdentifierNormalizer } = require('../mutators/normalizer.js');
describe('Mutation context', () => {
it('determines loop variables', () => {
const source = helpers.loadTestData('loop_variables.js');
const normalizer = new IdentifierNormalizer();
normalizer.mutate(source);
const context = analyzeContext(source);
const code = sourceHelpers.generateCode(source, []);
helpers.assertExpectedResult(
'loop_variables_source_expected.js', code);
helpers.assertExpectedResult(
'loop_variables_expected.js',
JSON.stringify(Array.from(context.loopVariables), null, 2));
});
it('determines large for-loops', () => {
const source = helpers.loadTestData('large_loops.js');
const normalizer = new IdentifierNormalizer();
normalizer.mutate(source);
analyzeContext(source);
const large = [];
const small = [];
babelTraverse(source.ast, {
CallExpression(path) {
// Only list our console.log calls.
if (!babelTypes.isMemberExpression(path.node.callee)) {
return;
}
const text = path.node.arguments[0].value;
const forNode = path.findParent((p) => p.isForStatement()).node;
if(common.isLargeLoop(forNode)) {
large.push(text);
} else {
small.push(text);
}
}
});
helpers.assertExpectedResult(
'large_loops_expected.js',
JSON.stringify({large: large, small: small}, null, 2));
});
});

View File

@ -0,0 +1,169 @@
// Copyright 2020 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.
/**
* @fileoverview Corpus loading.
*/
'use strict';
const assert = require('assert');
const sinon = require('sinon');
const corpus = require('../corpus.js');
const exceptions = require('../exceptions.js');
const helpers = require('./helpers.js');
const sourceHelpers = require('../source_helpers.js');
const sandbox = sinon.createSandbox();
function testSoftSkipped(count, softSkipped, paths) {
sandbox.stub(exceptions, 'getSoftSkipped').callsFake(() => {
return softSkipped;
});
const mjsunit = corpus.create('test_data', 'mjsunit_softskipped');
const cases = mjsunit.getRandomTestcasePaths(count);
assert.deepEqual(paths, cases);
}
describe('Loading corpus', () => {
afterEach(() => {
sandbox.restore();
});
it('keeps all tests with no soft-skipped tests', () => {
sandbox.stub(Math, 'random').callsFake(() => 0.9);
testSoftSkipped(
3,
[],
['mjsunit_softskipped/permitted.js',
'mjsunit_softskipped/object-literal.js',
'mjsunit_softskipped/regress/binaryen-123.js']);
});
it('choose one test with no soft-skipped tests', () => {
sandbox.stub(Math, 'random').callsFake(() => 0.9);
testSoftSkipped(
1,
[],
['mjsunit_softskipped/permitted.js']);
});
it('keeps soft-skipped tests', () => {
sandbox.stub(Math, 'random').callsFake(() => 0.9);
testSoftSkipped(
1,
[/^binaryen.*\.js/, 'object-literal.js'],
['mjsunit_softskipped/permitted.js']);
});
it('keeps no generated soft-skipped tests', () => {
sandbox.stub(Math, 'random').callsFake(() => 0.9);
const softSkipped = [
// Correctly listed full relative path of test case.
'mjsunit_softskipped/regress/binaryen-123.js',
// Only basename doesn't match.
'object-literal.js',
// Only pieces of the path don't match.
'mjsunit_softskipped',
];
sandbox.stub(exceptions, 'getGeneratedSoftSkipped').callsFake(
() => { return new Set(softSkipped); });
testSoftSkipped(
2,
// None soft-skipped for basenames and regexps.
[],
// Only binaryen-123.js gets filtered out.
['mjsunit_softskipped/object-literal.js',
'mjsunit_softskipped/permitted.js']);
});
it('keeps soft-skipped tests by chance', () => {
sandbox.stub(Math, 'random').callsFake(() => 0);
testSoftSkipped(
3,
[/^binaryen.*\.js/, 'object-literal.js'],
['mjsunit_softskipped/object-literal.js',
'mjsunit_softskipped/regress/binaryen-123.js',
'mjsunit_softskipped/permitted.js']);
});
it('keeps soft-skipped paths', () => {
sandbox.stub(Math, 'random').callsFake(() => 0.9);
sandbox.stub(exceptions, 'getSoftSkippedPaths').callsFake(() => {
return [/not_/];
});
const crashtests = corpus.create('test_data', 'crashtests_softskipped');
const cases = crashtests.getRandomTestcasePaths(2);
assert.deepEqual(
['crashtests_softskipped/test.js',
'crashtests_softskipped/not_great.js'],
cases);
});
it('caches relative paths', () => {
sandbox.stub(Math, 'random').callsFake(() => 0);
sandbox.stub(exceptions, 'getSoftSkipped').callsFake(
() => { return ['object-literal.js']; });
const generatedSoftSkipped = [
'mjsunit_softskipped/regress/binaryen-123.js',
];
sandbox.stub(exceptions, 'getGeneratedSoftSkipped').callsFake(
() => { return new Set(generatedSoftSkipped); });
const mjsunit = corpus.create('test_data' , 'mjsunit_softskipped');
assert.deepEqual(
['mjsunit_softskipped/object-literal.js',
'mjsunit_softskipped/regress/binaryen-123.js'],
mjsunit.softSkippedFiles);
assert.deepEqual(
['mjsunit_softskipped/permitted.js'],
mjsunit.permittedFiles);
assert.deepEqual(
['mjsunit_softskipped/permitted.js',
'mjsunit_softskipped/object-literal.js',
'mjsunit_softskipped/regress/binaryen-123.js'],
Array.from(mjsunit.relFiles()));
});
it('drops discouraged files', () => {
// Test that files with dropped flags are also dropped. This
// test sets this probability to 1.
sandbox.stub(Math, 'random').callsFake(() => 0.9);
sandbox.stub(corpus, 'DROP_DISCOURAGED_FILES_PROB').value(1);
const v8 = corpus.create('test_data/flags/corpus', 'v8');
const cases = v8.getRandomTestcases(3).map(x => x.relPath);
assert.deepEqual(['v8/input3.js'], cases);
})
it('keeps discouraged files', () => {
// Test that files with dropped flags are sometimes kept. This
// test sets this probability to 0.
sandbox.stub(Math, 'random').callsFake(() => 0.9);
sandbox.stub(corpus, 'DROP_DISCOURAGED_FILES_PROB').value(0);
const v8 = corpus.create('test_data/flags/corpus', 'v8');
const cases = v8.getRandomTestcases(3).map(x => x.relPath);
assert.deepEqual(['v8/input2.js', 'v8/input1.js', 'v8/input3.js'], cases);
})
});
describe('Fuzzilli corpus', () => {
afterEach(() => {
sandbox.restore();
});
it('loads sources with all flags', () => {
const source = sourceHelpers.loadSource(
helpers.FUZZILLI_TEST_CORPUS,
'fuzzilli/fuzzdir-1/corpus/program_1.js');
const expectedFlags = [
'--expose-gc',
'--fuzzing',
'--fuzzilli-flag1',
'--random-seed=123',
'--fuzzilli-flag2'
];
assert.deepEqual(expectedFlags, source.flags);
});
});

View File

@ -0,0 +1,42 @@
// Copyright 2021 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.
/**
* @fileoverview Test the script building the DB.
*/
'use strict';
const assert = require('assert');
const { execSync } = require("child_process");
const fs = require('fs');
const path = require('path');
const tempy = require('tempy');
const helpers = require('./helpers.js');
function buildDb(inputDir, corpusName, outputDir) {
execSync(
`node build_db.js -i ${inputDir} -o ${outputDir} ${corpusName}`,
{stdio: ['pipe']});
}
describe('DB tests', () => {
// Test feeds an expression that does not apply.
it('omits erroneous expressions', () => {
const outPath = tempy.directory();
buildDb('test_data/db', 'this', outPath);
const indexFile = path.join(outPath, 'index.json');
const indexJSON = JSON.parse(fs.readFileSync(indexFile), 'utf-8');
assert.deepEqual(
indexJSON, {"statements": [], "superStatements": [], "all": []});
});
// End to end test with various extracted and not extracted examples.
it('build db e2e', () => {
const outPath = tempy.directory();
buildDb('test_data/db', 'e2e', outPath);
helpers.assertExpectedPath('db/e2e_expected', outPath);
});
});

View File

@ -0,0 +1,256 @@
// Copyright 2020 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.
/**
* @fileoverview Tests for differential fuzzing.
*/
'use strict';
const assert = require('assert');
const path = require('path');
const program = require('commander');
const sinon = require('sinon');
const helpers = require('./helpers.js');
const scriptMutator = require('../script_mutator.js');
const sourceHelpers = require('../source_helpers.js');
const random = require('../random.js');
const runner = require('../runner.js');
const { DifferentialFuzzMutator, DifferentialFuzzSuppressions } = require(
'../mutators/differential_fuzz_mutator.js');
const { DifferentialScriptMutator, FuzzilliDifferentialScriptMutator } = require(
'../differential_script_mutator.js');
const sandbox = sinon.createSandbox();
function testMutators(settings, mutatorClass, inputFile, expectedFile) {
const source = helpers.loadTestData('differential_fuzz/' + inputFile);
const mutator = new mutatorClass(settings);
mutator.mutate(source);
const mutated = sourceHelpers.generateCode(source);
helpers.assertExpectedResult(
'differential_fuzz/' + expectedFile, mutated);
}
describe('Differential fuzzing', () => {
beforeEach(() => {
// Zero settings for all mutators.
this.settings = scriptMutator.defaultSettings();
for (const key of Object.keys(this.settings)) {
this.settings[key] = 0.0;
}
// By default, deterministically use all mutations of differential
// fuzzing.
this.settings['DIFF_FUZZ_EXTRA_PRINT'] = 1.0;
this.settings['DIFF_FUZZ_TRACK_CAUGHT'] = 1.0;
// Fake fuzzer being called with --input_dir flag.
this.oldInputDir = program.input_dir;
program.input_dir = helpers.BASE_DIR;
// Make remaining places deterministic that don't use global settings, e.g.
// try-catch skip behavior.
helpers.deterministicRandom(sandbox);
});
afterEach(() => {
sandbox.restore();
program.input_dir = this.oldInputDir;
});
it('applies suppressions', () => {
// This selects the first random variable when replacing .arguments.
sandbox.stub(random, 'single').callsFake(a => a[0]);
testMutators(
this.settings,
DifferentialFuzzSuppressions,
'suppressions.js',
'suppressions_expected.js');
});
it('adds extra printing', () => {
testMutators(
this.settings,
DifferentialFuzzMutator,
'mutations.js',
'mutations_expected.js');
});
it('does no extra printing', () => {
this.settings['DIFF_FUZZ_EXTRA_PRINT'] = 0.0;
testMutators(
this.settings,
DifferentialFuzzMutator,
'exceptions.js',
'exceptions_expected.js');
});
it('runs end to end', () => {
// Don't choose any zeroed settings or IGNORE_DEFAULT_PROB in try-catch
// mutator. Choose using original flags with >= 2%.
const chooseOrigFlagsProb = 0.2;
sandbox.stub(random, 'choose').callsFake((p) => p >= chooseOrigFlagsProb);
// Fake build directory from which two json configurations for flags are
// loaded.
const env = {
APP_DIR: 'test_data/differential_fuzz',
GENERATE: process.env.GENERATE,
};
sandbox.stub(process, 'env').value(env);
// Fake loading resources and instead load one fixed fake file for each.
sandbox.stub(sourceHelpers, 'loadResource').callsFake(() => {
return helpers.loadTestData('differential_fuzz/fake_resource.js');
});
// Load input files.
const files = [
'v8/differential_fuzz/input1.js',
'v8/differential_fuzz/input2.js',
];
const sources = files.map(helpers.loadV8TestData);
// Apply top-level fuzzing, with all probabilistic configs switched off.
this.settings['DIFF_FUZZ_EXTRA_PRINT'] = 0.0;
this.settings['DIFF_FUZZ_TRACK_CAUGHT'] = 0.0;
const mutator = new DifferentialScriptMutator(
this.settings, helpers.DB_DIR);
const mutated = mutator.mutateMultiple(sources);
helpers.assertExpectedResult(
'differential_fuzz/combined_expected.js', mutated.code);
// Flags for v8_foozzie.py are calculated from v8_fuzz_experiments.json and
// v8_fuzz_flags.json in test_data/differential_fuzz.
const expectedFlags = [
'--first-config=ignition',
'--second-config=ignition_turbo',
'--second-d8=d8',
'--second-config-extra-flags=--foo1',
'--second-config-extra-flags=--foo2',
'--first-config-extra-flags=--flag1',
'--second-config-extra-flags=--flag1',
'--first-config-extra-flags=--flag2',
'--second-config-extra-flags=--flag2',
'--first-config-extra-flags=--flag3',
'--second-config-extra-flags=--flag3',
'--first-config-extra-flags=--flag4',
'--second-config-extra-flags=--flag4'
];
assert.deepEqual(expectedFlags, mutated.flags);
});
it('runs end to end with fuzzilli inputs', () => {
// Similar to the test above but using a Fuzzilli input as well.
const chooseOrigFlagsProb = 0.2;
sandbox.stub(random, 'choose').callsFake((p) => p >= chooseOrigFlagsProb);
const env = {
APP_DIR: 'test_data/differential_fuzz',
GENERATE: process.env.GENERATE,
};
sandbox.stub(process, 'env').value(env);
sandbox.stub(sourceHelpers, 'loadResource').callsFake(() => {
return helpers.loadTestData('differential_fuzz/fake_resource.js');
});
const sources = [
helpers.loadV8TestData('v8/differential_fuzz/input2.js'),
helpers.loadFuzzilliTestData('fuzzilli/fuzzdir-1/corpus/program_1.js'),
];
this.settings['DIFF_FUZZ_EXTRA_PRINT'] = 0.0;
this.settings['DIFF_FUZZ_TRACK_CAUGHT'] = 0.0;
const mutator = new DifferentialScriptMutator(
this.settings, helpers.DB_DIR);
const mutated = mutator.mutateMultiple(sources);
helpers.assertExpectedResult(
'differential_fuzz/fuzzilli_combined_expected.js', mutated.code);
const expectedFlags = [
// Flags from v8_fuzz_experiments.json.
'--first-config=ignition',
'--second-config=ignition_turbo',
'--second-d8=d8',
// Flags from v8_fuzz_flags.json.
'--second-config-extra-flags=--foo1',
'--second-config-extra-flags=--foo2',
// Flags from the mjsunit input.
'--first-config-extra-flags=--flag4',
'--second-config-extra-flags=--flag4',
// Flags from the fuzzilli input.
'--first-config-extra-flags=--fuzzilli-flag1',
'--second-config-extra-flags=--fuzzilli-flag1',
'--first-config-extra-flags=--fuzzilli-flag2',
'--second-config-extra-flags=--fuzzilli-flag2',
];
assert.deepEqual(expectedFlags, mutated.flags);
});
it('chooses fuzzilli runner', () => {
sandbox.stub(random, 'randInt').callsFake(() => 2);
const env = {
APP_DIR: 'test_data/differential_fuzz',
GENERATE: process.env.GENERATE,
};
sandbox.stub(process, 'env').value(env);
const mutator = new DifferentialScriptMutator(
this.settings, helpers.DB_DIR);
assert.deepEqual(
runner.RandomCorpusRunnerWithFuzzilli, mutator.runnerClass);
});
});
// As above, but we don't zero the settings as we use zero settings in
// production here.
describe('Differential fuzzing with fuzzilli', () => {
beforeEach(() => {
// Fake fuzzer being called with --input_dir flag.
this.oldInputDir = program.input_dir;
program.input_dir = path.join(
helpers.BASE_DIR, 'differential_fuzz_fuzzilli');
helpers.deterministicRandom(sandbox);
});
afterEach(() => {
sandbox.restore();
program.input_dir = this.oldInputDir;
});
it('runs end to end', () => {
const env = {
APP_DIR: 'test_data/differential_fuzz',
GENERATE: process.env.GENERATE,
};
sandbox.stub(process, 'env').value(env);
sandbox.stub(sourceHelpers, 'loadResource').callsFake(() => {
return helpers.loadTestData('differential_fuzz/fake_resource.js');
});
const mutator = new FuzzilliDifferentialScriptMutator(
scriptMutator.defaultSettings(), helpers.DB_DIR);
// Configure 3 output tests and V8 engine.
const testRunner = new mutator.runnerClass(
program.input_dir, 'v8', 3);
for (const [i, inputs] of testRunner.enumerateInputs()) {
const mutated = mutator.mutateMultiple(inputs);
helpers.assertExpectedResult(
`differential_fuzz_fuzzilli/expected_code_${i}.js`, mutated.code);
helpers.assertExpectedResult(
`differential_fuzz_fuzzilli/expected_flags_${i}.js`,
JSON.stringify(mutated.flags, null, 2));
}
});
});

View File

@ -0,0 +1,113 @@
// Copyright 2020 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.
/**
* @fileoverview Tests for the differential-fuzzing library files.
*/
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const libPath = path.resolve(
path.join(__dirname, '..', 'resources', 'differential_fuzz_library.js'));
const code = fs.readFileSync(libPath, 'utf-8');
// We wire the print function to write to this result variable.
const resultDummy = 'let result; const print = text => { result = text; };';
// The prettyPrinted function from mjsunit is reused in the library.
const prettyPrint = 'let prettyPrinted = value => value;';
const hookedUpCode = resultDummy + prettyPrint + code;
// Runs the library, adds test code and verifies the result.
function testLibrary(testCode, expected) {
// The code isn't structured as a module. The test code is expected to
// evaluate to a result which we store in actual.
const actual = eval(hookedUpCode + testCode);
assert.deepEqual(expected, actual);
}
describe('Differential fuzzing library', () => {
it('prints objects', () => {
testLibrary(
'__prettyPrint([0, 1, 2, 3]); result;',
'[0, 1, 2, 3]');
testLibrary(
'__prettyPrint({0: 1, 2: 3}); result;',
'Object{0: 1, 2: 3}');
testLibrary(
'const o = {}; o.k = 42;__prettyPrint(o); result;',
'Object{k: 42}');
});
it('cuts off deep nesting', () => {
// We print only until a nesting depth of 4.
testLibrary(
'__prettyPrint({0: [1, 2, [3, {4: []}]]}); result;',
'Object{0: [1, 2, [3, Object{4: ...}]]}');
});
it('cuts off long strings', () => {
const long = new Array(66).join('a');
const head = new Array(55).join('a');
const tail = new Array(10).join('a');
testLibrary(
`__prettyPrint("${long}"); result;`,
`${head}[...]${tail}`);
// If the string gets longer, the cut-off version is still the same.
const veryLong = new Array(100).join('a');
testLibrary(
`__prettyPrint("${veryLong}"); result;`,
`${head}[...]${tail}`);
});
it('tracks hash difference', () => {
// Test that we track a hash value for each string we print.
const long = new Array(66).join('a');
testLibrary(
`__prettyPrint("${long}"); __hash;`,
2097980794);
// Test that the hash value differs, also when the cut-off result doesn't.
const veryLong = new Array(100).join('a');
testLibrary(
`__prettyPrint("${veryLong}"); __hash;`,
-428472866);
// Test that repeated calls update the hash.
testLibrary(
`__prettyPrint("${long}");__prettyPrint("${long}"); __hash;`,
-909224493);
});
it('limits extra printing', () => {
// Test that after exceeding the limit for calling extra printing, there
// is no new string printed (in the test case no new result added).
testLibrary(
'for (let i = 0; i < 20; i++) __prettyPrintExtra(i); result;',
'19');
testLibrary(
'for (let i = 0; i < 101; i++) __prettyPrintExtra(i); result;',
'99');
testLibrary(
'for (let i = 0; i < 102; i++) __prettyPrintExtra(i); result;',
'99');
});
it('tracks hash after limit', () => {
// Test that after exceeding the limit for calling extra printing, the
// hash is still updated.
testLibrary(
'for (let i = 0; i < 20; i++) __prettyPrintExtra(i); __hash;',
-945753644);
testLibrary(
'for (let i = 0; i < 101; i++) __prettyPrintExtra(i); __hash;',
1907055979);
testLibrary(
'for (let i = 0; i < 102; i++) __prettyPrintExtra(i); __hash;',
-590842070);
});
});

View File

@ -0,0 +1,43 @@
// Copyright 2025 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.
/**
* @fileoverview Test flag loading and merging.
*/
'use strict';
const assert = require('assert');
const sinon = require('sinon');
const helpers = require('./helpers.js');
const scriptMutator = require('../script_mutator.js');
const sandbox = sinon.createSandbox();
describe('Flag loading', () => {
beforeEach(() => {
helpers.deterministicRandom(sandbox);
// Zero settings for all mutators.
this.settings = scriptMutator.defaultSettings();
for (const key of Object.keys(this.settings)) {
this.settings[key] = 0.0;
}
});
afterEach(() => {
sandbox.restore();
});
it('drops blocked flags', () => {
const inputs = [
'flags/blocked_flags/input1.js',
'flags/blocked_flags/input2.js'];
const sources = inputs.map(input => helpers.loadV8TestData(input));
const mutator = new scriptMutator.ScriptMutator(this.settings, helpers.DB_DIR);
const mutated = mutator.mutateMultiple(sources);
assert.deepEqual(['--future', '--js-staging'], mutated.flags);
});
});

View File

@ -0,0 +1,101 @@
// Copyright 2020 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.
/**
* @fileoverview Test normalization.
*/
'use strict';
const path = require('path');
const sinon = require('sinon');
const corpus = require('../corpus.js');
const helpers = require('./helpers.js');
const sourceHelpers = require('../source_helpers.js');
const { ScriptMutator } = require('../script_mutator.js');
const sandbox = sinon.createSandbox();
function testLoad(testPath, expectedPath, loadFun=helpers.loadTestData) {
const mutator = new ScriptMutator({}, helpers.DB_DIR);
const source = loadFun(testPath);
const dependencies = mutator.resolveInputDependencies([source]);
const code = sourceHelpers.generateCode(source, dependencies);
helpers.assertExpectedResult(expectedPath, code);
}
describe('V8 dependencies', () => {
it('test', () => {
testLoad(
'mjsunit/test_load.js',
'mjsunit/test_load_expected.js');
});
it('does not loop indefinitely', () => {
testLoad(
'mjsunit/test_load_self.js',
'mjsunit/test_load_self_expected.js');
});
it('loads mjsunit dependencies also from another corpus', () => {
const corpusDir = path.join(helpers.BASE_DIR, 'load' ,'fuzzilli_scenario');
const v8Corpus = corpus.create(corpusDir, 'v8');
const fuzzilliCorpus = corpus.create(
corpusDir, 'fuzzilli', false, v8Corpus);
const load = (relPath) => {
return sourceHelpers.loadSource(fuzzilliCorpus, relPath);
};
testLoad(
'fuzzilli/fuzzdir-1/corpus/program_x.js',
'load/fuzzilli_scenario/test_load_expected.js',
load);
});
});
describe('Chakra dependencies', () => {
it('test', () => {
testLoad(
'chakra/load.js',
'chakra/load_expected.js');
});
});
describe('JSTest dependencies', () => {
afterEach(() => {
sandbox.restore();
});
it('test', () => {
const fakeStubs = sourceHelpers.loadSource(
helpers.TEST_CORPUS, 'JSTests/fake_stub.js');
sandbox.stub(sourceHelpers, 'loadResource').callsFake(() => fakeStubs);
testLoad('JSTests/load.js', 'JSTests/load_expected.js');
});
});
describe('SpiderMonkey dependencies', () => {
it('test', () => {
testLoad(
'spidermonkey/test/load.js',
'spidermonkey/test/load_expected.js');
});
});
describe('Sandbox dependencies', () => {
it('test', () => {
testLoad(
'sandbox/load.js',
'sandbox/load_expected.js');
});
});
describe('Language features', () => {
it('test', () => {
testLoad(
'language/features.js',
'language/features_expected.js');
});
});

View File

@ -0,0 +1,50 @@
// Copyright 2020 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.
/**
* @fileoverview Tests for mutating arrays
*/
'use strict';
const sinon = require('sinon');
const babylon = require('@babel/parser');
const common = require('../mutators/common.js');
const helpers = require('./helpers.js');
const scriptMutator = require('../script_mutator.js');
const sourceHelpers = require('../source_helpers.js');
const arrayMutator = require('../mutators/array_mutator.js');
const sandbox = sinon.createSandbox();
describe('Mutate arrays', () => {
afterEach(() => {
sandbox.restore();
});
it('performs all mutations', () => {
// Make random operations deterministic.
sandbox.stub(common, 'randomValue').callsFake(
() => babylon.parseExpression('""'));
helpers.deterministicRandom(sandbox);
// Ensure large arrays are not mutated. For testing make arrays > 3 large.
sandbox.stub(arrayMutator, 'MAX_ARRAY_LENGTH').value(3);
const source = helpers.loadTestData('mutate_arrays.js');
const settings = scriptMutator.defaultSettings();
settings['MUTATE_ARRAYS'] = 1.0;
const mutator = new arrayMutator.ArrayMutator(settings);
mutator.mutate(source);
const mutated = sourceHelpers.generateCode(source);
helpers.assertExpectedResult(
'mutate_arrays_expected.js', mutated);
});
});

Some files were not shown because too many files have changed in this diff Show More