Profiling fuzzer CPU use
profile_fuzzer records an on-CPU sampling profile for one
production libFuzzer binary. It is intended to find functions and call paths
that consume a significant part of the fuzzing budget. Use
compare_fuzzers afterwards to measure whether a
change actually improves executions per second.
The profiler requires Linux perf and Python 3.10 or newer. The fuzzer should
be an optimized, symbolized AddressSanitizer/libFuzzer build with frame pointers.
The default binaries produced by mainline.sh use the standalone replay engine
and are deliberately rejected.
Build a profiling binary
An OSS-Fuzz libFuzzer build supplies the same compiler, sanitizer, and runtime
shape used by the fleet. Given an OSS-Fuzz checkout at /path/to/oss-fuzz:
python3 /path/to/oss-fuzz/infra/helper.py build_fuzzers \
--sanitizer address \
--engine libfuzzer \
curl "$PWD"
The binaries, seed archives, dictionaries, and options are written beneath
/path/to/oss-fuzz/build/out/curl/. A target’s packaged .options file is part
of the workload: for example, the structured targets set max_len, and the
multi target sets a shorter per-input timeout.
Downloaded public corpora provide a more representative input population:
./scripts/download_public_corpus.sh
Record a profile
Profile one target for 60 seconds:
uv run profile_fuzzer \
--binary-dir /path/to/oss-fuzz/build/out/curl \
--target curl_fuzzer_proto_http \
--public-corpus-root ossfuzz_corpus \
--seconds 60 \
--output-dir build/profiles/proto-http
The profiler combines the checked-in corpus when a source checkout is
available, the target’s packaged seed ZIP, and compatible public corpora. It
deduplicates inputs by SHA-256 and runs with a fixed libFuzzer seed. Repeat
--corpus PATH to replace these default sources. Each path may be a directory,
ZIP archive, or individual input.
For structured targets, the public root must contain
.structured-schema-owned-eligibility-v1; see Corpus management.
An explicit --corpus path remains the caller’s schema/revision assertion.
By default, the profiler loads <target>.options beside the binary, falling
back to ossconfig/<target>.options when a source checkout is available. Use
--options-file to select another file and --fuzzer-arg=-name=value for
additional non-managed libFuzzer settings. The profiler owns duration, seed,
timeouts, artifacts, final stats, and single-process execution so the metadata
cannot disagree with the sampled workload.
The default event is the user-space software clock at 99 Hz. It works without a virtualized hardware performance counter and measures time actually executing on a CPU:
uv run profile_fuzzer ... \
--event cpu-clock:u \
--frequency 99
The default fp call graph requires frame pointers. Use --call-graph dwarf
for an existing binary that omitted them; DWARF stack samples are larger and
cost more to collect. --cpu N optionally pins the workload to one CPU.
The output directory must not already exist. A successful run contains:
perf.data, the original sampling data;perf-report.txt, a symbolized flat report with all sampled symbols;fuzzer.log, including the exact perf and libFuzzer diagnostics;metadata.json, with binary and corpus hashes, commands, effective options, host settings, execution metrics, and supplied provenance;corpus.zip, the exact deduplicated inputs present before fuzzing starts;summary.md, including the highest-overhead symbols; andartifacts/, for any testcase emitted by libFuzzer.
Pass corpus.zip back through --corpus to repeat the workload after its
original sources have changed or disappeared.
The text report remains useful after the build host is gone. Interactive use of
perf.data requires the exact profiled binary and its build ID; its SHA-256 and
original path are recorded in metadata.json. Preserve that binary alongside
the profile, then add it to perf’s build-ID cache before opening the data if the
original path is unavailable.
CI or build wrappers can attach revision information without exposing the complete environment:
uv run profile_fuzzer ... \
--provenance curl_revision=0123456789abcdef \
--provenance oss_fuzz_revision=fedcba9876543210
Run a profile in GitHub Actions
The Profile fuzzer workflow can be dispatched manually from the Actions
tab. Leave the target filter blank to create one matrix job for every target
offered by the workflow, or enter a case-sensitive substring such as http2 to
profile only matching targets. Then select a 30, 60, or 120 second duration.
Each job builds its target with OSS-Fuzz’s AddressSanitizer/libFuzzer
configuration, downloads the public corpus, and records a user-space
cpu-clock profile.
Each uploaded profile-... artifact contains the report, raw perf.data,
folded stacks in perf-folded.txt, an interactive flamegraph.svg, the log,
metadata, exact corpus.zip, build-toolchain manifest, and any testcase emitted
by the fuzzer. It also includes oss-fuzz-build.tar with the exact binary,
options, seed archive, and llvm-symbolizer needed to inspect the raw profile
after the hosted runner has gone away.
Interpreting results
Start with the widest stacks in flamegraph.svg and the highest-overhead symbols
in the flat report. The flame graph shows inclusive call paths, making it easier
to distinguish work owned by the harness from libcurl, TLS, parsing, compression,
protobuf mutation, and sanitizer checks. A hot function can be called frequently
because it is valuable coverage, so treat the profile as a place to investigate
rather than a reason to remove behavior.
Sampling overhead makes the run unsuitable as a throughput benchmark. Compare the candidate against an unchanged baseline with the same corpus and repeated runs as described in Benchmarking.
CPU samples deliberately exclude time spent asleep or blocked. Targets whose purpose includes timing, backpressure, or scheduler behavior need separate off-CPU analysis.
If perf record reports a permission error, inspect
/proc/sys/kernel/perf_event_paranoid and the host’s perf policy. The profiler
does not invoke sudo; changing that policy belongs to the local machine or CI
runner configuration.