
The methodology used to generate this CL is documented in https://crbug.com/1098010#c95. No-Try: true No-Presubmit: true Bug: 1098010 Change-Id: I3a8a7b150e7bd64690534727150646081df50439 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3900697 Reviewed-by: Mark Mentovai <mark@chromium.org> Auto-Submit: Avi Drissman <avi@chromium.org> Owners-Override: Avi Drissman <avi@chromium.org> Commit-Queue: Avi Drissman <avi@chromium.org> Cr-Commit-Position: refs/heads/main@{#1047644}
231 lines
6.8 KiB
Python
Executable File
231 lines
6.8 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# Copyright 2018 The Chromium Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""Run a single fuzz target built with code coverage instrumentation.
|
|
|
|
This script assumes that corresponding corpus was downloaded via gclient sync
|
|
and saved to: src/testing/libfuzzer/fuzzer_corpus/{fuzzer_name}/.
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import zipfile
|
|
|
|
_THIS_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
# Path to the fuzzer corpus directory that is used for bots.
|
|
_CORPUS_FOR_BOTS_DIR = os.path.join(_THIS_DIR, os.path.pardir, os.path.pardir,
|
|
'testing', 'libfuzzer',
|
|
'fuzzer_corpus_for_bots')
|
|
|
|
# Dummy corpus in case real corpus doesn't exist.
|
|
_DUMMY_INPUT_CONTENTS = 'dummy input just to have at least one corpus unit'
|
|
_DUMMY_INPUT_FILENAME = 'dummy_corpus_input'
|
|
|
|
# Used for running fuzzer targets in code coverage config.
|
|
_DUMMY_CORPUS_DIRECTORY = 'dummy_corpus_dir_which_should_be_empty'
|
|
|
|
_LIBFUZZER_FLAGS = ['-merge=1', '-timeout=60', '-rss_limit_mb=8192']
|
|
|
|
_SLEEP_DURATION_SECONDS = 8
|
|
|
|
|
|
def _PrepareCorpus(fuzzer_name, output_dir):
|
|
"""Prepares the corpus to run fuzzer target.
|
|
|
|
If a corpus for bots is available, use it directly, otherwise, creates a
|
|
dummy corpus.
|
|
|
|
Args:
|
|
fuzzer_name (str): Name of the fuzzer to create corpus for.
|
|
output_dir (str): An output directory to store artifacts.
|
|
|
|
Returns:
|
|
A path to the directory of the prepared corpus.
|
|
"""
|
|
corpus_dir = os.path.join(output_dir, fuzzer_name + '_corpus')
|
|
_RecreateDir(corpus_dir)
|
|
|
|
corpus_for_bots = glob.glob(
|
|
os.path.join(os.path.abspath(_CORPUS_FOR_BOTS_DIR), fuzzer_name, '*.zip'))
|
|
if len(corpus_for_bots) >= 2:
|
|
raise RuntimeError(
|
|
'Expected only one, but multiple versions of corpus exit')
|
|
|
|
if len(corpus_for_bots) == 1:
|
|
zipfile.ZipFile(corpus_for_bots[0]).extractall(path=corpus_dir)
|
|
return corpus_dir
|
|
|
|
logging.info('Corpus for %s does not exist, create a dummy corpus input',
|
|
fuzzer_name)
|
|
dummy_input_path = os.path.join(corpus_dir, _DUMMY_INPUT_FILENAME)
|
|
with open(dummy_input_path, 'wb') as fh:
|
|
fh.write(_DUMMY_INPUT_CONTENTS)
|
|
|
|
return corpus_dir
|
|
|
|
|
|
def _ParseCommandArguments():
|
|
"""Adds and parses relevant arguments for tool comands.
|
|
|
|
Returns:
|
|
A dictionary representing the arguments.
|
|
"""
|
|
arg_parser = argparse.ArgumentParser()
|
|
|
|
arg_parser.add_argument(
|
|
'-f',
|
|
'--fuzzer',
|
|
type=str,
|
|
required=True,
|
|
help='Path to the fuzz target executable.')
|
|
|
|
arg_parser.add_argument(
|
|
'-o',
|
|
'--output-dir',
|
|
type=str,
|
|
required=True,
|
|
help='Output directory where corpus and coverage dumps can be stored in.')
|
|
|
|
arg_parser.add_argument(
|
|
'-t',
|
|
'--timeout',
|
|
type=int,
|
|
required=True,
|
|
help='Timeout value for running a single fuzz target.')
|
|
|
|
# Ignored. Used to comply with isolated script contract, see chromium_tests
|
|
# and swarming recipe modules for more details.
|
|
arg_parser.add_argument(
|
|
'--isolated-script-test-output',
|
|
type=str,
|
|
required=False,
|
|
help=argparse.SUPPRESS)
|
|
|
|
# Ditto.
|
|
arg_parser.add_argument(
|
|
'--isolated-script-test-perf-output',
|
|
type=str,
|
|
required=False,
|
|
help=argparse.SUPPRESS)
|
|
|
|
if len(sys.argv) == 1:
|
|
arg_parser.print_help()
|
|
sys.exit(1)
|
|
|
|
args = arg_parser.parse_args()
|
|
|
|
assert os.path.isfile(
|
|
args.fuzzer), ("Fuzzer '%s' does not exist." % args.fuzzer)
|
|
|
|
assert os.path.isdir(
|
|
args.output_dir), ("Output dir '%s' does not exist." % args.output_dir)
|
|
|
|
assert args.timeout > 0, 'Invalid timeout value: %d.' % args.timeout
|
|
|
|
return args
|
|
|
|
|
|
def _RecreateDir(dir_path):
|
|
if os.path.exists(dir_path):
|
|
shutil.rmtree(dir_path)
|
|
os.mkdir(dir_path)
|
|
|
|
|
|
def _RunFuzzTarget(fuzzer, fuzzer_name, output_dir, corpus_dir, timeout):
|
|
# The way we run fuzz targets in code coverage config (-merge=1) requires an
|
|
# empty directory to be provided to fuzz target. We run fuzz targets with
|
|
# -merge=1 because that mode is crash-resistant.
|
|
dummy_corpus_dir = os.path.join(output_dir, _DUMMY_CORPUS_DIRECTORY)
|
|
_RecreateDir(dummy_corpus_dir)
|
|
|
|
cmd = [fuzzer] + _LIBFUZZER_FLAGS + [dummy_corpus_dir, corpus_dir]
|
|
|
|
try:
|
|
_RunWithTimeout(cmd, timeout)
|
|
except Exception as e:
|
|
logging.info('Failed to run %s: %s', fuzzer_name, e)
|
|
|
|
shutil.rmtree(dummy_corpus_dir)
|
|
|
|
|
|
def _RunWithTimeout(cmd, timeout):
|
|
logging.info('Run fuzz target using the following command: %s', str(cmd))
|
|
|
|
# TODO: we may need to use |creationflags=subprocess.CREATE_NEW_PROCESS_GROUP|
|
|
# on Windows or send |signal.CTRL_C_EVENT| signal if the process times out.
|
|
runner = subprocess.Popen(cmd)
|
|
|
|
timer = 0
|
|
while timer < timeout and runner.poll() is None:
|
|
time.sleep(_SLEEP_DURATION_SECONDS)
|
|
timer += _SLEEP_DURATION_SECONDS
|
|
|
|
if runner.poll() is None:
|
|
try:
|
|
logging.info('Fuzz target timed out, interrupting it.')
|
|
# libFuzzer may spawn some child processes, that is why we have to call
|
|
# os.killpg, which would send the signal to our Python process as well, so
|
|
# we just catch and ignore it in this try block.
|
|
os.killpg(os.getpgid(runner.pid), signal.SIGINT)
|
|
except KeyboardInterrupt:
|
|
# Python's default signal handler raises KeyboardInterrupt exception for
|
|
# SIGINT, suppress it here to prevent interrupting the script itself.
|
|
pass
|
|
|
|
runner.communicate()
|
|
|
|
logging.info('Finished running the fuzz target.')
|
|
|
|
|
|
def Main():
|
|
log_format = '[%(asctime)s %(levelname)s] %(message)s'
|
|
logging.basicConfig(level=logging.INFO, format=log_format)
|
|
|
|
args = _ParseCommandArguments()
|
|
fuzzer_name = os.path.splitext(os.path.basename(args.fuzzer))[0]
|
|
corpus_dir = _PrepareCorpus(fuzzer_name, args.output_dir)
|
|
start_time = time.time()
|
|
_RunFuzzTarget(args.fuzzer, fuzzer_name, args.output_dir, corpus_dir,
|
|
args.timeout)
|
|
end_time = time.time()
|
|
shutil.rmtree(corpus_dir)
|
|
|
|
if args.isolated_script_test_output:
|
|
# TODO(crbug.com/913827): Actually comply with the isolated script contract
|
|
# on src/testing/scripts/common.
|
|
with open(args.isolated_script_test_output, 'w') as f:
|
|
json.dump({
|
|
'version': 3,
|
|
'interrupted': False,
|
|
'path_delimiter': '.',
|
|
'seconds_since_epoch': int(start_time),
|
|
'num_failures_by_type': {
|
|
'FAIL': 0,
|
|
'PASS': 1
|
|
},
|
|
'num_regressions': 0,
|
|
'tests': {
|
|
fuzzer_name: {
|
|
'expected': 'PASS',
|
|
'actual': 'PASS',
|
|
'times': [int(end_time - start_time),]
|
|
},
|
|
}
|
|
}, f)
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(Main())
|