Skip to content

Commit 91f90ff

Browse files
authored
Add OSS-Fuzz on Demand build steps to upload testcases (#13296)
Add build steps to generate an access token and use it to upload a testcase if OSS-Fuzz on Demand found crashes. - Add `ood_upload_testcase.py` script to which upload a testcase on ClusterFuzz External upload testcase endpoint. - Add a build step to generate the access token using `gcloud auth print-access-token`. - Add a build step to run `ood_upload_testcase.py` in Google Cloud Build. Related to b/401215144 .
1 parent e8528a4 commit 91f90ff

File tree

3 files changed

+215
-15
lines changed

3 files changed

+215
-15
lines changed

infra/build/functions/fuzzbench.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
FUZZBENCH_PATH = '/fuzzbench'
3535
GCB_WORKSPACE_DIR = '/workspace'
3636
OOD_OUTPUT_CORPUS_DIR = f'{GCB_WORKSPACE_DIR}/ood_output_corpus'
37+
OOD_CRASHES_DIR = f'{GCB_WORKSPACE_DIR}/crashes'
3738

3839

3940
def get_engine_project_image_name(fuzzing_engine, project):
@@ -129,16 +130,14 @@ def get_build_fuzzers_steps(fuzzing_engine, project, env):
129130
project), '--file', engine_dockerfile_path,
130131
os.path.join(FUZZBENCH_PATH, 'fuzzers')
131132
]
132-
engine_step = [
133-
{
134-
'name': 'gcr.io/cloud-builders/docker',
135-
'args': build_args,
136-
'volumes': [{
137-
'name': 'fuzzbench_path',
138-
'path': FUZZBENCH_PATH,
139-
}],
140-
},
141-
]
133+
engine_step = {
134+
'name': 'gcr.io/cloud-builders/docker',
135+
'args': build_args,
136+
'volumes': [{
137+
'name': 'fuzzbench_path',
138+
'path': FUZZBENCH_PATH,
139+
}],
140+
}
142141
steps.append(engine_step)
143142

144143
compile_project_step = {
@@ -240,7 +239,7 @@ def get_build_ood_image_steps(fuzzing_engine, project, env_dict):
240239
'build', '--tag', ood_image, '--file', fuzzer_runtime_dockerfile_path,
241240
os.path.join(GCB_WORKSPACE_DIR + FUZZBENCH_PATH, 'fuzzers')
242241
]
243-
},
242+
}
244243
steps.append(build_runtime_step)
245244

246245
oss_fuzz_on_demand_dockerfile_path = f'{GCB_WORKSPACE_DIR}/oss-fuzz/infra/build/functions/ood.Dockerfile'
@@ -310,23 +309,54 @@ def get_extract_crashes_steps(fuzzing_engine, project, env_dict):
310309
}
311310
steps.append(download_libfuzzer_build_step)
312311

313-
crashes_dir = f'{GCB_WORKSPACE_DIR}/crashes/'
314312
extract_crashes_step = {
315313
'name':
316314
get_engine_project_image_name(fuzzing_engine, project),
317315
'args': [
318316
'bash', '-c', f'unzip {libfuzzer_build_dir}{build_filename} '
319-
f'-d {libfuzzer_build_dir} && mkdir -p {crashes_dir} && '
317+
f'-d {libfuzzer_build_dir} && mkdir -p {OOD_CRASHES_DIR} && '
320318
f'{libfuzzer_build_dir}{env_dict["FUZZ_TARGET"]} {OOD_OUTPUT_CORPUS_DIR} '
321-
f'-runs=0 -artifact_prefix={crashes_dir}; '
322-
f'echo "\nCrashes found by OOD:" && ls {crashes_dir} '
319+
f'-runs=0 -artifact_prefix={OOD_CRASHES_DIR}/; '
320+
f'echo "\nCrashes found by OOD:" && ls {OOD_CRASHES_DIR} '
323321
],
324322
}
325323
steps.append(extract_crashes_step)
326324

327325
return steps
328326

329327

328+
def get_upload_testcase_steps(project, env_dict):
329+
"""Returns the build steps to upload a testcase in the ClusterFuzz External
330+
upload testcase endpoint."""
331+
steps = []
332+
333+
access_token_file_path = f'{GCB_WORKSPACE_DIR}/at.txt'
334+
get_access_token_step = {
335+
'name':
336+
'google/cloud-sdk',
337+
'args': [
338+
'bash', '-c',
339+
f'gcloud auth print-access-token > {access_token_file_path}'
340+
]
341+
}
342+
steps.append(get_access_token_step)
343+
344+
upload_testcase_script_path = f'{GCB_WORKSPACE_DIR}/oss-fuzz/infra/build/functions/ood_upload_testcase.py'
345+
job_name = f'libfuzzer_asan_{project.name}'
346+
target_name = f'{project.name}_{env_dict["FUZZ_TARGET"]}'
347+
upload_testcase_step = {
348+
'name':
349+
'python:3.8',
350+
'args': [
351+
'python3', upload_testcase_script_path, OOD_CRASHES_DIR, job_name,
352+
target_name, access_token_file_path
353+
]
354+
}
355+
steps.append(upload_testcase_step)
356+
357+
return steps
358+
359+
330360
def get_build_steps( # pylint: disable=too-many-locals, too-many-arguments
331361
project_name, project_yaml, dockerfile_lines, config):
332362
"""Returns build steps for project."""
@@ -349,6 +379,7 @@ def get_build_steps( # pylint: disable=too-many-locals, too-many-arguments
349379
steps += get_push_and_run_ood_image_steps(config.fuzzing_engine, project,
350380
env_dict)
351381
steps += get_extract_crashes_steps(config.fuzzing_engine, project, env_dict)
382+
steps += get_upload_testcase_steps(project, env_dict)
352383

353384
return steps
354385

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright 2025 Google LLC
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
################################################################################
18+
"""Upload OSS-Fuzz on Demand testcases."""
19+
20+
import json
21+
import logging
22+
import os
23+
import sys
24+
import subprocess
25+
26+
try:
27+
import requests
28+
except ImportError:
29+
print("requests library not found. Installing...")
30+
subprocess.check_call([sys.executable, "-m", "pip", "install", "requests"])
31+
import requests
32+
33+
POST_URL = 'https://oss-fuzz.com/upload-testcase/upload-oauth'
34+
35+
36+
def get_access_token(access_token_path):
37+
"""Returns the ACCESS_TOKEN for upload testcase requests"""
38+
with open(access_token_path, 'r') as f:
39+
line = f.readline()
40+
return line.strip()
41+
42+
43+
def get_headers(access_token_path):
44+
"""Returns the headers required to upload testcase requests"""
45+
access_token = get_access_token(access_token_path)
46+
return {
47+
'Authorization': 'Bearer ' + access_token,
48+
}
49+
50+
51+
def upload_testcase(upload_url, testcase_path, job, target, access_token_path):
52+
"""Make an upload testcase request."""
53+
files = {
54+
'file': open(testcase_path, 'rb'),
55+
}
56+
data = {
57+
'job': job,
58+
'target': target,
59+
}
60+
try:
61+
resp = requests.post(upload_url,
62+
files=files,
63+
data=data,
64+
headers=get_headers(access_token_path))
65+
resp.raise_for_status()
66+
result = json.loads(resp.text)
67+
print('Upload succeeded. Testcase ID is', result['id'])
68+
except:
69+
print('Failed to upload with status', resp.status_code)
70+
print(resp.text)
71+
72+
73+
def get_file_path(dir_path):
74+
"""Returns the path of a file inside 'dir_path'. Returns None if there are no
75+
files inside the the given directory."""
76+
files = []
77+
for entry in os.scandir(dir_path):
78+
if entry.is_file():
79+
return f'{dir_path}/{entry.name}'
80+
return None
81+
82+
83+
def main():
84+
"""Upload an OSS-Fuzz on Demand testcase."""
85+
testcase_dir_path = sys.argv[1]
86+
job = sys.argv[2]
87+
target = sys.argv[3]
88+
access_token_path = sys.argv[4]
89+
testcase_path = get_file_path(testcase_dir_path)
90+
91+
if not testcase_path:
92+
print('OSS-Fuzz on Demand did not find any crashes.')
93+
else:
94+
upload_testcase(POST_URL, testcase_path, job, target, access_token_path)
95+
96+
return 0
97+
98+
99+
if __name__ == '__main__':
100+
main()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Copyright 2025 Google LLC
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+
"""Tests for ood_upload_testcase_test.py."""
17+
18+
import os
19+
import shutil
20+
import tempfile
21+
import unittest
22+
from unittest import mock
23+
24+
import ood_upload_testcase
25+
26+
27+
class GetFilePath(unittest.TestCase):
28+
"""Tests for get_file_path."""
29+
30+
def setUp(self):
31+
"""Set tem_dir attribute"""
32+
self.temp_dir = tempfile.mkdtemp()
33+
34+
def tearDown(self):
35+
"""Remove temp_dir after tests"""
36+
shutil.rmtree(self.temp_dir)
37+
38+
def test_no_files(self):
39+
"""Test for empty directory"""
40+
self.assertIsNone(ood_upload_testcase.get_file_path(self.temp_dir))
41+
42+
def test_single_file(self):
43+
"""Test for single file"""
44+
file_name = 'test_file.txt'
45+
file_path = os.path.join(self.temp_dir, file_name)
46+
open(file_path, 'w').close()
47+
self.assertEqual(ood_upload_testcase.get_file_path(self.temp_dir),
48+
file_path)
49+
50+
def test_multiple_files(self):
51+
"""Test for multiple files"""
52+
file_names = ['file1.txt', 'file2.csv', 'data.json']
53+
file_paths = []
54+
for name in file_names:
55+
file_path = os.path.join(self.temp_dir, name)
56+
file_paths.append(file_path)
57+
open(file_path, 'w').close()
58+
self.assertIn(ood_upload_testcase.get_file_path(self.temp_dir), file_paths)
59+
60+
def test_with_subdirectory(self):
61+
"""Test for directory with subdirectory"""
62+
os.makedirs(os.path.join(self.temp_dir, 'subdir'))
63+
self.assertIsNone(ood_upload_testcase.get_file_path(self.temp_dir))
64+
65+
file_name = 'test_file.txt'
66+
file_path = os.path.join(self.temp_dir, file_name)
67+
open(file_path, 'w').close()
68+
self.assertEqual(ood_upload_testcase.get_file_path(self.temp_dir),
69+
file_path)

0 commit comments

Comments
 (0)