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/python2 17"""Starts and runs coverage build on Google Cloud Builder. 18Usage: build_and_run_coverage.py <project_dir> 19""" 20import datetime 21import json 22import logging 23import os 24import sys 25 26import build_lib 27import build_project 28 29SANITIZER = 'coverage' 30CONFIGURATION = ['FUZZING_ENGINE=libfuzzer', 'SANITIZER=%s' % SANITIZER] 31PLATFORM = 'linux' 32 33COVERAGE_BUILD_TAG = 'coverage' 34 35# Where code coverage reports need to be uploaded to. 36COVERAGE_BUCKET_NAME = 'oss-fuzz-coverage' 37 38# Link to the code coverage report in HTML format. 39HTML_REPORT_URL_FORMAT = (build_lib.GCS_URL_BASENAME + COVERAGE_BUCKET_NAME + 40 '/{project}/reports/{date}/{platform}/index.html') 41 42# This is needed for ClusterFuzz to pick up the most recent reports data. 43LATEST_REPORT_INFO_URL = ('/' + COVERAGE_BUCKET_NAME + 44 '/latest_report_info/{project}.json') 45LATEST_REPORT_INFO_CONTENT_TYPE = 'application/json' 46 47# Link where to upload code coverage report files to. 48UPLOAD_URL_FORMAT = 'gs://' + COVERAGE_BUCKET_NAME + '/{project}/{type}/{date}' 49 50# Languages from project.yaml that have code coverage support. 51LANGUAGES_WITH_COVERAGE_SUPPORT = ['c', 'c++', 'go', 'rust'] 52 53 54def usage(): 55 """Exit with code 1 and display syntax to use this file.""" 56 sys.stderr.write("Usage: " + sys.argv[0] + " <project_dir>\n") 57 sys.exit(1) 58 59 60# pylint: disable=too-many-locals 61def get_build_steps(project_name, project_yaml_file, dockerfile_lines, 62 image_project, base_images_project): 63 """Returns build steps for project.""" 64 project_yaml = build_project.load_project_yaml(project_name, 65 project_yaml_file, 66 image_project) 67 if project_yaml['disabled']: 68 logging.info('Project "%s" is disabled.', project_name) 69 return [] 70 71 if project_yaml['language'] not in LANGUAGES_WITH_COVERAGE_SUPPORT: 72 logging.info( 73 'Project "%s" is written in "%s", coverage is not supported yet.', 74 project_name, project_yaml['language']) 75 return [] 76 77 name = project_yaml['name'] 78 image = project_yaml['image'] 79 language = project_yaml['language'] 80 report_date = datetime.datetime.now().strftime('%Y%m%d') 81 82 build_steps = build_lib.project_image_steps(name, image, language) 83 84 env = CONFIGURATION[:] 85 out = '/workspace/out/' + SANITIZER 86 env.append('OUT=' + out) 87 env.append('FUZZING_LANGUAGE=' + language) 88 89 workdir = build_project.workdir_from_dockerfile(dockerfile_lines) 90 if not workdir: 91 workdir = '/src' 92 93 failure_msg = ('*' * 80 + '\nCoverage build failed.\nTo reproduce, run:\n' 94 'python infra/helper.py build_image {name}\n' 95 'python infra/helper.py build_fuzzers --sanitizer coverage ' 96 '{name}\n' + '*' * 80).format(name=name) 97 98 # Compilation step. 99 build_steps.append({ 100 'name': 101 image, 102 'env': 103 env, 104 'args': [ 105 'bash', 106 '-c', 107 # Remove /out to make sure there are non instrumented binaries. 108 # `cd /src && cd {workdir}` (where {workdir} is parsed from the 109 # Dockerfile). Container Builder overrides our workdir so we need 110 # to add this step to set it back. 111 ('rm -r /out && cd /src && cd {workdir} && mkdir -p {out} && ' 112 'compile || (echo "{failure_msg}" && false)' 113 ).format(workdir=workdir, out=out, failure_msg=failure_msg), 114 ], 115 }) 116 117 download_corpora_steps = build_lib.download_corpora_steps(project_name) 118 if not download_corpora_steps: 119 logging.info('Skipping code coverage build for %s.', project_name) 120 return [] 121 122 build_steps.extend(download_corpora_steps) 123 124 failure_msg = ('*' * 80 + '\nCode coverage report generation failed.\n' 125 'To reproduce, run:\n' 126 'python infra/helper.py build_image {name}\n' 127 'python infra/helper.py build_fuzzers --sanitizer coverage ' 128 '{name}\n' 129 'python infra/helper.py coverage {name}\n' + 130 '*' * 80).format(name=name) 131 132 # Unpack the corpus and run coverage script. 133 coverage_env = env + [ 134 'HTTP_PORT=', 135 'COVERAGE_EXTRA_ARGS=%s' % project_yaml['coverage_extra_args'].strip(), 136 ] 137 if 'dataflow' in project_yaml['fuzzing_engines']: 138 coverage_env.append('FULL_SUMMARY_PER_TARGET=1') 139 140 build_steps.append({ 141 'name': 'gcr.io/{0}/base-runner'.format(base_images_project), 142 'env': coverage_env, 143 'args': [ 144 'bash', '-c', 145 ('for f in /corpus/*.zip; do unzip -q $f -d ${f%%.*} || (' 146 'echo "Failed to unpack the corpus for $(basename ${f%%.*}). ' 147 'This usually means that corpus backup for a particular fuzz ' 148 'target does not exist. If a fuzz target was added in the last ' 149 '24 hours, please wait one more day. Otherwise, something is ' 150 'wrong with the fuzz target or the infrastructure, and corpus ' 151 'pruning task does not finish successfully." && exit 1' 152 '); done && coverage || (echo "' + failure_msg + '" && false)') 153 ], 154 'volumes': [{ 155 'name': 'corpus', 156 'path': '/corpus' 157 }], 158 }) 159 160 # Upload the report. 161 upload_report_url = UPLOAD_URL_FORMAT.format(project=project_name, 162 type='reports', 163 date=report_date) 164 165 # Delete the existing report as gsutil cannot overwrite it in a useful way due 166 # to the lack of `-T` option (it creates a subdir in the destination dir). 167 build_steps.append(build_lib.gsutil_rm_rf_step(upload_report_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(out, 'report'), 176 upload_report_url, 177 ], 178 }) 179 180 # Upload the fuzzer stats. Delete the old ones just in case. 181 upload_fuzzer_stats_url = UPLOAD_URL_FORMAT.format(project=project_name, 182 type='fuzzer_stats', 183 date=report_date) 184 build_steps.append(build_lib.gsutil_rm_rf_step(upload_fuzzer_stats_url)) 185 build_steps.append({ 186 'name': 187 'gcr.io/cloud-builders/gsutil', 188 'args': [ 189 '-m', 190 'cp', 191 '-r', 192 os.path.join(out, 'fuzzer_stats'), 193 upload_fuzzer_stats_url, 194 ], 195 }) 196 197 # Upload the fuzzer logs. Delete the old ones just in case 198 upload_fuzzer_logs_url = UPLOAD_URL_FORMAT.format(project=project_name, 199 type='logs', 200 date=report_date) 201 build_steps.append(build_lib.gsutil_rm_rf_step(upload_fuzzer_logs_url)) 202 build_steps.append({ 203 'name': 204 'gcr.io/cloud-builders/gsutil', 205 'args': [ 206 '-m', 207 'cp', 208 '-r', 209 os.path.join(out, 'logs'), 210 upload_fuzzer_logs_url, 211 ], 212 }) 213 214 # Upload srcmap. 215 srcmap_upload_url = UPLOAD_URL_FORMAT.format(project=project_name, 216 type='srcmap', 217 date=report_date) 218 srcmap_upload_url = srcmap_upload_url.rstrip('/') + '.json' 219 build_steps.append({ 220 'name': 'gcr.io/cloud-builders/gsutil', 221 'args': [ 222 'cp', 223 '/workspace/srcmap.json', 224 srcmap_upload_url, 225 ], 226 }) 227 228 # Update the latest report information file for ClusterFuzz. 229 latest_report_info_url = build_lib.get_signed_url( 230 LATEST_REPORT_INFO_URL.format(project=project_name), 231 content_type=LATEST_REPORT_INFO_CONTENT_TYPE) 232 latest_report_info_body = json.dumps({ 233 'fuzzer_stats_dir': 234 upload_fuzzer_stats_url, 235 'html_report_url': 236 HTML_REPORT_URL_FORMAT.format(project=project_name, 237 date=report_date, 238 platform=PLATFORM), 239 'report_date': 240 report_date, 241 'report_summary_path': 242 os.path.join(upload_report_url, PLATFORM, 'summary.json'), 243 }) 244 245 build_steps.append( 246 build_lib.http_upload_step(latest_report_info_body, 247 latest_report_info_url, 248 LATEST_REPORT_INFO_CONTENT_TYPE)) 249 return build_steps 250 251 252def main(): 253 """Build and run coverage for projects.""" 254 if len(sys.argv) != 2: 255 usage() 256 257 image_project = 'oss-fuzz' 258 base_images_project = 'oss-fuzz-base' 259 project_dir = sys.argv[1].rstrip(os.path.sep) 260 project_name = os.path.basename(project_dir) 261 dockerfile_path = os.path.join(project_dir, 'Dockerfile') 262 project_yaml_path = os.path.join(project_dir, 'project.yaml') 263 264 with open(dockerfile_path) as docker_file: 265 dockerfile_lines = docker_file.readlines() 266 267 with open(project_yaml_path) as project_yaml_file: 268 steps = get_build_steps(project_name, project_yaml_file, dockerfile_lines, 269 image_project, base_images_project) 270 271 build_project.run_build(steps, project_name, COVERAGE_BUILD_TAG) 272 273 274if __name__ == "__main__": 275 main() 276