• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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