Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
.gitignore @Datadog/libdatadog
.gitlab-ci.yml @Datadog/apm-common-components-core
.gitlab/benchmarks.yml @Datadog/apm-common-components-core
.gitlab/fuzz.yml @Datadog/chaos-platform
benchmark/ @Datadog/apm-common-components-core
bin_tests/ @Datadog/libdatadog-profiling
build-common/ @Datadog/apm-common-components-core
Expand Down Expand Up @@ -64,6 +65,7 @@ tests/spawn_from_lib/ @Datadog/libdatadog-php @Datadog/libdatadog
tests/windows_package/ @Datadog/apm-common-components-core
tools/ @Datadog/apm-common-components-core
windows/ @Datadog/libdatadog-core
fuzz/ @Datadog/chaos-platform

# Specific overrides (must come after their general patterns above)
bin_tests/tests/test_the_tests.rs @Datadog/libdatadog-core
Expand Down
1 change: 1 addition & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ variables:

include:
- local: .gitlab/benchmarks.yml
- local: .gitlab/fuzz.yml

trigger_internal_build:
variables:
Expand Down
31 changes: 31 additions & 0 deletions .gitlab/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Fuzzing job configuration
# This job discovers, builds, and uploads all cargo-fuzz targets to the internal fuzzing infrastructure
# See ci/README_FUZZING.md for more information

variables:
BASE_CI_IMAGE: registry.ddbuild.io/ci/benchmarking-platform:libdatadog-benchmarks

fuzz:
tags: ["arch:amd64"]
needs: []
image:
name: $BASE_CI_IMAGE
rules:
# runs on gitlab schedule and on merge to main.
# Also allow manual run in branches for ease of debug / testing
- if: '$CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "schedule"'
allow_failure: true
- if: $CI_COMMIT_BRANCH == "main"
allow_failure: true
- when: manual
allow_failure: true
timeout: 1h
script:
- 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
- rustup default nightly
- cargo install cargo-fuzz
- pip3 install requests toml
- python3 fuzz/fuzz_infra.py
allow_failure: true
variables:
KUBERNETES_SERVICE_ACCOUNT_OVERWRITE: libdatadog
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

241 changes: 241 additions & 0 deletions fuzz/fuzz_infra.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
#!/usr/bin/env python3

"""
Script for running fuzz targets in the internal fuzzing infrastructure.
This is called from .gitlab/fuzz.yml.

If you want to run this locally, please set the VAULT_FUZZING_TOKEN environment variable
(i.e: ddtool auth token security-fuzzing-platform --datacenter=us1.ddbuild.io)

In CI, this is expected to run with the base image defined in ./ci/Dockerfiles/Dockerfile.fuzz.

"""

import os
from subprocess import Popen, PIPE
import requests
import toml

DEFAULT_FUZZING_SLACK_CHANNEL = "fuzzing-ops" # TODO: change me once we validated everything is not spamming and set up correctly.
# Lets reuse the token for all requests to avoid issues.
# The process should be short lived enough that the token should be valid for the duration.
_cached_token = None


def get_auth_header():
global _cached_token
if os.getenv("VAULT_FUZZING_TOKEN") is not None:
return os.getenv("VAULT_FUZZING_TOKEN")

if _cached_token is None:
_cached_token = (
os.popen(
"vault read -field=token identity/oidc/token/security-fuzzing-platform"
)
.read()
.strip()
)
return _cached_token


def get_commit_sha():
return os.getenv("CI_COMMIT_SHA")


def upload_fuzz(
directory,
git_sha,
fuzz_test,
team="apm-sdk-rust",
core_count=2,
duration=3600,
proc_count=2,
fuzz_memory=4,
):
"""
This builds and uploads fuzz targets to the internal fuzzing infrastructure.
It needs to be passed the -fuzz flag in order to build the fuzz with efficient coverage guidance.
"""

api_url = "https://fuzzing-api.us1.ddbuild.io/api/v1"

# Get the auth token a single time and reuse it for all requests
auth_header = get_auth_header()
if not auth_header:
print("❌ Failed to get auth header")
exit(1)

# We let the API handle package name length validation
# It will be returned, truncated / reformated, if needed in the json response.
# We simply force the prefix to be `libdatadog-` for ease of filtering (until we improve that part on the API side)
# As a note: more than 63 characters will be truncated by the API
pkgname_prefix = "libdatadog-"
pkgname = (
(pkgname_prefix + directory + "-" + fuzz_test)
.replace("_", "-")
.replace("/", "-")
)
pkgname = pkgname.strip("-.") # Remove trailing dashes and dots.
print(f"pkgname: {pkgname}")

print(f"Getting presigned URL for {pkgname}...")
headers = {"Authorization": f"Bearer {auth_header}"}
presigned_response = requests.post(
f"{api_url}/apps/{pkgname}/builds/{git_sha}/url", headers=headers, timeout=30
)

if not presigned_response.ok:
print(
f"❌ Failed to get presigned URL (status {presigned_response.status_code})"
)
try:
error_detail = presigned_response.json()
print(f"Error details: {error_detail}")
except Exception as e:
print(f"Raw error response: {presigned_response.text} {e}")
presigned_response.raise_for_status()
presigned_url = presigned_response.json()["data"]["url"]

print(f"Uploading {pkgname} ({fuzz_test}) for {git_sha}...")
# Upload file to presigned URL
with open(
f"{directory}/target/x86_64-unknown-linux-gnu/release/{fuzz_test}", "rb"
) as f:
upload_response = requests.put(presigned_url, data=f, timeout=300)

if not upload_response.ok:
print(f"❌ Failed to upload file (status {upload_response.status_code})")
try:
error_detail = upload_response.json()
print(f"Error details: {error_detail}")
except Exception as e:
print(f"Raw error response: {upload_response.text} {e}")
upload_response.raise_for_status()

print(f"Starting fuzzer for {pkgname} ({fuzz_test})...")
# Start new fuzzer
run_payload = {
"app": pkgname,
"debug": False,
"version": git_sha,
"core_count": core_count,
"duration": duration,
"type": "cargo-fuzz",
"binary": fuzz_test,
"team": team,
"process_count": proc_count,
"memory": fuzz_memory,
"repository_url": "https://github.com/DataDog/libdatadog",
"slack_channel": DEFAULT_FUZZING_SLACK_CHANNEL,
}

headers = {
"Authorization": f"Bearer {auth_header}",
"Content-Type": "application/json",
}

try:
response = requests.post(
f"{api_url}/apps/{pkgname}/fuzzers",
headers=headers,
json=run_payload,
timeout=30,
)
response.raise_for_status()
except Exception as e:
error_detail = response.json()
print(f"❌ API request failed with status {response.status_code}")
print(f"Error details: {error_detail}")
print(f"Raw error response: {response.text} {e}")

print(f"✅ Started fuzzer for {pkgname} ({fuzz_test})...")
response_json = response.json()
print(response_json)


def search_fuzz_tests(directory) -> list[str]:
fuzz_list_cmd = ["cargo", "+nightly", "fuzz", "list"]
process = Popen(fuzz_list_cmd, cwd=directory, stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate()

if process.returncode != 0:
print(f"❌ Failed to list fuzz tests in {directory}")
print(f"Command: {' '.join(fuzz_list_cmd)}")
print(f"Exit code: {process.returncode}")
if stderr:
print(f"Error output: {stderr.decode('utf-8')}")
if stdout:
print(f"Standard output: {stdout.decode('utf-8')}")
return []

return stdout.decode("utf-8").splitlines()


def build_fuzz(directory, fuzz_test) -> bool:
build_cmd = ["cargo", "+nightly", "fuzz", "build", fuzz_test]
return Popen(build_cmd, cwd=directory).wait() == 0


# We want to search for all crates in the repository.
# We can't simply run `cargo fuzz list` in the root directory.
def is_fuzz_crate(cargo_toml_path) -> bool:
"""Check if a Cargo.toml file has cargo-fuzz = true in its metadata."""
try:
with open(cargo_toml_path, "r") as f:
cargo_config = toml.load(f)
return (
cargo_config.get("package", {})
.get("metadata", {})
.get("cargo-fuzz", False)
)
except Exception as e:
print(f"Warning: Could not parse {cargo_toml_path}: {e}")
return False


def find_cargo_roots(directory) -> list[str]:
print(f"Finding cargo roots in {directory}")
cargo_roots = []
for root, dirs, files in os.walk(directory):
# Skip target directories to avoid scanning build artifacts
if "target" in dirs:
dirs.remove("target")

if "Cargo.toml" in files:
cargo_toml_path = os.path.join(root, "Cargo.toml")
if is_fuzz_crate(cargo_toml_path):
print(f"Found fuzz cargo root: {root}")
cargo_roots.append(root)
else:
print(f"Skipping non-fuzz cargo root: {root}")
return cargo_roots


if __name__ == "__main__":
cargo_roots = find_cargo_roots(os.getcwd())
print(cargo_roots)
git_sha = get_commit_sha()

for cargo_root in cargo_roots:
fuzz_tests = search_fuzz_tests(cargo_root)
print(f"Found {len(fuzz_tests)} fuzz tests in {cargo_root}")
if len(fuzz_tests) == 0:
print(f"No fuzz tests found in {cargo_root}, skipping...")
continue

for fuzz_test in fuzz_tests:
print(f"Building fuzz for {cargo_root}/{fuzz_test} ({git_sha})")
err = build_fuzz(cargo_root, fuzz_test)
if not err:
print(
f"❌ Failed to build fuzz for {cargo_root}/{fuzz_test} ({git_sha}). Skipping uploading."
)
continue

# Make cargo_root relative to the root of the repository, so the generated target name is libdatadog-<foldername>-<fuzz-test>
# In the future, the api will support a custom path flag
repo_root = os.path.abspath(os.getcwd())
rel_cargo_root = os.path.relpath(cargo_root, repo_root)
print(f"Uploading fuzz for {rel_cargo_root}/{fuzz_test} ({git_sha})")
upload_fuzz(rel_cargo_root, git_sha, fuzz_test)

4 changes: 4 additions & 0 deletions libdd-trace-normalization/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ bench = false
[dependencies]
anyhow = "1.0"
libdd-trace-protobuf = { version = "1.0.0", path = "../libdd-trace-protobuf" }
arbitrary = { version = "1.3", features = ["derive"], optional = true }

[features]
fuzzing = ["arbitrary"]

[dev-dependencies]
rand = "0.8.5"
Expand Down
4 changes: 4 additions & 0 deletions libdd-trace-normalization/fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts

Loading
Loading