• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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