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"""Tests the functionality of the cifuzz module."""
15import os
16import shutil
17import sys
18import tempfile
19import unittest
20from unittest import mock
21
22import parameterized
23
24# pylint: disable=wrong-import-position
25INFRA_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26sys.path.append(INFRA_DIR)
27
28OSS_FUZZ_DIR = os.path.dirname(INFRA_DIR)
29
30import build_fuzzers
31import config_utils
32import continuous_integration
33import test_helpers
34
35# NOTE: This integration test relies on
36# https://github.com/google/oss-fuzz/tree/master/projects/example project.
37EXAMPLE_PROJECT = 'example'
38
39# Location of data used for testing.
40TEST_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)),
41                              'test_data')
42
43# An example fuzzer that triggers an crash.
44# Binary is a copy of the example project's do_stuff_fuzzer and can be
45# generated by running "python3 infra/helper.py build_fuzzers example".
46EXAMPLE_CRASH_FUZZER = 'example_crash_fuzzer'
47
48# An example fuzzer that does not trigger a crash.
49# Binary is a modified version of example project's do_stuff_fuzzer. It is
50# created by removing the bug in my_api.cpp.
51EXAMPLE_NOCRASH_FUZZER = 'example_nocrash_fuzzer'
52
53# A fuzzer to be built in build_fuzzers integration tests.
54EXAMPLE_BUILD_FUZZER = 'do_stuff_fuzzer'
55
56# pylint: disable=no-self-use,protected-access,too-few-public-methods
57
58
59def create_config(**kwargs):
60  """Creates a config object and then sets every attribute that is a key in
61  |kwargs| to the corresponding value. Asserts that each key in |kwargs| is an
62  attribute of Config."""
63  with mock.patch('os.path.basename', return_value=None), mock.patch(
64      'config_utils.get_project_src_path',
65      return_value=None), mock.patch('config_utils._is_dry_run',
66                                     return_value=True):
67    config = config_utils.BuildFuzzersConfig()
68
69  for key, value in kwargs.items():
70    assert hasattr(config, key), 'Config doesn\'t have attribute: ' + key
71    setattr(config, key, value)
72  return config
73
74
75class BuildFuzzersTest(unittest.TestCase):
76  """Unit tests for build_fuzzers."""
77
78  @mock.patch('build_specified_commit.detect_main_repo',
79              return_value=('example.com', '/path'))
80  @mock.patch('repo_manager._clone', return_value=None)
81  @mock.patch('continuous_integration.checkout_specified_commit')
82  @mock.patch('helper.docker_run')
83  def test_cifuzz_env_var(self, mocked_docker_run, _, __, ___):
84    """Tests that the CIFUZZ env var is set."""
85
86    with tempfile.TemporaryDirectory() as tmp_dir:
87      build_fuzzers.build_fuzzers(
88          create_config(project_name=EXAMPLE_PROJECT,
89                        project_repo_name=EXAMPLE_PROJECT,
90                        workspace=tmp_dir,
91                        pr_ref='refs/pull/1757/merge'))
92    docker_run_command = mocked_docker_run.call_args_list[0][0][0]
93
94    def command_has_env_var_arg(command, env_var_arg):
95      for idx, element in enumerate(command):
96        if idx == 0:
97          continue
98
99        if element == env_var_arg and command[idx - 1] == '-e':
100          return True
101      return False
102
103    self.assertTrue(command_has_env_var_arg(docker_run_command, 'CIFUZZ=True'))
104
105
106class InternalGithubBuildTest(unittest.TestCase):
107  """Tests for building OSS-Fuzz projects on GitHub actions."""
108  PROJECT_NAME = 'myproject'
109  PROJECT_REPO_NAME = 'myproject'
110  SANITIZER = 'address'
111  COMMIT_SHA = 'fake'
112  PR_REF = 'fake'
113
114  def _create_builder(self, tmp_dir):
115    """Creates an InternalGithubBuilder and returns it."""
116    config = create_config(project_name=self.PROJECT_NAME,
117                           project_repo_name=self.PROJECT_REPO_NAME,
118                           workspace=tmp_dir,
119                           sanitizer=self.SANITIZER,
120                           commit_sha=self.COMMIT_SHA,
121                           pr_ref=self.PR_REF,
122                           is_github=True)
123    ci_system = continuous_integration.get_ci(config)
124    return build_fuzzers.Builder(config, ci_system)
125
126  @mock.patch('repo_manager._clone', side_effect=None)
127  @mock.patch('continuous_integration.checkout_specified_commit',
128              side_effect=None)
129  def test_correct_host_repo_path(self, _, __):
130    """Tests that the correct self.host_repo_path is set by
131    build_image_and_checkout_src. Specifically, we want the name of the
132    directory the repo is in to match the name used in the docker
133    image/container, so that it will replace the host's copy properly."""
134    image_repo_path = '/src/repo_dir'
135    with tempfile.TemporaryDirectory() as tmp_dir, mock.patch(
136        'build_specified_commit.detect_main_repo',
137        return_value=('inferred_url', image_repo_path)):
138      builder = self._create_builder(tmp_dir)
139      builder.build_image_and_checkout_src()
140
141    self.assertEqual(os.path.basename(builder.host_repo_path),
142                     os.path.basename(image_repo_path))
143
144
145@unittest.skipIf(not os.getenv('INTEGRATION_TESTS'),
146                 'INTEGRATION_TESTS=1 not set')
147class BuildFuzzersIntegrationTest(unittest.TestCase):
148  """Integration tests for build_fuzzers."""
149
150  def setUp(self):
151    self.tmp_dir_obj = tempfile.TemporaryDirectory()
152    self.workspace = self.tmp_dir_obj.name
153    self.out_dir = os.path.join(self.workspace, 'out')
154    test_helpers.patch_environ(self)
155
156  def tearDown(self):
157    self.tmp_dir_obj.cleanup()
158
159  def test_external_github_project(self):
160    """Tests building fuzzers from an external project on Github."""
161    project_name = 'external-project'
162    build_integration_path = 'fuzzer-build-integration'
163    git_url = 'https://github.com/jonathanmetzman/cifuzz-external-example.git'
164    # This test is dependant on the state of
165    # github.com/jonathanmetzman/cifuzz-external-example.
166    config = create_config(project_name=project_name,
167                           project_repo_name=project_name,
168                           workspace=self.workspace,
169                           build_integration_path=build_integration_path,
170                           git_url=git_url,
171                           commit_sha='HEAD',
172                           base_commit='HEAD^1')
173    self.assertTrue(build_fuzzers.build_fuzzers(config))
174    self.assertTrue(
175        os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER)))
176
177  def test_valid_commit(self):
178    """Tests building fuzzers with valid inputs."""
179    config = create_config(
180        project_name=EXAMPLE_PROJECT,
181        project_repo_name='oss-fuzz',
182        workspace=self.workspace,
183        commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523',
184        base_commit='da0746452433dc18bae699e355a9821285d863c8',
185        is_github=True)
186    self.assertTrue(build_fuzzers.build_fuzzers(config))
187    self.assertTrue(
188        os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER)))
189
190  def test_valid_pull_request(self):
191    """Tests building fuzzers with valid pull request."""
192    # TODO(metzman): What happens when this branch closes?
193    config = create_config(project_name=EXAMPLE_PROJECT,
194                           project_repo_name='oss-fuzz',
195                           workspace=self.workspace,
196                           pr_ref='refs/pull/1757/merge',
197                           base_ref='master',
198                           is_github=True)
199    self.assertTrue(build_fuzzers.build_fuzzers(config))
200    self.assertTrue(
201        os.path.exists(os.path.join(self.out_dir, EXAMPLE_BUILD_FUZZER)))
202
203  def test_invalid_pull_request(self):
204    """Tests building fuzzers with invalid pull request."""
205    config = create_config(project_name=EXAMPLE_PROJECT,
206                           project_repo_name='oss-fuzz',
207                           workspace=self.workspace,
208                           pr_ref='ref-1/merge',
209                           base_ref='master',
210                           is_github=True)
211    self.assertTrue(build_fuzzers.build_fuzzers(config))
212
213  def test_invalid_project_name(self):
214    """Tests building fuzzers with invalid project name."""
215    config = create_config(
216        project_name='not_a_valid_project',
217        project_repo_name='oss-fuzz',
218        workspace=self.workspace,
219        commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523')
220    self.assertFalse(build_fuzzers.build_fuzzers(config))
221
222  def test_invalid_repo_name(self):
223    """Tests building fuzzers with invalid repo name."""
224    config = create_config(
225        project_name=EXAMPLE_PROJECT,
226        project_repo_name='not-real-repo',
227        workspace=self.workspace,
228        commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523')
229    self.assertFalse(build_fuzzers.build_fuzzers(config))
230
231  def test_invalid_commit_sha(self):
232    """Tests building fuzzers with invalid commit SHA."""
233    config = create_config(project_name=EXAMPLE_PROJECT,
234                           project_repo_name='oss-fuzz',
235                           workspace=self.workspace,
236                           commit_sha='',
237                           is_github=True)
238    with self.assertRaises(AssertionError):
239      build_fuzzers.build_fuzzers(config)
240
241  def test_invalid_workspace(self):
242    """Tests building fuzzers with invalid workspace."""
243    config = create_config(
244        project_name=EXAMPLE_PROJECT,
245        project_repo_name='oss-fuzz',
246        workspace=os.path.join(self.workspace, 'not', 'a', 'dir'),
247        commit_sha='0b95fe1039ed7c38fea1f97078316bfc1030c523')
248    self.assertFalse(build_fuzzers.build_fuzzers(config))
249
250
251class CheckFuzzerBuildTest(unittest.TestCase):
252  """Tests the check_fuzzer_build function in the cifuzz module."""
253
254  SANITIZER = 'address'
255  LANGUAGE = 'c++'
256
257  def setUp(self):
258    self.tmp_dir_obj = tempfile.TemporaryDirectory()
259    self.test_files_path = os.path.join(self.tmp_dir_obj.name, 'test_files')
260    shutil.copytree(TEST_DATA_PATH, self.test_files_path)
261
262  def tearDown(self):
263    self.tmp_dir_obj.cleanup()
264
265  def test_correct_fuzzer_build(self):
266    """Checks check_fuzzer_build function returns True for valid fuzzers."""
267    test_fuzzer_dir = os.path.join(self.test_files_path, 'out')
268    self.assertTrue(
269        build_fuzzers.check_fuzzer_build(test_fuzzer_dir, self.SANITIZER,
270                                         self.LANGUAGE))
271
272  def test_not_a_valid_fuzz_path(self):
273    """Tests that False is returned when a bad path is given."""
274    self.assertFalse(
275        build_fuzzers.check_fuzzer_build('not/a/valid/path', self.SANITIZER,
276                                         self.LANGUAGE))
277
278  def test_not_a_valid_fuzzer(self):
279    """Checks a directory that exists but does not have fuzzers is False."""
280    self.assertFalse(
281        build_fuzzers.check_fuzzer_build(self.test_files_path, self.SANITIZER,
282                                         self.LANGUAGE))
283
284  @mock.patch('helper.docker_run')
285  def test_allow_broken_fuzz_targets_percentage(self, mocked_docker_run):
286    """Tests that ALLOWED_BROKEN_TARGETS_PERCENTAGE is set when running
287    docker if passed to check_fuzzer_build."""
288    mocked_docker_run.return_value = 0
289    test_fuzzer_dir = os.path.join(TEST_DATA_PATH, 'out')
290    build_fuzzers.check_fuzzer_build(test_fuzzer_dir,
291                                     self.SANITIZER,
292                                     self.LANGUAGE,
293                                     allowed_broken_targets_percentage='0')
294    self.assertIn('-e ALLOWED_BROKEN_TARGETS_PERCENTAGE=0',
295                  ' '.join(mocked_docker_run.call_args[0][0]))
296
297
298@unittest.skip('Test is too long to be run with presubmit.')
299class BuildSantizerIntegrationTest(unittest.TestCase):
300  """Integration tests for the build_fuzzers.
301    Note: This test relies on "curl" being an OSS-Fuzz project."""
302  PROJECT_NAME = 'curl'
303  PR_REF = 'fake_pr'
304
305  @classmethod
306  def _create_config(cls, tmp_dir, sanitizer):
307    return create_config(project_name=cls.PROJECT_NAME,
308                         project_repo_name=cls.PROJECT_NAME,
309                         workspace=tmp_dir,
310                         pr_ref=cls.PR_REF,
311                         sanitizer=sanitizer)
312
313  @parameterized.parameterized.expand([('memory',), ('undefined',)])
314  def test_valid_project_curl(self, sanitizer):
315    """Tests that MSAN can be detected from project.yaml"""
316    with tempfile.TemporaryDirectory() as tmp_dir:
317      self.assertTrue(
318          build_fuzzers.build_fuzzers(self._create_config(tmp_dir, sanitizer)))
319
320
321class GetDockerBuildFuzzersArgsContainerTest(unittest.TestCase):
322  """Tests that _get_docker_build_fuzzers_args_container works as intended."""
323
324  def test_get_docker_build_fuzzers_args_container(self):
325    """Tests that _get_docker_build_fuzzers_args_container works as intended."""
326    out_dir = '/my/out'
327    container = 'my-container'
328    result = build_fuzzers._get_docker_build_fuzzers_args_container(
329        out_dir, container)
330    self.assertEqual(result, ['-e', 'OUT=/my/out', '--volumes-from', container])
331
332
333class GetDockerBuildFuzzersArgsNotContainerTest(unittest.TestCase):
334  """Tests that _get_docker_build_fuzzers_args_not_container works as
335  intended."""
336
337  def test_get_docker_build_fuzzers_args_no_container(self):
338    """Tests that _get_docker_build_fuzzers_args_not_container works
339    as intended."""
340    host_out_dir = '/cifuzz/out'
341    host_repo_path = '/host/repo'
342    result = build_fuzzers._get_docker_build_fuzzers_args_not_container(
343        host_out_dir, host_repo_path)
344    expected_result = [
345        '-e', 'OUT=/out', '-v', '/cifuzz/out:/out', '-v',
346        '/host/repo:/host/repo'
347    ]
348    self.assertEqual(result, expected_result)
349
350
351class GetDockerBuildFuzzersArgsMsanTest(unittest.TestCase):
352  """Tests that _get_docker_build_fuzzers_args_msan works as intended."""
353
354  def test_get_docker_build_fuzzers_args_msan(self):
355    """Tests that _get_docker_build_fuzzers_args_msan works as intended."""
356    work_dir = '/work_dir'
357    result = build_fuzzers._get_docker_build_fuzzers_args_msan(work_dir)
358    expected_result = ['-e', 'MSAN_LIBS_PATH=/work_dir/msan']
359    self.assertEqual(result, expected_result)
360
361
362if __name__ == '__main__':
363  unittest.main()
364