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