#!/usr/bin/env python3 # # 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. """List all available fuzz tests and create wrapper scripts for each. Invoked by GN depending on the unit-tests target. Expected to be called from the root out dir of GN. Writes the wrappers and 'fuzztests.stamp' into the ./fuzztests/ directory. """ import os import re import shutil import stat import subprocess import sys from pathlib import Path, PurePath # Set up path to be able to import action_helpers BASE_DIR = Path(__file__).absolute().parent.parent.parent sys.path.append(str(BASE_DIR / 'build')) import action_helpers CENTIPEDE = 'centipede' EXECUTABLE = 'v8_unittests' FUZZ_TEST_DIR = 'fuzztests' FUZZ_TEST_STAMP = 'fuzztests.stamp' # When this script is enabled, we expect to find at least the demo tests. MIN_FUZZTESTS = 2 # This is an arbitrary safeguard. If we run into it and it's legit, # just double it. MAX_FUZZTESTS = 100 WRAPPER_HEADER = """ #!/bin/sh BINARY_DIR="$(cd "${{0%/*}}"/..; pwd)" cd $BINARY_DIR """.strip() CENTIPEDE_WRAPPER = WRAPPER_HEADER + """ exec $BINARY_DIR/centipede $@ """ FUZZTEST_WRAPPER = WRAPPER_HEADER + """ # Normal fuzzing. if [ "$#" -eq "0" ]; then exec $BINARY_DIR/v8_unittests --fuzz={test} --corpus_database="" fi # Fuzztest replay. if [ "$#" -eq "1" ]; then unset CENTIPEDE_RUNNER_FLAGS FUZZTEST_REPLAY=$1 exec $BINARY_DIR/v8_unittests --fuzz={test} --corpus_database="" fi """ FUZZER_NAME_RE = re.compile(r'^\w+\.\w+$') def list_fuzz_tests(executable): env = os.environ env['ASAN_OPTIONS'] = 'detect_odr_violation=0' env['CENTIPEDE_RUNNER_FLAGS'] = 'stack_limit_kb=0:' test_list = subprocess.check_output( [executable, '--list_fuzz_tests=true'], env=env, cwd='.', ).decode('utf-8') return sorted(set(re.findall('Fuzz test: (.*)', test_list))) def fuzz_test_to_file_name(test): assert FUZZER_NAME_RE.match(test) fuzztest_name = re.sub(r'((f|F)uzz(t|T)est|(t|T)est)', '', test) fuzztest_name = re.sub(r'\.', ' ', fuzztest_name) fuzztest_name = re.sub('([A-Z]+)', r' \1', fuzztest_name) fuzztest_name = re.sub('([A-Z][a-z]+)', r' \1', fuzztest_name) splitted = fuzztest_name.split() splitted = map(str.lower, splitted) splitted = filter(bool, splitted) return 'v8_' + '_'.join(splitted) + '_fuzztest' def create_wrapper(file_name, template, test=''): with action_helpers.atomic_output(file_name) as f: wrapper = template.format(test=test) f.write(wrapper.encode('utf-8')) # Make the wrapper world-executable. st = os.stat(file_name) m = st.st_mode os.chmod(file_name, m | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) def setup_fuzztests_dir(cwd): # This script owns the fuzztests subdirectory. Purge everything # to enable consistent incremental builds. fuzz_test_dir = cwd / FUZZ_TEST_DIR shutil.rmtree(fuzz_test_dir, ignore_errors=True) fuzz_test_dir.mkdir(exist_ok=True) # Centipede is later expected to be side-by-side with the fuzz-test # targets. Create a bash redirect so that shared libraries in cwd # keep loading. create_wrapper(fuzz_test_dir / CENTIPEDE, CENTIPEDE_WRAPPER) return fuzz_test_dir def create_fuzztest_wrapper(fuzz_test_dir, test_name): fuzztest_path = fuzz_test_dir / fuzz_test_to_file_name(test_name) create_wrapper(fuzztest_path, FUZZTEST_WRAPPER, test_name) def main(): # The CWD is expected to be the root out dir of GN. cwd = Path(os.getcwd()) # We expect the unit-test executable present in the root dir. executable = cwd / EXECUTABLE assert executable.exists() fuzz_test_dir = setup_fuzztests_dir(cwd) fuzz_tests = list_fuzz_tests(executable) assert MIN_FUZZTESTS <= len(fuzz_tests) <= MAX_FUZZTESTS for test_name in fuzz_tests: create_fuzztest_wrapper(fuzz_test_dir, test_name) # This is a place holder telling GN that we're done. with action_helpers.atomic_output(fuzz_test_dir / FUZZ_TEST_STAMP) as f: f.write('\n'.join(fuzz_tests).encode('utf-8')) if __name__ == '__main__': main()