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 stat 21import subprocess 22import sys 23 24import helper 25 26ALLOWED_FUZZ_TARGET_EXTENSIONS = ['', '.exe'] 27FUZZ_TARGET_SEARCH_STRING = 'LLVMFuzzerTestOneInput' 28VALID_TARGET_NAME = re.compile(r'^[a-zA-Z0-9_-]+$') 29 30# Location of google cloud storage for latest OSS-Fuzz builds. 31GCS_BASE_URL = 'https://storage.googleapis.com/' 32 33 34def chdir_to_root(): 35 """Changes cwd to OSS-Fuzz root directory.""" 36 # Change to oss-fuzz main directory so helper.py runs correctly. 37 if os.getcwd() != helper.OSS_FUZZ_DIR: 38 os.chdir(helper.OSS_FUZZ_DIR) 39 40 41def execute(command, location=None, check_result=False): 42 """ Runs a shell command in the specified directory location. 43 44 Args: 45 command: The command as a list to be run. 46 location: The directory the command is run in. 47 check_result: Should an exception be thrown on failed command. 48 49 Returns: 50 stdout, stderr, error code. 51 52 Raises: 53 RuntimeError: running a command resulted in an error. 54 """ 55 56 if not location: 57 location = os.getcwd() 58 process = subprocess.Popen(command, 59 stdout=subprocess.PIPE, 60 stderr=subprocess.PIPE, 61 cwd=location) 62 out, err = process.communicate() 63 out = out.decode('utf-8', errors='ignore') 64 err = err.decode('utf-8', errors='ignore') 65 if err: 66 logging.debug('Stderr of command \'%s\' is %s.', ' '.join(command), err) 67 if check_result and process.returncode: 68 raise RuntimeError( 69 'Executing command \'{0}\' failed with error: {1}.'.format( 70 ' '.join(command), err)) 71 return out, err, process.returncode 72 73 74def get_fuzz_targets(path): 75 """Get list of fuzz targets in a directory. 76 77 Args: 78 path: A path to search for fuzz targets in. 79 80 Returns: 81 A list of paths to fuzzers or an empty list if None. 82 """ 83 if not os.path.exists(path): 84 return [] 85 fuzz_target_paths = [] 86 for root, _, fuzzers in os.walk(path): 87 for fuzzer in fuzzers: 88 file_path = os.path.join(root, fuzzer) 89 if is_fuzz_target_local(file_path): 90 fuzz_target_paths.append(file_path) 91 92 return fuzz_target_paths 93 94 95def get_container_name(): 96 """Gets the name of the current docker container you are in. 97 98 Returns: 99 Container name or None if not in a container. 100 """ 101 result = subprocess.run( # pylint: disable=subprocess-run-check 102 ['systemd-detect-virt', '-c'], 103 stdout=subprocess.PIPE).stdout 104 if b'docker' not in result: 105 return None 106 with open('/etc/hostname') as file_handle: 107 return file_handle.read().strip() 108 109 110def is_fuzz_target_local(file_path): 111 """Returns whether |file_path| is a fuzz target binary (local path). 112 Copied from clusterfuzz src/python/bot/fuzzers/utils.py 113 with slight modifications. 114 """ 115 filename, file_extension = os.path.splitext(os.path.basename(file_path)) 116 if not VALID_TARGET_NAME.match(filename): 117 # Check fuzz target has a valid name (without any special chars). 118 return False 119 120 if file_extension not in ALLOWED_FUZZ_TARGET_EXTENSIONS: 121 # Ignore files with disallowed extensions (to prevent opening e.g. .zips). 122 return False 123 124 if not os.path.exists(file_path) or not os.access(file_path, os.X_OK): 125 return False 126 127 if filename.endswith('_fuzzer'): 128 return True 129 130 if os.path.exists(file_path) and not stat.S_ISREG(os.stat(file_path).st_mode): 131 return False 132 133 with open(file_path, 'rb') as file_handle: 134 return file_handle.read().find(FUZZ_TARGET_SEARCH_STRING.encode()) != -1 135 136 137def binary_print(string): 138 """Print that can print a binary string.""" 139 if isinstance(string, bytes): 140 string += b'\n' 141 else: 142 string += '\n' 143 sys.stdout.buffer.write(string) 144 sys.stdout.flush() 145 146 147def url_join(*url_parts): 148 """Joins URLs together using the POSIX join method. 149 150 Args: 151 url_parts: Sections of a URL to be joined. 152 153 Returns: 154 Joined URL. 155 """ 156 return posixpath.join(*url_parts) 157 158 159def gs_url_to_https(url): 160 """Converts |url| from a GCS URL (beginning with 'gs://') to an HTTPS one.""" 161 return url_join(GCS_BASE_URL, remove_prefix(url, 'gs://')) 162 163 164def remove_prefix(string, prefix): 165 """Returns |string| without the leading substring |prefix|.""" 166 # Match behavior of removeprefix from python3.9: 167 # https://www.python.org/dev/peps/pep-0616/ 168 if string.startswith(prefix): 169 return string[len(prefix):] 170 171 return string 172