| # Copyright 2020 Google Inc. |
| # |
| # 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. |
| # |
| ################################################################################ |
| """Utility module for Google Cloud Build scripts.""" |
| import base64 |
| import collections |
| import os |
| import six.moves.urllib.parse as urlparse |
| import sys |
| import time |
| |
| import requests |
| |
| import google.auth |
| import googleapiclient.discovery |
| from oauth2client.service_account import ServiceAccountCredentials |
| |
| BUILD_TIMEOUT = 12 * 60 * 60 |
| |
| # Needed for reading public target.list.* files. |
| GCS_URL_BASENAME = 'https://storage.googleapis.com/' |
| |
| GCS_UPLOAD_URL_FORMAT = '/{0}/{1}/{2}' |
| |
| # Where corpus backups can be downloaded from. |
| CORPUS_BACKUP_URL = ('/{project}-backup.clusterfuzz-external.appspot.com/' |
| 'corpus/libFuzzer/{fuzzer}/latest.zip') |
| |
| # Cloud Builder has a limit of 100 build steps and 100 arguments for each step. |
| CORPUS_DOWNLOAD_BATCH_SIZE = 100 |
| |
| TARGETS_LIST_BASENAME = 'targets.list' |
| |
| EngineInfo = collections.namedtuple( |
| 'EngineInfo', |
| ['upload_bucket', 'supported_sanitizers', 'supported_architectures']) |
| |
| ENGINE_INFO = { |
| 'libfuzzer': |
| EngineInfo(upload_bucket='clusterfuzz-builds', |
| supported_sanitizers=['address', 'memory', 'undefined'], |
| supported_architectures=['x86_64', 'i386']), |
| 'afl': |
| EngineInfo(upload_bucket='clusterfuzz-builds-afl', |
| supported_sanitizers=['address'], |
| supported_architectures=['x86_64']), |
| 'honggfuzz': |
| EngineInfo(upload_bucket='clusterfuzz-builds-honggfuzz', |
| supported_sanitizers=['address'], |
| supported_architectures=['x86_64']), |
| 'dataflow': |
| EngineInfo(upload_bucket='clusterfuzz-builds-dataflow', |
| supported_sanitizers=['dataflow'], |
| supported_architectures=['x86_64']), |
| 'none': |
| EngineInfo(upload_bucket='clusterfuzz-builds-no-engine', |
| supported_sanitizers=['address'], |
| supported_architectures=['x86_64']), |
| } |
| |
| |
| def get_targets_list_filename(sanitizer): |
| """Returns target list filename.""" |
| return TARGETS_LIST_BASENAME + '.' + sanitizer |
| |
| |
| def get_targets_list_url(bucket, project, sanitizer): |
| """Returns target list url.""" |
| filename = get_targets_list_filename(sanitizer) |
| url = GCS_UPLOAD_URL_FORMAT.format(bucket, project, filename) |
| return url |
| |
| |
| def get_upload_bucket(engine, architecture, testing): |
| """Returns the upload bucket for |engine| and architecture. Returns the |
| testing bucket if |testing|.""" |
| bucket = ENGINE_INFO[engine].upload_bucket |
| if architecture != 'x86_64': |
| bucket += '-' + architecture |
| if testing: |
| bucket += '-testing' |
| return bucket |
| |
| |
| def _get_targets_list(project_name, testing): |
| """Returns target list.""" |
| # libFuzzer ASan 'x86_84' is the default configuration, get list of targets |
| # from it. |
| bucket = get_upload_bucket('libfuzzer', 'x86_64', testing) |
| url = get_targets_list_url(bucket, project_name, 'address') |
| |
| url = urlparse.urljoin(GCS_URL_BASENAME, url) |
| response = requests.get(url) |
| if not response.status_code == 200: |
| sys.stderr.write('Failed to get list of targets from "%s".\n' % url) |
| sys.stderr.write('Status code: %d \t\tText:\n%s\n' % |
| (response.status_code, response.text)) |
| return None |
| |
| return response.text.split() |
| |
| |
| # pylint: disable=no-member |
| def get_signed_url(path, method='PUT', content_type=''): |
| """Returns signed url.""" |
| timestamp = int(time.time() + BUILD_TIMEOUT) |
| blob = f'{method}\n\n{content_type}\n{timestamp}\n{path}' |
| |
| service_account_path = os.environ.get('GOOGLE_APPLICATION_CREDENTIALS') |
| if service_account_path: |
| creds = ServiceAccountCredentials.from_json_keyfile_name( |
| os.environ['GOOGLE_APPLICATION_CREDENTIALS']) |
| client_id = creds.service_account_email |
| signature = base64.b64encode(creds.sign_blob(blob)[1]) |
| else: |
| credentials, project = google.auth.default() |
| iam = googleapiclient.discovery.build('iamcredentials', |
| 'v1', |
| credentials=credentials, |
| cache_discovery=False) |
| client_id = project + '@appspot.gserviceaccount.com' |
| service_account = f'projects/-/serviceAccounts/{client_id}' |
| response = iam.projects().serviceAccounts().signBlob( |
| name=service_account, |
| body={ |
| 'delegates': [], |
| 'payload': base64.b64encode(blob.encode('utf-8')).decode('utf-8'), |
| }).execute() |
| signature = response['signedBlob'] |
| |
| values = { |
| 'GoogleAccessId': client_id, |
| 'Expires': timestamp, |
| 'Signature': signature, |
| } |
| return f'https://storage.googleapis.com{path}?{urlparse.urlencode(values)}' |
| |
| |
| def download_corpora_steps(project_name, testing): |
| """Returns GCB steps for downloading corpora backups for the given project. |
| """ |
| fuzz_targets = _get_targets_list(project_name, testing) |
| if not fuzz_targets: |
| sys.stderr.write('No fuzz targets found for project "%s".\n' % project_name) |
| return None |
| |
| steps = [] |
| # Split fuzz targets into batches of CORPUS_DOWNLOAD_BATCH_SIZE. |
| for i in range(0, len(fuzz_targets), CORPUS_DOWNLOAD_BATCH_SIZE): |
| download_corpus_args = [] |
| for binary_name in fuzz_targets[i:i + CORPUS_DOWNLOAD_BATCH_SIZE]: |
| qualified_name = binary_name |
| qualified_name_prefix = '%s_' % project_name |
| if not binary_name.startswith(qualified_name_prefix): |
| qualified_name = qualified_name_prefix + binary_name |
| |
| url = get_signed_url(CORPUS_BACKUP_URL.format(project=project_name, |
| fuzzer=qualified_name), |
| method='GET') |
| |
| corpus_archive_path = os.path.join('/corpus', binary_name + '.zip') |
| download_corpus_args.append('%s %s' % (corpus_archive_path, url)) |
| |
| steps.append({ |
| 'name': 'gcr.io/oss-fuzz-base/base-runner', |
| 'entrypoint': 'download_corpus', |
| 'args': download_corpus_args, |
| 'volumes': [{ |
| 'name': 'corpus', |
| 'path': '/corpus' |
| }], |
| }) |
| |
| return steps |
| |
| |
| def http_upload_step(data, signed_url, content_type): |
| """Returns a GCB step to upload data to the given URL via GCS HTTP API.""" |
| step = { |
| 'name': |
| 'gcr.io/cloud-builders/curl', |
| 'args': [ |
| '-H', |
| 'Content-Type: ' + content_type, |
| '-X', |
| 'PUT', |
| '-d', |
| data, |
| signed_url, |
| ], |
| } |
| return step |
| |
| |
| def gsutil_rm_rf_step(url): |
| """Returns a GCB step to recursively delete the object with given GCS url.""" |
| step = { |
| 'name': 'gcr.io/cloud-builders/gsutil', |
| 'entrypoint': 'sh', |
| 'args': [ |
| '-c', |
| 'gsutil -m rm -rf %s || exit 0' % url, |
| ], |
| } |
| return step |
| |
| |
| def get_pull_test_images_steps(test_image_suffix): |
| """Returns steps to pull testing versions of base-images and tag them so that |
| they are used in builds.""" |
| images = [ |
| 'gcr.io/oss-fuzz-base/base-builder', |
| 'gcr.io/oss-fuzz-base/base-builder-swift', |
| 'gcr.io/oss-fuzz-base/base-builder-jvm', |
| 'gcr.io/oss-fuzz-base/base-builder-go', |
| 'gcr.io/oss-fuzz-base/base-builder-python', |
| 'gcr.io/oss-fuzz-base/base-builder-rust', |
| ] |
| steps = [] |
| for image in images: |
| test_image = image + '-' + test_image_suffix |
| steps.append({ |
| 'name': 'gcr.io/cloud-builders/docker', |
| 'args': [ |
| 'pull', |
| test_image, |
| ], |
| 'waitFor': '-' # Start this immediately, don't wait for previous step. |
| }) |
| |
| # This step is hacky but gives us great flexibility. OSS-Fuzz has hardcoded |
| # references to gcr.io/oss-fuzz-base/base-builder (in dockerfiles, for |
| # example) and gcr.io/oss-fuzz-base-runner (in this build code). But the |
| # testing versions of those images are called e.g. |
| # gcr.io/oss-fuzz-base/base-builder-testing and |
| # gcr.io/oss-fuzz-base/base-runner-testing. How can we get the build to use |
| # the testing images instead of the real ones? By doing this step: tagging |
| # the test image with the non-test version, so that the test version is used |
| # instead of pulling the real one. |
| steps.append({ |
| 'name': 'gcr.io/cloud-builders/docker', |
| 'args': ['tag', test_image, image], |
| }) |
| return steps |
| |
| |
| def get_srcmap_step_id(): |
| """Returns the id for the srcmap step.""" |
| return 'srcmap' |
| |
| |
| def project_image_steps(name, |
| image, |
| language, |
| branch=None, |
| test_image_suffix=None): |
| """Returns GCB steps to build OSS-Fuzz project image.""" |
| clone_step = { |
| 'args': [ |
| 'clone', 'https://github.com/google/oss-fuzz.git', '--depth', '1' |
| ], |
| 'name': 'gcr.io/cloud-builders/git', |
| } |
| if branch: |
| # Do this to support testing other branches. |
| clone_step['args'].extend(['--branch', branch]) |
| |
| steps = [clone_step] |
| if test_image_suffix: |
| steps.extend(get_pull_test_images_steps(test_image_suffix)) |
| |
| srcmap_step_id = get_srcmap_step_id() |
| steps += [{ |
| 'name': 'gcr.io/cloud-builders/docker', |
| 'args': [ |
| 'build', |
| '-t', |
| image, |
| '.', |
| ], |
| 'dir': 'oss-fuzz/projects/' + name, |
| }, { |
| 'name': image, |
| 'args': [ |
| 'bash', '-c', |
| 'srcmap > /workspace/srcmap.json && cat /workspace/srcmap.json' |
| ], |
| 'env': [ |
| 'OSSFUZZ_REVISION=$REVISION_ID', |
| 'FUZZING_LANGUAGE=%s' % language, |
| ], |
| 'id': srcmap_step_id |
| }] |
| |
| return steps |