1# Copyright 2020 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Utilities for OSS-Fuzz infrastructure.""" 15 16import logging 17import os 18import posixpath 19import re 20import shlex 21import stat 22import subprocess 23import sys 24 25import helper 26 27ALLOWED_FUZZ_TARGET_EXTENSIONS = ['', '.exe'] 28FUZZ_TARGET_SEARCH_STRING = 'LLVMFuzzerTestOneInput' 29VALID_TARGET_NAME_REGEX = re.compile(r'^[a-zA-Z0-9_-]+$') 30BLOCKLISTED_TARGET_NAME_REGEX = re.compile(r'^(jazzer_driver.*)$') 31 32# Location of google cloud storage for latest OSS-Fuzz builds. 33GCS_BASE_URL = 'https://storage.googleapis.com/' 34 35 36def chdir_to_root(): 37 """Changes cwd to OSS-Fuzz root directory.""" 38 # Change to oss-fuzz main directory so helper.py runs correctly. 39 if os.getcwd() != helper.OSS_FUZZ_DIR: 40 os.chdir(helper.OSS_FUZZ_DIR) 41 42 43def command_to_string(command): 44 """Returns the stringfied version of |command| a list representing a binary to 45 run and arguments to pass to it or a string representing a binary to run.""" 46 if isinstance(command, str): 47 return command 48 return shlex.join(command) 49 50 51def execute(command, env=None, location=None, check_result=False): 52 """Runs a shell command in the specified directory location. 53 54 Args: 55 command: The command as a list to be run. 56 env: (optional) an environment to pass to Popen to run the command in. 57 location (optional): The directory to run command in. 58 check_result (optional): Should an exception be thrown on failure. 59 60 Returns: 61 stdout, stderr, returncode. 62 63 Raises: 64 RuntimeError: running a command resulted in an error. 65 """ 66 67 if not location: 68 location = os.getcwd() 69 process = subprocess.Popen(command, 70 stdout=subprocess.PIPE, 71 stderr=subprocess.PIPE, 72 cwd=location, 73 env=env) 74 out, err = process.communicate() 75 out = out.decode('utf-8', errors='ignore') 76 err = err.decode('utf-8', errors='ignore') 77 78 command_str = command_to_string(command) 79 if err: 80 logging.debug('Stderr of command "%s" is: %s.', command_str, err) 81 if check_result and process.returncode: 82 raise RuntimeError('Executing command "{0}" failed with error: {1}.'.format( 83 command_str, err)) 84 return out, err, process.returncode 85 86 87def get_fuzz_targets(path, top_level_only=False): 88 """Gets fuzz targets in a directory. 89 90 Args: 91 path: A path to search for fuzz targets in. 92 top_level_only: If True, only search |path|, do not recurse into subdirs. 93 94 Returns: 95 A list of paths to fuzzers or an empty list if None. 96 """ 97 if not os.path.exists(path): 98 return [] 99 fuzz_target_paths = [] 100 for root, _, fuzzers in os.walk(path): 101 if top_level_only and path != root: 102 continue 103 104 for fuzzer in fuzzers: 105 file_path = os.path.join(root, fuzzer) 106 if is_fuzz_target_local(file_path): 107 fuzz_target_paths.append(file_path) 108 109 return fuzz_target_paths 110 111 112def get_container_name(): 113 """Gets the name of the current docker container you are in. 114 115 Returns: 116 Container name or None if not in a container. 117 """ 118 result = subprocess.run( # pylint: disable=subprocess-run-check 119 ['systemd-detect-virt', '-c'], 120 stdout=subprocess.PIPE).stdout 121 if b'docker' not in result: 122 return None 123 with open('/etc/hostname') as file_handle: 124 return file_handle.read().strip() 125 126 127def is_fuzz_target_local(file_path): 128 """Returns whether |file_path| is a fuzz target binary (local path). 129 Copied from clusterfuzz src/python/bot/fuzzers/utils.py 130 with slight modifications. 131 """ 132 # pylint: disable=too-many-return-statements 133 filename, file_extension = os.path.splitext(os.path.basename(file_path)) 134 if not VALID_TARGET_NAME_REGEX.match(filename): 135 # Check fuzz target has a valid name (without any special chars). 136 return False 137 138 if BLOCKLISTED_TARGET_NAME_REGEX.match(filename): 139 # Check fuzz target an explicitly disallowed name (e.g. binaries used for 140 # jazzer-based targets). 141 return False 142 143 if file_extension not in ALLOWED_FUZZ_TARGET_EXTENSIONS: 144 # Ignore files with disallowed extensions (to prevent opening e.g. .zips). 145 return False 146 147 if not os.path.exists(file_path) or not os.access(file_path, os.X_OK): 148 return False 149 150 if filename.endswith('_fuzzer'): 151 return True 152 153 if os.path.exists(file_path) and not stat.S_ISREG(os.stat(file_path).st_mode): 154 return False 155 156 with open(file_path, 'rb') as file_handle: 157 return file_handle.read().find(FUZZ_TARGET_SEARCH_STRING.encode()) != -1 158 159 160def binary_print(string): 161 """Prints string. Can print a binary string.""" 162 if isinstance(string, bytes): 163 string += b'\n' 164 else: 165 string += '\n' 166 sys.stdout.buffer.write(string) 167 sys.stdout.flush() 168 169 170def url_join(*url_parts): 171 """Joins URLs together using the POSIX join method. 172 173 Args: 174 url_parts: Sections of a URL to be joined. 175 176 Returns: 177 Joined URL. 178 """ 179 return posixpath.join(*url_parts) 180 181 182def gs_url_to_https(url): 183 """Converts |url| from a GCS URL (beginning with 'gs://') to an HTTPS one.""" 184 return url_join(GCS_BASE_URL, remove_prefix(url, 'gs://')) 185 186 187def remove_prefix(string, prefix): 188 """Returns |string| without the leading substring |prefix|.""" 189 # Match behavior of removeprefix from python3.9: 190 # https://www.python.org/dev/peps/pep-0616/ 191 if string.startswith(prefix): 192 return string[len(prefix):] 193 194 return string 195