1# Copyright 2020 Google Inc. 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# 15################################################################################ 16#!/usr/bin/env python3 17"""Starts and runs coverage build on Google Cloud Builder. 18 19Usage: build_and_run_coverage.py <project>. 20""" 21import json 22import logging 23import os 24import sys 25 26import build_lib 27import build_project 28 29SANITIZER = 'coverage' 30FUZZING_ENGINE = 'libfuzzer' 31ARCHITECTURE = 'x86_64' 32 33PLATFORM = 'linux' 34 35COVERAGE_BUILD_TYPE = 'coverage' 36 37# Where code coverage reports need to be uploaded to. 38COVERAGE_BUCKET_NAME = 'oss-fuzz-coverage' 39 40# This is needed for ClusterFuzz to pick up the most recent reports data. 41 42LATEST_REPORT_INFO_CONTENT_TYPE = 'application/json' 43 44# Languages from project.yaml that have code coverage support. 45LANGUAGES_WITH_COVERAGE_SUPPORT = ['c', 'c++', 'go', 'jvm', 'rust', 'swift'] 46 47 48class Bucket: # pylint: disable=too-few-public-methods 49 """Class representing the coverage GCS bucket.""" 50 51 def __init__(self, project, date, platform, testing): 52 self.coverage_bucket_name = 'oss-fuzz-coverage' 53 if testing: 54 self.coverage_bucket_name += '-testing' 55 56 self.date = date 57 self.project = project 58 self.html_report_url = ( 59 f'{build_lib.GCS_URL_BASENAME}{self.coverage_bucket_name}/{project}' 60 f'/reports/{date}/{platform}/index.html') 61 self.latest_report_info_url = (f'/{COVERAGE_BUCKET_NAME}' 62 f'/latest_report_info/{project}.json') 63 64 def get_upload_url(self, upload_type): 65 """Returns an upload url for |upload_type|.""" 66 return (f'gs://{self.coverage_bucket_name}/{self.project}' 67 f'/{upload_type}/{self.date}') 68 69 70def get_build_steps( # pylint: disable=too-many-locals, too-many-arguments 71 project_name, project_yaml_contents, dockerfile_lines, image_project, 72 base_images_project, config): 73 """Returns build steps for project.""" 74 project = build_project.Project(project_name, project_yaml_contents, 75 dockerfile_lines, image_project) 76 if project.disabled: 77 logging.info('Project "%s" is disabled.', project.name) 78 return [] 79 80 if project.fuzzing_language not in LANGUAGES_WITH_COVERAGE_SUPPORT: 81 logging.info( 82 'Project "%s" is written in "%s", coverage is not supported yet.', 83 project.name, project.fuzzing_language) 84 return [] 85 86 report_date = build_project.get_datetime_now().strftime('%Y%m%d') 87 bucket = Bucket(project.name, report_date, PLATFORM, config.testing) 88 89 build_steps = build_lib.project_image_steps( 90 project.name, 91 project.image, 92 project.fuzzing_language, 93 branch=config.branch, 94 test_image_suffix=config.test_image_suffix) 95 96 build = build_project.Build('libfuzzer', 'coverage', 'x86_64') 97 env = build_project.get_env(project.fuzzing_language, build) 98 build_steps.append( 99 build_project.get_compile_step(project, build, env, config.parallel)) 100 download_corpora_steps = build_lib.download_corpora_steps( 101 project.name, testing=config.testing) 102 if not download_corpora_steps: 103 logging.info('Skipping code coverage build for %s.', project.name) 104 return [] 105 106 build_steps.extend(download_corpora_steps) 107 108 failure_msg = ('*' * 80 + '\nCode coverage report generation failed.\n' 109 'To reproduce, run:\n' 110 f'python infra/helper.py build_image {project.name}\n' 111 'python infra/helper.py build_fuzzers --sanitizer coverage ' 112 f'{project.name}\n' 113 f'python infra/helper.py coverage {project.name}\n' + '*' * 80) 114 115 # Unpack the corpus and run coverage script. 116 coverage_env = env + [ 117 'HTTP_PORT=', 118 f'COVERAGE_EXTRA_ARGS={project.coverage_extra_args.strip()}', 119 ] 120 if 'dataflow' in project.fuzzing_engines: 121 coverage_env.append('FULL_SUMMARY_PER_TARGET=1') 122 123 build_steps.append({ 124 'name': 125 build_project.get_runner_image_name(base_images_project, 126 config.test_image_suffix), 127 'env': 128 coverage_env, 129 'args': [ 130 'bash', '-c', 131 ('for f in /corpus/*.zip; do unzip -q $f -d ${f%%.*} || (' 132 'echo "Failed to unpack the corpus for $(basename ${f%%.*}). ' 133 'This usually means that corpus backup for a particular fuzz ' 134 'target does not exist. If a fuzz target was added in the last ' 135 '24 hours, please wait one more day. Otherwise, something is ' 136 'wrong with the fuzz target or the infrastructure, and corpus ' 137 'pruning task does not finish successfully." && exit 1' 138 '); done && coverage || (echo "' + failure_msg + '" && false)') 139 ], 140 'volumes': [{ 141 'name': 'corpus', 142 'path': '/corpus' 143 }], 144 }) 145 146 # Upload the report. 147 upload_report_url = bucket.get_upload_url('reports') 148 149 # Delete the existing report as gsutil cannot overwrite it in a useful way due 150 # to the lack of `-T` option (it creates a subdir in the destination dir). 151 build_steps.append(build_lib.gsutil_rm_rf_step(upload_report_url)) 152 build_steps.append({ 153 'name': 154 'gcr.io/cloud-builders/gsutil', 155 'args': [ 156 '-m', 157 'cp', 158 '-r', 159 os.path.join(build.out, 'report'), 160 upload_report_url, 161 ], 162 }) 163 164 # Upload the fuzzer stats. Delete the old ones just in case. 165 upload_fuzzer_stats_url = bucket.get_upload_url('fuzzer_stats') 166 167 build_steps.append(build_lib.gsutil_rm_rf_step(upload_fuzzer_stats_url)) 168 build_steps.append({ 169 'name': 170 'gcr.io/cloud-builders/gsutil', 171 'args': [ 172 '-m', 173 'cp', 174 '-r', 175 os.path.join(build.out, 'fuzzer_stats'), 176 upload_fuzzer_stats_url, 177 ], 178 }) 179 180 # Upload the fuzzer logs. Delete the old ones just in case 181 upload_fuzzer_logs_url = bucket.get_upload_url('logs') 182 build_steps.append(build_lib.gsutil_rm_rf_step(upload_fuzzer_logs_url)) 183 build_steps.append({ 184 'name': 185 'gcr.io/cloud-builders/gsutil', 186 'args': [ 187 '-m', 188 'cp', 189 '-r', 190 os.path.join(build.out, 'logs'), 191 upload_fuzzer_logs_url, 192 ], 193 }) 194 195 # Upload srcmap. 196 srcmap_upload_url = bucket.get_upload_url('srcmap') 197 srcmap_upload_url = srcmap_upload_url.rstrip('/') + '.json' 198 build_steps.append({ 199 'name': 'gcr.io/cloud-builders/gsutil', 200 'args': [ 201 'cp', 202 '/workspace/srcmap.json', 203 srcmap_upload_url, 204 ], 205 }) 206 207 # Update the latest report information file for ClusterFuzz. 208 latest_report_info_url = build_lib.get_signed_url( 209 bucket.latest_report_info_url, 210 content_type=LATEST_REPORT_INFO_CONTENT_TYPE) 211 latest_report_info_body = json.dumps({ 212 'fuzzer_stats_dir': 213 upload_fuzzer_stats_url, 214 'html_report_url': 215 bucket.html_report_url, 216 'report_date': 217 report_date, 218 'report_summary_path': 219 os.path.join(upload_report_url, PLATFORM, 'summary.json'), 220 }) 221 222 build_steps.append( 223 build_lib.http_upload_step(latest_report_info_body, 224 latest_report_info_url, 225 LATEST_REPORT_INFO_CONTENT_TYPE)) 226 return build_steps 227 228 229def main(): 230 """Build and run coverage for projects.""" 231 return build_project.build_script_main( 232 'Generates coverage report for project.', get_build_steps, 233 COVERAGE_BUILD_TYPE) 234 235 236if __name__ == '__main__': 237 sys.exit(main()) 238