2020-01-29 19:03:43 +00:00
|
|
|
# Copyright 2020 Google LLC
|
|
|
|
#
|
|
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
# you may not use this file except in compliance with the License.
|
|
|
|
# You may obtain a copy of the License at
|
|
|
|
#
|
|
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
#
|
|
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
# See the License for the specific language governing permissions and
|
|
|
|
# limitations under the License.
|
|
|
|
"""Utilities for OSS-Fuzz infrastructure."""
|
|
|
|
|
2020-02-04 19:51:18 +00:00
|
|
|
import logging
|
2020-01-29 19:03:43 +00:00
|
|
|
import os
|
2021-01-20 14:47:48 +00:00
|
|
|
import posixpath
|
2020-01-29 19:03:43 +00:00
|
|
|
import re
|
2021-10-05 13:16:54 +00:00
|
|
|
import shlex
|
2020-01-29 19:03:43 +00:00
|
|
|
import stat
|
2020-01-31 18:19:12 +00:00
|
|
|
import subprocess
|
2020-12-08 17:05:48 +00:00
|
|
|
import sys
|
2020-01-29 19:03:43 +00:00
|
|
|
|
|
|
|
import helper
|
|
|
|
|
|
|
|
ALLOWED_FUZZ_TARGET_EXTENSIONS = ['', '.exe']
|
|
|
|
FUZZ_TARGET_SEARCH_STRING = 'LLVMFuzzerTestOneInput'
|
2021-09-29 15:43:40 +00:00
|
|
|
VALID_TARGET_NAME_REGEX = re.compile(r'^[a-zA-Z0-9_-]+$')
|
|
|
|
BLOCKLISTED_TARGET_NAME_REGEX = re.compile(r'^(jazzer_driver.*)$')
|
2020-01-29 19:03:43 +00:00
|
|
|
|
2021-01-20 14:47:48 +00:00
|
|
|
# Location of google cloud storage for latest OSS-Fuzz builds.
|
|
|
|
GCS_BASE_URL = 'https://storage.googleapis.com/'
|
|
|
|
|
2020-01-29 19:03:43 +00:00
|
|
|
|
|
|
|
def chdir_to_root():
|
|
|
|
"""Changes cwd to OSS-Fuzz root directory."""
|
|
|
|
# Change to oss-fuzz main directory so helper.py runs correctly.
|
2020-03-24 17:04:27 +00:00
|
|
|
if os.getcwd() != helper.OSS_FUZZ_DIR:
|
|
|
|
os.chdir(helper.OSS_FUZZ_DIR)
|
2020-01-29 19:03:43 +00:00
|
|
|
|
|
|
|
|
2021-10-05 13:16:54 +00:00
|
|
|
def command_to_string(command):
|
|
|
|
"""Returns the stringfied version of |command| a list representing a binary to
|
|
|
|
run and arguments to pass to it or a string representing a binary to run."""
|
|
|
|
if isinstance(command, str):
|
|
|
|
return command
|
|
|
|
return shlex.join(command)
|
|
|
|
|
|
|
|
|
2021-10-20 02:51:57 +00:00
|
|
|
def execute(command,
|
|
|
|
env=None,
|
|
|
|
location=None,
|
|
|
|
check_result=False,
|
|
|
|
log_command=True):
|
2021-08-05 20:27:24 +00:00
|
|
|
"""Runs a shell command in the specified directory location.
|
2020-01-29 19:03:43 +00:00
|
|
|
|
2020-01-31 18:19:12 +00:00
|
|
|
Args:
|
|
|
|
command: The command as a list to be run.
|
2021-08-05 20:27:24 +00:00
|
|
|
env: (optional) an environment to pass to Popen to run the command in.
|
2021-10-05 13:16:54 +00:00
|
|
|
location (optional): The directory to run command in.
|
|
|
|
check_result (optional): Should an exception be thrown on failure.
|
2020-01-29 19:03:43 +00:00
|
|
|
|
2020-01-31 18:19:12 +00:00
|
|
|
Returns:
|
2021-10-05 13:16:54 +00:00
|
|
|
stdout, stderr, returncode.
|
2020-01-29 19:03:43 +00:00
|
|
|
|
2020-01-31 18:19:12 +00:00
|
|
|
Raises:
|
|
|
|
RuntimeError: running a command resulted in an error.
|
|
|
|
"""
|
2020-01-29 19:03:43 +00:00
|
|
|
|
2020-01-31 18:19:12 +00:00
|
|
|
if not location:
|
|
|
|
location = os.getcwd()
|
2020-02-04 19:51:18 +00:00
|
|
|
process = subprocess.Popen(command,
|
|
|
|
stdout=subprocess.PIPE,
|
|
|
|
stderr=subprocess.PIPE,
|
2021-08-05 20:27:24 +00:00
|
|
|
cwd=location,
|
|
|
|
env=env)
|
2020-01-31 18:19:12 +00:00
|
|
|
out, err = process.communicate()
|
2020-05-04 06:50:25 +00:00
|
|
|
out = out.decode('utf-8', errors='ignore')
|
|
|
|
err = err.decode('utf-8', errors='ignore')
|
2021-10-05 13:16:54 +00:00
|
|
|
|
2021-10-20 02:51:57 +00:00
|
|
|
if log_command:
|
|
|
|
command_str = command_to_string(command)
|
|
|
|
display_err = err
|
|
|
|
else:
|
|
|
|
command_str = 'redacted'
|
|
|
|
display_err = 'redacted'
|
|
|
|
|
2020-02-04 19:51:18 +00:00
|
|
|
if err:
|
2021-10-20 02:51:57 +00:00
|
|
|
logging.debug('Stderr of command "%s" is: %s.', command_str, display_err)
|
2020-02-04 19:51:18 +00:00
|
|
|
if check_result and process.returncode:
|
2021-10-05 13:16:54 +00:00
|
|
|
raise RuntimeError('Executing command "{0}" failed with error: {1}.'.format(
|
2021-10-20 02:51:57 +00:00
|
|
|
command_str, display_err))
|
2020-02-04 19:51:18 +00:00
|
|
|
return out, err, process.returncode
|
2020-01-29 19:03:43 +00:00
|
|
|
|
|
|
|
|
2021-11-09 12:49:21 +00:00
|
|
|
def get_fuzz_targets(path):
|
2021-10-05 13:16:54 +00:00
|
|
|
"""Gets fuzz targets in a directory.
|
2020-01-29 19:03:43 +00:00
|
|
|
|
|
|
|
Args:
|
|
|
|
path: A path to search for fuzz targets in.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A list of paths to fuzzers or an empty list if None.
|
|
|
|
"""
|
|
|
|
if not os.path.exists(path):
|
|
|
|
return []
|
|
|
|
fuzz_target_paths = []
|
2020-04-01 17:10:51 +00:00
|
|
|
for root, _, fuzzers in os.walk(path):
|
|
|
|
for fuzzer in fuzzers:
|
|
|
|
file_path = os.path.join(root, fuzzer)
|
2020-01-29 19:03:43 +00:00
|
|
|
if is_fuzz_target_local(file_path):
|
|
|
|
fuzz_target_paths.append(file_path)
|
|
|
|
|
|
|
|
return fuzz_target_paths
|
|
|
|
|
|
|
|
|
|
|
|
def get_container_name():
|
|
|
|
"""Gets the name of the current docker container you are in.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Container name or None if not in a container.
|
|
|
|
"""
|
2020-07-09 21:18:21 +00:00
|
|
|
result = subprocess.run( # pylint: disable=subprocess-run-check
|
|
|
|
['systemd-detect-virt', '-c'],
|
|
|
|
stdout=subprocess.PIPE).stdout
|
2020-07-09 19:51:34 +00:00
|
|
|
if b'docker' not in result:
|
|
|
|
return None
|
2020-01-29 19:03:43 +00:00
|
|
|
with open('/etc/hostname') as file_handle:
|
|
|
|
return file_handle.read().strip()
|
2020-01-31 18:19:12 +00:00
|
|
|
|
|
|
|
|
2021-11-09 12:49:21 +00:00
|
|
|
def is_executable(file_path):
|
|
|
|
"""Returns True if |file_path| is an exectuable."""
|
|
|
|
return os.path.exists(file_path) and os.access(file_path, os.X_OK)
|
|
|
|
|
|
|
|
|
2020-01-31 18:19:12 +00:00
|
|
|
def is_fuzz_target_local(file_path):
|
|
|
|
"""Returns whether |file_path| is a fuzz target binary (local path).
|
|
|
|
Copied from clusterfuzz src/python/bot/fuzzers/utils.py
|
|
|
|
with slight modifications.
|
|
|
|
"""
|
2021-09-29 15:43:40 +00:00
|
|
|
# pylint: disable=too-many-return-statements
|
2020-01-31 18:19:12 +00:00
|
|
|
filename, file_extension = os.path.splitext(os.path.basename(file_path))
|
2021-09-29 15:43:40 +00:00
|
|
|
if not VALID_TARGET_NAME_REGEX.match(filename):
|
2020-01-31 18:19:12 +00:00
|
|
|
# Check fuzz target has a valid name (without any special chars).
|
|
|
|
return False
|
|
|
|
|
2021-09-29 15:43:40 +00:00
|
|
|
if BLOCKLISTED_TARGET_NAME_REGEX.match(filename):
|
|
|
|
# Check fuzz target an explicitly disallowed name (e.g. binaries used for
|
|
|
|
# jazzer-based targets).
|
|
|
|
return False
|
|
|
|
|
2020-01-31 18:19:12 +00:00
|
|
|
if file_extension not in ALLOWED_FUZZ_TARGET_EXTENSIONS:
|
|
|
|
# Ignore files with disallowed extensions (to prevent opening e.g. .zips).
|
|
|
|
return False
|
|
|
|
|
2021-11-09 12:49:21 +00:00
|
|
|
if not is_executable(file_path):
|
2020-01-31 18:19:12 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
if filename.endswith('_fuzzer'):
|
|
|
|
return True
|
|
|
|
|
|
|
|
if os.path.exists(file_path) and not stat.S_ISREG(os.stat(file_path).st_mode):
|
|
|
|
return False
|
|
|
|
|
|
|
|
with open(file_path, 'rb') as file_handle:
|
|
|
|
return file_handle.read().find(FUZZ_TARGET_SEARCH_STRING.encode()) != -1
|
2020-12-08 17:05:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
def binary_print(string):
|
2021-10-05 13:16:54 +00:00
|
|
|
"""Prints string. Can print a binary string."""
|
2020-12-08 17:05:48 +00:00
|
|
|
if isinstance(string, bytes):
|
|
|
|
string += b'\n'
|
|
|
|
else:
|
|
|
|
string += '\n'
|
|
|
|
sys.stdout.buffer.write(string)
|
|
|
|
sys.stdout.flush()
|
2021-01-20 14:47:48 +00:00
|
|
|
|
|
|
|
|
|
|
|
def url_join(*url_parts):
|
|
|
|
"""Joins URLs together using the POSIX join method.
|
|
|
|
|
|
|
|
Args:
|
|
|
|
url_parts: Sections of a URL to be joined.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Joined URL.
|
|
|
|
"""
|
|
|
|
return posixpath.join(*url_parts)
|
|
|
|
|
|
|
|
|
|
|
|
def gs_url_to_https(url):
|
2021-01-20 14:51:23 +00:00
|
|
|
"""Converts |url| from a GCS URL (beginning with 'gs://') to an HTTPS one."""
|
2021-01-20 18:13:42 +00:00
|
|
|
return url_join(GCS_BASE_URL, remove_prefix(url, 'gs://'))
|
|
|
|
|
|
|
|
|
|
|
|
def remove_prefix(string, prefix):
|
|
|
|
"""Returns |string| without the leading substring |prefix|."""
|
2021-01-20 20:59:11 +00:00
|
|
|
# Match behavior of removeprefix from python3.9:
|
|
|
|
# https://www.python.org/dev/peps/pep-0616/
|
|
|
|
if string.startswith(prefix):
|
|
|
|
return string[len(prefix):]
|
|
|
|
|
|
|
|
return string
|