• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2021 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"""Module for getting the configuration CIFuzz needs to run."""
15
16import logging
17import enum
18import os
19import sys
20import json
21
22import environment
23
24# pylint: disable=wrong-import-position,import-error
25sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
26
27import constants
28
29RUN_FUZZERS_MODES = ['batch', 'ci', 'coverage', 'prune']
30SANITIZERS = ['address', 'memory', 'undefined', 'coverage']
31
32# TODO(metzman): Set these on config objects so there's one source of truth.
33DEFAULT_ENGINE = 'libfuzzer'
34DEFAULT_ARCHITECTURE = 'x86_64'
35
36# This module deals a lot with env variables. Many of these will be set by users
37# and others beyond CIFuzz's control. Thus, you should be careful about using
38# the environment.py helpers for getting env vars, since it can cause values
39# that should be interpreted as strings to be returned as other types (bools or
40# ints for example). The environment.py helpers should not be used for values
41# that are supposed to be strings.
42
43
44def _get_pr_ref(event):
45  if event == 'pull_request':
46    return os.getenv('GITHUB_REF')
47  return None
48
49
50def _get_sanitizer():
51  return os.getenv('SANITIZER', constants.DEFAULT_SANITIZER).lower()
52
53
54def _is_dry_run():
55  """Returns True if configured to do a dry run."""
56  return environment.get_bool('DRY_RUN', False)
57
58
59def _get_language():
60  """Returns the project language."""
61  # Get language from environment. We took this approach because the convenience
62  # given to OSS-Fuzz users by not making them specify the language again (and
63  # getting it from the project.yaml) is outweighed by the complexity in
64  # implementing this. A lot of the complexity comes from our unittests not
65  # setting a proper projet at this point.
66  return os.getenv('LANGUAGE', constants.DEFAULT_LANGUAGE)
67
68
69# pylint: disable=too-few-public-methods,too-many-instance-attributes
70
71
72class BaseCiEnvironment:
73  """Base class for CiEnvironment subclasses."""
74
75  @property
76  def workspace(self):
77    """Returns the workspace."""
78    raise NotImplementedError('Child class must implment method.')
79
80  @property
81  def git_sha(self):
82    """Returns the Git SHA to diff against."""
83    raise NotImplementedError('Child class must implment method.')
84
85  @property
86  def token(self):
87    """Returns the CI API token."""
88    raise NotImplementedError('Child class must implment method.')
89
90  @property
91  def project_src_path(self):
92    """Returns the manually checked out path of the project's source if
93    specified or None."""
94
95    path = os.getenv('PROJECT_SRC_PATH')
96    if not path:
97      logging.debug('No PROJECT_SRC_PATH.')
98      return path
99
100    logging.debug('PROJECT_SRC_PATH set: %s.', path)
101    return path
102
103
104class GenericCiEnvironment(BaseCiEnvironment):
105  """CI Environment for generic CI systems."""
106
107  @property
108  def workspace(self):
109    """Returns the workspace."""
110    return os.getenv('WORKSPACE')
111
112  @property
113  def git_sha(self):
114    """Returns the Git SHA to diff against."""
115    return os.getenv('GIT_SHA')
116
117  @property
118  def token(self):
119    """Returns the CI API token."""
120    return os.getenv('TOKEN')
121
122  @property
123  def project_repo_owner_and_name(self):
124    """Returns a tuple containing the project repo owner and None."""
125    repository = os.getenv('REPOSITORY')
126    # Repo owner is a githubism.
127    return None, repository
128
129
130class GithubEnvironment(BaseCiEnvironment):
131  """CI environment for GitHub."""
132
133  @property
134  def workspace(self):
135    """Returns the workspace."""
136    return os.getenv('GITHUB_WORKSPACE')
137
138  @property
139  def git_sha(self):
140    """Returns the Git SHA to diff against."""
141    return os.getenv('GITHUB_SHA')
142
143  @property
144  def token(self):
145    """Returns the CI API token."""
146    return os.getenv('GITHUB_TOKEN')
147
148  @property
149  def project_src_path(self):
150    """Returns the manually checked out path of the project's source if
151    specified or None. The path returned is relative to |self.workspace| since
152    on github the checkout will be relative to there."""
153    # On GitHub, they don't know the absolute path, it is relative to
154    # |workspace|.
155    project_src_path = super().project_src_path
156    if project_src_path is None:
157      return project_src_path
158    return os.path.join(self.workspace, project_src_path)
159
160  @property
161  def project_repo_owner_and_name(self):
162    """Returns a tuple containing the project repo owner and the name of the
163    repo."""
164    # On GitHub this includes owner and repo name.
165    repository = os.getenv('GITHUB_REPOSITORY')
166    # Use os.path.split to split owner from repo.
167    return os.path.split(repository)
168
169
170class ConfigError(Exception):
171  """Error for invalid configuration."""
172
173
174class BaseConfig:
175  """Object containing constant configuration for CIFuzz."""
176
177  class Platform(enum.Enum):
178    """Enum representing the different platforms CIFuzz runs on."""
179    EXTERNAL_GITHUB = 0  # Non-OSS-Fuzz on GitHub actions.
180    INTERNAL_GITHUB = 1  # OSS-Fuzz on GitHub actions.
181    INTERNAL_GENERIC_CI = 2  # OSS-Fuzz on any CI.
182    EXTERNAL_GENERIC_CI = 3  # Non-OSS-Fuzz on any CI.
183
184  def __init__(self):
185    # Need to set these before calling self.platform.
186    self._github_event_path = os.getenv('GITHUB_EVENT_PATH')
187    self.is_github = bool(self._github_event_path)
188    logging.debug('Is github: %s.', self.is_github)
189    self.oss_fuzz_project_name = os.getenv('OSS_FUZZ_PROJECT_NAME')
190
191    self._ci_env = _get_ci_environment(self.platform)
192    self.workspace = self._ci_env.workspace
193
194    self.project_repo_owner, self.project_repo_name = (
195        self._ci_env.project_repo_owner_and_name)
196
197    # Check if failures should not be reported.
198    self.dry_run = _is_dry_run()
199
200    self.sanitizer = _get_sanitizer()
201
202    self.build_integration_path = (
203        constants.DEFAULT_EXTERNAL_BUILD_INTEGRATION_PATH)
204    self.language = _get_language()
205    self.low_disk_space = environment.get_bool('LOW_DISK_SPACE', False)
206
207    self.token = self._ci_env.token
208    self.git_store_repo = os.environ.get('GIT_STORE_REPO')
209    self.git_store_branch = os.environ.get('GIT_STORE_BRANCH')
210    self.git_store_branch_coverage = os.environ.get('GIT_STORE_BRANCH_COVERAGE',
211                                                    self.git_store_branch)
212    self.docker_in_docker = os.environ.get('DOCKER_IN_DOCKER')
213
214    # TODO(metzman): Fix tests to create valid configurations and get rid of
215    # CIFUZZ_TEST here and in presubmit.py.
216    if not os.getenv('CIFUZZ_TEST') and not self.validate():
217      raise ConfigError('Invalid Configuration.')
218
219  def validate(self):
220    """Returns False if the configuration is invalid."""
221    # Do validation here so that unittests don't need to make a fully-valid
222    # config.
223    if not self.workspace:
224      logging.error('Must set WORKSPACE.')
225      return False
226
227    if self.sanitizer not in SANITIZERS:
228      logging.error('Invalid SANITIZER: %s. Must be one of: %s.',
229                    self.sanitizer, SANITIZERS)
230      return False
231
232    if self.language not in constants.LANGUAGES:
233      logging.error('Invalid LANGUAGE: %s. Must be one of: %s.', self.language,
234                    constants.LANGUAGES)
235      return False
236
237    return True
238
239  @property
240  def is_internal(self):
241    """Returns True if this is an OSS-Fuzz project."""
242    return bool(self.oss_fuzz_project_name)
243
244  @property
245  def platform(self):
246    """Returns the platform CIFuzz is runnning on."""
247    if not self.is_internal:
248      if not self.is_github:
249        return self.Platform.EXTERNAL_GENERIC_CI
250      return self.Platform.EXTERNAL_GITHUB
251
252    if self.is_github:
253      return self.Platform.INTERNAL_GITHUB
254    return self.Platform.INTERNAL_GENERIC_CI
255
256  @property
257  def is_coverage(self):
258    """Returns True if this CIFuzz run (building fuzzers and running them) for
259    generating a coverage report."""
260    return self.sanitizer == 'coverage'
261
262
263_CI_ENVIRONMENT_MAPPING = {
264    BaseConfig.Platform.EXTERNAL_GITHUB: GithubEnvironment,
265    BaseConfig.Platform.INTERNAL_GITHUB: GithubEnvironment,
266    BaseConfig.Platform.INTERNAL_GENERIC_CI: GenericCiEnvironment,
267    BaseConfig.Platform.EXTERNAL_GENERIC_CI: GenericCiEnvironment,
268}
269
270
271def _get_ci_environment(platform):
272  """Returns the CI environment object for |platform|."""
273  return _CI_ENVIRONMENT_MAPPING[platform]()
274
275
276class RunFuzzersConfig(BaseConfig):
277  """Class containing constant configuration for running fuzzers in CIFuzz."""
278
279  def __init__(self):
280    super().__init__()
281    # TODO(metzman): Pick a better default for pruning.
282    self.fuzz_seconds = int(os.environ.get('FUZZ_SECONDS', 600))
283    self.run_fuzzers_mode = os.environ.get('RUN_FUZZERS_MODE', 'ci').lower()
284    if self.is_coverage:
285      self.run_fuzzers_mode = 'coverage'
286
287    self.report_unreproducible_crashes = environment.get_bool(
288        'REPORT_UNREPRODUCIBLE_CRASHES', False)
289
290    # TODO(metzman): Fix tests to create valid configurations and get rid of
291    # CIFUZZ_TEST here and in presubmit.py.
292    if not os.getenv('CIFUZZ_TEST') and not self._run_config_validate():
293      raise ConfigError('Invalid Run Configuration.')
294
295  def _run_config_validate(self):
296    """Do extra validation on RunFuzzersConfig.__init__(). Do not name this
297    validate or else it will be called when using the parent's __init__ and will
298    fail. Returns True if valid."""
299    if self.run_fuzzers_mode not in RUN_FUZZERS_MODES:
300      logging.error('Invalid RUN_FUZZERS_MODE: %s. Must be one of %s.',
301                    self.run_fuzzers_mode, RUN_FUZZERS_MODES)
302      return False
303
304    return True
305
306
307class BuildFuzzersConfig(BaseConfig):
308  """Class containing constant configuration for building fuzzers in CIFuzz."""
309
310  def _get_config_from_event_path(self, event):
311    if not self._github_event_path:
312      return
313    with open(self._github_event_path, encoding='utf-8') as file_handle:
314      event_data = json.load(file_handle)
315    if event == 'push':
316      self.base_commit = event_data['before']
317      logging.debug('base_commit: %s', self.base_commit)
318    elif event == 'pull_request':
319      self.pr_ref = f'refs/pull/{event_data["pull_request"]["number"]}/merge'
320      logging.debug('pr_ref: %s', self.pr_ref)
321
322    self.git_url = event_data['repository']['html_url']
323
324  def __init__(self):
325    """Get the configuration from CIFuzz from the environment. These variables
326    are set by GitHub or the user."""
327    super().__init__()
328    self.commit_sha = self._ci_env.git_sha
329    event = os.getenv('GITHUB_EVENT_NAME')
330
331    self.pr_ref = None
332    self.git_url = None
333    self.base_commit = None
334    self._get_config_from_event_path(event)
335
336    self.base_ref = os.getenv('GITHUB_BASE_REF')
337    self.project_src_path = self._ci_env.project_src_path
338
339    self.allowed_broken_targets_percentage = os.getenv(
340        'ALLOWED_BROKEN_TARGETS_PERCENTAGE')
341    self.bad_build_check = environment.get_bool('BAD_BUILD_CHECK', True)
342    # pylint: disable=consider-using-ternary
343    self.keep_unaffected_fuzz_targets = (
344        # Not from a commit or PR.
345        (not self.base_ref and not self.base_commit) or
346        environment.get_bool('KEEP_UNAFFECTED_FUZZERS'))
347    self.upload_build = environment.get_bool('UPLOAD_BUILD', False)
348    if self.upload_build:
349      logging.info('Keeping all fuzzers because we are uploading build.')
350      self.keep_unaffected_fuzz_targets = True
351
352    if self.sanitizer == 'coverage':
353      self.keep_unaffected_fuzz_targets = True
354      self.bad_build_check = False
355