# Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Module used by CI tools in order to interact with fuzzers. This module helps CI tools do the following: 1. Build fuzzers. 2. Run fuzzers. Eventually it will be used to help CI tools determine which fuzzers to run. """ import logging import os import shutil import sys import fuzz_target # pylint: disable=wrong-import-position # pylint: disable=import-error sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import build_specified_commit import helper import repo_manager import utils # From clusterfuzz: src/python/crash_analysis/crash_analyzer.py # Used to get the beginning of the stack trace. STACKTRACE_TOOL_MARKERS = [ 'AddressSanitizer', 'ASAN:', 'CFI: Most likely a control flow integrity violation;', 'ERROR: libFuzzer', 'KASAN:', 'LeakSanitizer', 'MemorySanitizer', 'ThreadSanitizer', 'UndefinedBehaviorSanitizer', 'UndefinedSanitizer', ] # From clusterfuzz: src/python/crash_analysis/crash_analyzer.py # Used to get the end of the stack trace. STACKTRACE_END_MARKERS = [ 'ABORTING', 'END MEMORY TOOL REPORT', 'End of process memory map.', 'END_KASAN_OUTPUT', 'SUMMARY:', 'Shadow byte and word', '[end of stack trace]', '\nExiting', 'minidump has been written', ] # TODO: Turn default logging to WARNING when CIFuzz is stable logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG) def build_fuzzers(project_name, project_repo_name, workspace, pr_ref=None, commit_sha=None): """Builds all of the fuzzers for a specific OSS-Fuzz project. Args: project_name: The name of the OSS-Fuzz project being built. project_repo_name: The name of the projects repo. workspace: The location in a shared volume to store a git repo and build artifacts. pr_ref: The pull request reference to be built. commit_sha: The commit sha for the project to be built at. Returns: True if build succeeded or False on failure. """ # Validate inputs. assert pr_ref or commit_sha if not os.path.exists(workspace): logging.error('Invalid workspace: %s.', workspace) return False git_workspace = os.path.join(workspace, 'storage') os.makedirs(git_workspace, exist_ok=True) out_dir = os.path.join(workspace, 'out') os.makedirs(out_dir, exist_ok=True) # Detect repo information. inferred_url, oss_fuzz_repo_path = build_specified_commit.detect_main_repo( project_name, repo_name=project_repo_name) if not inferred_url or not oss_fuzz_repo_path: logging.error('Could not detect repo from project %s.', project_name) return False src_in_docker = os.path.dirname(oss_fuzz_repo_path) oss_fuzz_repo_name = os.path.basename(oss_fuzz_repo_path) # Checkout projects repo in the shared volume. build_repo_manager = repo_manager.RepoManager(inferred_url, git_workspace, repo_name=oss_fuzz_repo_name) try: if pr_ref: build_repo_manager.checkout_pr(pr_ref) else: build_repo_manager.checkout_commit(commit_sha) except RuntimeError: logging.error('Can not check out requested state.') return False except ValueError: logging.error('Invalid commit SHA requested %s.', commit_sha) return False # Build Fuzzers using docker run. command = [ '--cap-add', 'SYS_PTRACE', '-e', 'FUZZING_ENGINE=libfuzzer', '-e', 'SANITIZER=address', '-e', 'ARCHITECTURE=x86_64' ] container = utils.get_container_name() if container: command += ['-e', 'OUT=' + out_dir, '--volumes-from', container] bash_command = 'rm -rf {0} && cp -r {1} {2} && compile'.format( os.path.join(src_in_docker, oss_fuzz_repo_name, '*'), os.path.join(git_workspace, oss_fuzz_repo_name), src_in_docker) else: command += [ '-e', 'OUT=' + '/out', '-v', '%s:%s' % (os.path.join(git_workspace, oss_fuzz_repo_name), os.path.join(src_in_docker, oss_fuzz_repo_name)), '-v', '%s:%s' % (out_dir, '/out') ] bash_command = 'compile' command.extend([ 'gcr.io/oss-fuzz/' + project_name, '/bin/bash', '-c', ]) command.append(bash_command) if helper.docker_run(command): logging.error('Building fuzzers failed.') return False return True def run_fuzzers(fuzz_seconds, workspace, project_name): """Runs all fuzzers for a specific OSS-Fuzz project. Args: fuzz_seconds: The total time allotted for fuzzing. workspace: The location in a shared volume to store a git repo and build artifacts. project_name: The name of the relevant OSS-Fuzz project. Returns: (True if run was successful, True if bug was found). """ # Validate inputs. if not os.path.exists(workspace): logging.error('Invalid workspace: %s.', workspace) return False, False out_dir = os.path.join(workspace, 'out') artifacts_dir = os.path.join(out_dir, 'artifacts') os.makedirs(artifacts_dir, exist_ok=True) if not fuzz_seconds or fuzz_seconds < 1: logging.error('Fuzz_seconds argument must be greater than 1, but was: %s.', format(fuzz_seconds)) return False, False # Get fuzzer information. fuzzer_paths = utils.get_fuzz_targets(out_dir) if not fuzzer_paths: logging.error('No fuzzers were found in out directory: %s.', format(out_dir)) return False, False fuzz_seconds_per_target = fuzz_seconds // len(fuzzer_paths) # Run fuzzers for alotted time. for fuzzer_path in fuzzer_paths: target = fuzz_target.FuzzTarget(fuzzer_path, fuzz_seconds_per_target, out_dir, project_name) test_case, stack_trace = target.fuzz() if not test_case or not stack_trace: logging.info('Fuzzer %s, finished running.', target.target_name) else: logging.info('Fuzzer %s, detected error: %s.', target.target_name, stack_trace) shutil.move(test_case, os.path.join(artifacts_dir, 'test_case')) parse_fuzzer_output(stack_trace, artifacts_dir) return True, True return True, False def parse_fuzzer_output(fuzzer_output, out_dir): """Parses the fuzzer output from a fuzz target binary. Args: fuzzer_output: A fuzz target binary output string to be parsed. out_dir: The location to store the parsed output files. """ # Get index of key file points. for marker in STACKTRACE_TOOL_MARKERS: marker_index = fuzzer_output.find(marker) if marker_index: begin_summary = marker_index break end_summary = -1 for marker in STACKTRACE_END_MARKERS: marker_index = fuzzer_output.find(marker) if marker_index: end_summary = marker_index + len(marker) break if begin_summary is None or end_summary is None: return summary_str = fuzzer_output[begin_summary:end_summary] if not summary_str: return # Write sections of fuzzer output to specific files. summary_file_path = os.path.join(out_dir, 'bug_summary.txt') with open(summary_file_path, 'a') as summary_handle: summary_handle.write(summary_str)