Skip to content

Commit 1679c4b

Browse files
committed
Testing CI setup
1 parent 7ae8bd8 commit 1679c4b

File tree

3 files changed

+273
-0
lines changed

3 files changed

+273
-0
lines changed

.gitlab-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ variables:
77

88
include:
99
- local: .gitlab/benchmarks.yml
10+
- local: .gitlab/fuzz.yml
1011

1112
trigger_internal_build:
1213
variables:

.gitlab/fuzz.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Fuzzing job configuration
2+
# This job discovers, builds, and uploads all cargo-fuzz targets to the internal fuzzing infrastructure
3+
# See ci/README_FUZZING.md for more information
4+
5+
variables:
6+
BASE_CI_IMAGE: registry.ddbuild.io/ci/benchmarking-platform:libdatadog-benchmarks
7+
8+
fuzz:
9+
tags: ["arch:amd64"]
10+
needs: []
11+
image:
12+
name: $BASE_CI_IMAGE
13+
rules:
14+
# runs on gitlab schedule and on merge to main.
15+
# Also allow manual run in branches for ease of debug / testing
16+
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "schedule"'
17+
allow_failure: true
18+
- if: $CI_COMMIT_BRANCH == "main"
19+
allow_failure: true
20+
- when: manual
21+
allow_failure: true
22+
timeout: 1h
23+
script:
24+
- VAULT_VERSION=1.15.4 && curl -fsSL "https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip" -o vault.zip && unzip vault.zip && mv vault /usr/local/bin/vault && rm vault.zip && chmod +x /usr/local/bin/vault
25+
- rustup default nightly
26+
- cargo install cargo-fuzz
27+
- pip3 install requests toml
28+
- python3 fuzz/fuzz_infra.py
29+
allow_failure: true
30+
variables:
31+
KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: libdatadog

fuzz/fuzz_infra.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
Script for running fuzz targets in the internal fuzzing infrastructure.
5+
This is called from .gitlab/fuzz.yml.
6+
7+
If you want to run this locally, please set the VAULT_FUZZING_TOKEN environment variable
8+
(i.e: ddtool auth token security-fuzzing-platform --datacenter=us1.ddbuild.io)
9+
10+
In CI, this is expected to run with the base image defined in ./ci/Dockerfiles/Dockerfile.fuzz.
11+
12+
"""
13+
14+
import os
15+
from subprocess import Popen, PIPE
16+
import requests
17+
import toml
18+
19+
DEFAULT_FUZZING_SLACK_CHANNEL = "fuzzing-ops" # TODO: change me once we validated everything is not spamming and set up correctly.
20+
# Lets reuse the token for all requests to avoid issues.
21+
# The process should be short lived enough that the token should be valid for the duration.
22+
_cached_token = None
23+
24+
25+
def get_auth_header():
26+
global _cached_token
27+
if os.getenv("VAULT_FUZZING_TOKEN") is not None:
28+
return os.getenv("VAULT_FUZZING_TOKEN")
29+
30+
if _cached_token is None:
31+
_cached_token = (
32+
os.popen(
33+
"vault read -field=token identity/oidc/token/security-fuzzing-platform"
34+
)
35+
.read()
36+
.strip()
37+
)
38+
return _cached_token
39+
40+
41+
def get_commit_sha():
42+
return os.getenv("CI_COMMIT_SHA")
43+
44+
45+
def upload_fuzz(
46+
directory,
47+
git_sha,
48+
fuzz_test,
49+
team="apm-sdk-rust",
50+
core_count=2,
51+
duration=3600,
52+
proc_count=2,
53+
fuzz_memory=4,
54+
):
55+
"""
56+
This builds and uploads fuzz targets to the internal fuzzing infrastructure.
57+
It needs to be passed the -fuzz flag in order to build the fuzz with efficient coverage guidance.
58+
"""
59+
60+
api_url = "https://fuzzing-api.us1.ddbuild.io/api/v1"
61+
62+
# Get the auth token a single time and reuse it for all requests
63+
auth_header = get_auth_header()
64+
if not auth_header:
65+
print("❌ Failed to get auth header")
66+
exit(1)
67+
68+
# We let the API handle package name length validation
69+
# It will be returned, truncated / reformated, if needed in the json response.
70+
# We simply force the prefix to be `libdatadog-` for ease of filtering (until we improve that part on the API side)
71+
# As a note: more than 63 characters will be truncated by the API
72+
pkgname_prefix = "libdatadog-"
73+
pkgname = (
74+
(pkgname_prefix + directory + "-" + fuzz_test)
75+
.replace("_", "-")
76+
.replace("/", "-")
77+
)
78+
pkgname = pkgname.strip("-.") # Remove trailing dashes and dots.
79+
print(f"pkgname: {pkgname}")
80+
81+
print(f"Getting presigned URL for {pkgname}...")
82+
headers = {"Authorization": f"Bearer {auth_header}"}
83+
presigned_response = requests.post(
84+
f"{api_url}/apps/{pkgname}/builds/{git_sha}/url", headers=headers, timeout=30
85+
)
86+
87+
if not presigned_response.ok:
88+
print(
89+
f"❌ Failed to get presigned URL (status {presigned_response.status_code})"
90+
)
91+
try:
92+
error_detail = presigned_response.json()
93+
print(f"Error details: {error_detail}")
94+
except Exception as e:
95+
print(f"Raw error response: {presigned_response.text} {e}")
96+
presigned_response.raise_for_status()
97+
presigned_url = presigned_response.json()["data"]["url"]
98+
99+
print(f"Uploading {pkgname} ({fuzz_test}) for {git_sha}...")
100+
# Upload file to presigned URL
101+
with open(
102+
f"{directory}/target/x86_64-unknown-linux-gnu/release/{fuzz_test}", "rb"
103+
) as f:
104+
upload_response = requests.put(presigned_url, data=f, timeout=300)
105+
106+
if not upload_response.ok:
107+
print(f"❌ Failed to upload file (status {upload_response.status_code})")
108+
try:
109+
error_detail = upload_response.json()
110+
print(f"Error details: {error_detail}")
111+
except Exception as e:
112+
print(f"Raw error response: {upload_response.text} {e}")
113+
upload_response.raise_for_status()
114+
115+
print(f"Starting fuzzer for {pkgname} ({fuzz_test})...")
116+
# Start new fuzzer
117+
run_payload = {
118+
"app": pkgname,
119+
"debug": False,
120+
"version": git_sha,
121+
"core_count": core_count,
122+
"duration": duration,
123+
"type": "cargo-fuzz",
124+
"binary": fuzz_test,
125+
"team": team,
126+
"process_count": proc_count,
127+
"memory": fuzz_memory,
128+
"repository_url": "https://github.com/DataDog/libdatadog",
129+
"slack_channel": DEFAULT_FUZZING_SLACK_CHANNEL,
130+
}
131+
132+
headers = {
133+
"Authorization": f"Bearer {auth_header}",
134+
"Content-Type": "application/json",
135+
}
136+
137+
try:
138+
response = requests.post(
139+
f"{api_url}/apps/{pkgname}/fuzzers",
140+
headers=headers,
141+
json=run_payload,
142+
timeout=30,
143+
)
144+
response.raise_for_status()
145+
except Exception as e:
146+
error_detail = response.json()
147+
print(f"❌ API request failed with status {response.status_code}")
148+
print(f"Error details: {error_detail}")
149+
print(f"Raw error response: {response.text} {e}")
150+
151+
print(f"✅ Started fuzzer for {pkgname} ({fuzz_test})...")
152+
response_json = response.json()
153+
print(response_json)
154+
155+
156+
def search_fuzz_tests(directory) -> list[str]:
157+
fuzz_list_cmd = ["cargo", "+nightly", "fuzz", "list"]
158+
process = Popen(fuzz_list_cmd, cwd=directory, stdout=PIPE, stderr=PIPE)
159+
stdout, stderr = process.communicate()
160+
161+
if process.returncode != 0:
162+
print(f"❌ Failed to list fuzz tests in {directory}")
163+
print(f"Command: {' '.join(fuzz_list_cmd)}")
164+
print(f"Exit code: {process.returncode}")
165+
if stderr:
166+
print(f"Error output: {stderr.decode('utf-8')}")
167+
if stdout:
168+
print(f"Standard output: {stdout.decode('utf-8')}")
169+
return []
170+
171+
return stdout.decode("utf-8").splitlines()
172+
173+
174+
def build_fuzz(directory, fuzz_test) -> bool:
175+
build_cmd = ["cargo", "+nightly", "fuzz", "build", fuzz_test]
176+
return Popen(build_cmd, cwd=directory).wait() == 0
177+
178+
179+
# We want to search for all crates in the repository.
180+
# We can't simply run `cargo fuzz list` in the root directory.
181+
def is_fuzz_crate(cargo_toml_path) -> bool:
182+
"""Check if a Cargo.toml file has cargo-fuzz = true in its metadata."""
183+
try:
184+
with open(cargo_toml_path, "r") as f:
185+
cargo_config = toml.load(f)
186+
return (
187+
cargo_config.get("package", {})
188+
.get("metadata", {})
189+
.get("cargo-fuzz", False)
190+
)
191+
except Exception as e:
192+
print(f"Warning: Could not parse {cargo_toml_path}: {e}")
193+
return False
194+
195+
196+
def find_cargo_roots(directory) -> list[str]:
197+
print(f"Finding cargo roots in {directory}")
198+
cargo_roots = []
199+
for root, dirs, files in os.walk(directory):
200+
# Skip target directories to avoid scanning build artifacts
201+
if "target" in dirs:
202+
dirs.remove("target")
203+
204+
if "Cargo.toml" in files:
205+
cargo_toml_path = os.path.join(root, "Cargo.toml")
206+
if is_fuzz_crate(cargo_toml_path):
207+
print(f"Found fuzz cargo root: {root}")
208+
cargo_roots.append(root)
209+
else:
210+
print(f"Skipping non-fuzz cargo root: {root}")
211+
return cargo_roots
212+
213+
214+
if __name__ == "__main__":
215+
cargo_roots = find_cargo_roots(os.getcwd())
216+
print(cargo_roots)
217+
git_sha = get_commit_sha()
218+
219+
for cargo_root in cargo_roots:
220+
fuzz_tests = search_fuzz_tests(cargo_root)
221+
print(f"Found {len(fuzz_tests)} fuzz tests in {cargo_root}")
222+
if len(fuzz_tests) == 0:
223+
print(f"No fuzz tests found in {cargo_root}, skipping...")
224+
continue
225+
226+
for fuzz_test in fuzz_tests:
227+
print(f"Building fuzz for {cargo_root}/{fuzz_test} ({git_sha})")
228+
err = build_fuzz(cargo_root, fuzz_test)
229+
if not err:
230+
print(
231+
f"❌ Failed to build fuzz for {cargo_root}/{fuzz_test} ({git_sha}). Skipping uploading."
232+
)
233+
continue
234+
235+
# Make cargo_root relative to the root of the repository, so the generated target name is libdatadog-<foldername>-<fuzz-test>
236+
# In the future, the api will support a custom path flag
237+
repo_root = os.path.abspath(os.getcwd())
238+
rel_cargo_root = os.path.relpath(cargo_root, repo_root)
239+
print(f"Uploading fuzz for {rel_cargo_root}/{fuzz_test} ({git_sha})")
240+
upload_fuzz(rel_cargo_root, git_sha, fuzz_test)
241+

0 commit comments

Comments
 (0)