curl-fuzzer
curl-fuzzer contains the fuzz targets, seed corpora, build integration, and testcase tools used by curl’s OSS-Fuzz project. It can also build standalone binaries for local regression testing and crash reproduction.
The repository has three target families:
- Legacy fuzzers (
curl_fuzzerand its protocol variants) drive libcurl from a stable Type-Length-Value (TLV) input. - Structured fuzzers use a protobuf
Scenario, target-specific mutation policies, and bounded in-process protocol peers. - Direct fuzzers feed raw bytes to focused parsers such as URL, DoH, netrc, and buffer-queue code.
Start with Getting started, then use the dedicated legacy or structured guide when changing a harness or adding seeds.
Online tools
The corpus decoder accepts legacy TLV
and structured protobuf Scenario files. It normally detects the format,
allows manual selection for ambiguous inputs, and runs entirely in the browser.
Selected testcase data never leaves the device. For command-line protobuf
decoding, see
Scenarios and corpora.
Sources of truth
Avoid copying lists that must be kept synchronized:
scripts/fuzz_targetsdefines the targets packaged for each architecture and sanitizer.schemas/curl_fuzzer.protois the complete structured scenario schema, including the stableCurlOptionIdvalues used by serialized corpora. Its marker-delimited enum block also defines the activeSetOptionsurface.corpora/contains checked-in legacy and direct-fuzzer inputs.scenarios/curl_fuzzer_proto/contains the reviewable structured seed sources.
Getting started
Prerequisites
The primary local workflow targets 64-bit Linux. It requires:
- Bash and common build tools;
- Clang and Clang++;
- CMake 3.24 or newer;
- Ninja or Make;
- Python 3;
- Git and network access for the first dependency build.
The build downloads and compiles curl and its static dependencies. It can take several minutes and use substantial disk space on its first run; later runs reuse the build tree.
Build all fuzzers
From the repository root, run:
./mainline.sh
This builds curl master and the fuzz targets with AddressSanitizer. Outputs are
placed under build/. The local build links a standalone replay engine and, on
the default non-MemorySanitizer path, runs the CTest suite.
To use an existing curl checkout:
./mainline.sh -c /path/to/curl
To build one CMake target instead of the aggregate fuzz target:
./mainline.sh -t curl_fuzzer_http
MemorySanitizer builds use a separate directory and are compile/link checks:
SANITIZER=memory ./mainline.sh
Replay inputs
Standalone binaries accept files and directories. A file produces a detailed per-input trace; a directory is walked recursively:
FUZZ_VERBOSE=1 ./build/curl_fuzzer_http \
corpora/curl_fuzzer_http/test_url_http
./build/curl_fuzzer_http corpora/curl_fuzzer_http/
Set FUZZ_VERBOSE to any value to enable libcurl’s verbose output. Structured
targets use generated binary seeds under build/generated_corpora/:
./build/curl_fuzzer_proto_http \
build/generated_corpora/curl_fuzzer_proto_http/
Run mutation fuzzing
mainline.sh produces replay binaries by default; it does not begin a mutation
fuzzing campaign. The most production-like local route uses the
OSS-Fuzz helper:
git clone --depth 1 https://github.com/google/oss-fuzz.git .oss-fuzz
python3 .oss-fuzz/infra/helper.py build_image curl
python3 .oss-fuzz/infra/helper.py build_fuzzers \
--sanitizer address --engine libfuzzer curl "$PWD"
mkdir -p build/local-corpus/curl_fuzzer_http
cp corpora/curl_fuzzer_http/* build/local-corpus/curl_fuzzer_http/
python3 .oss-fuzz/infra/helper.py run_fuzzer \
--corpus-dir "$PWD/build/local-corpus/curl_fuzzer_http" \
curl curl_fuzzer_http
Pass libFuzzer arguments after the target name when needed. The helper keeps the compiler, sanitizer, engine, and packaging behavior aligned with OSS-Fuzz.
Architecture
Every packaged legacy or structured target has a same-named source file under
fuzzer_entrypoints/.
These thin files give OSS-Fuzz and Fuzz Introspector an unambiguous target
identity while sharing the substantial harness implementation. Direct targets
define their entrypoints in their top-level implementation files instead.
| Target family | Shared implementation | Input model |
|---|---|---|
curl_fuzzer and protocol variants | legacy_fuzzer.cc, curl_fuzzer_tlv.cc, and callback/socket helpers | Stable binary TLV stream |
curl_fuzzer_proto* | proto_fuzzer/ and generated protobuf sources | curl.fuzzer.proto.Scenario |
fuzz_url, fuzz_bufq, fuzz_doh, fuzz_netrc | Dedicated source files | Raw target-specific bytes |
The normal data flow is:
seed corpus -> fuzzer engine -> target entrypoint -> shared harness -> libcurl
Legacy targets configure transfers and scripted responses from TLVs. Fixed structured targets first constrain a protobuf scenario to the compiled target profile, then run it against an appropriate bounded in-process peer. The original compatibility target retains its historical mixed behavior. Direct targets skip the transfer harness and exercise a narrow parser or data structure.
Build-time generation
The structured build derives several artifacts from the checked-in schema:
- The option-manifest generator reads the active
CurlOptionIdnames and values from the schema, checks them against the selected curl headers, stages a copy of the schema in the build tree, and writes the C++ option dispatch manifest. protocgenerates the C++ message implementation.- Textproto files under
scenarios/are encoded into binary entries underbuild/generated_corpora/. - OSS-Fuzz packaging creates one seed archive per target.
The checked-in enum values are part of the corpus wire format. Build-time
validation catches drift between that format and the curl revision being
built. The entries between the schema’s CURL-OPTIONS markers are the single
source of truth for options exposed through SetOption.
Conditional targets
scripts/fuzz_targets
is the canonical packaged target list. The structured suite is 64-bit only.
The GnuTLS, Mbed TLS, and HTTP/3 variants are omitted under MemorySanitizer
because their complete dependency stacks cannot currently be instrumented.
Corpus management
Checked-in seeds
Legacy and direct-fuzzer seeds are binary files under corpora/<target>/.
Structured seeds are authored as textproto under
scenarios/curl_fuzzer_proto/<lane>/; their generated binary form belongs in
the build tree and must not be checked in.
When the aggregate build runs, CMake writes a manifest for each generated structured corpus. Packaging and coverage use that manifest so removed scenarios cannot survive as stale build artifacts.
Public OSS-Fuzz corpora
Download the current public corpus for every supported target with:
./scripts/download_public_corpus.sh
The script writes to ossfuzz_corpus/<target>/, skips targets whose public
archive is not available yet, and leaves an existing non-empty directory alone.
Force a refresh with:
./scripts/download_public_corpus.sh -f
OSS-Fuzz prefixes target names that do not already begin with curl_. The
download script owns this mapping; documentation should not maintain a parallel
list of storage URLs.
Downloaded corpora are ignored by Git. Coverage automatically includes them
when present. Some fixed structured lanes also replay compatible historical
curl_fuzzer_proto inputs, with their target policy normalizing the scenario
before execution.
Seed archives
scripts/create_zip.sh is used by the OSS-Fuzz build to package seed corpora.
For structured targets it reads the generated manifest; for legacy and direct
targets it archives the corresponding checked-in corpus directory. Per-target
runtime settings and dictionaries live under ossconfig/.
Legacy TLV fuzzers
The legacy fuzzers are the curl_fuzzer executable and the protocol-specific
curl_fuzzer_* executables declared with curl_add_fuzzer() in
CMakeLists.txt. They consume a compact binary
type-length-value stream. The name legacy distinguishes this
input model from the structured protobuf scenario fuzzers; these targets are
still maintained and run by OSS-Fuzz.
The current protocol-specific targets cover DICT, file, FTP, Gopher, HTTP,
HTTPS, IMAP, LDAP, MQTT, POP3, RTSP, SMTP, TFTP, and WebSocket. The unsuffixed
curl_fuzzer target enables the reviewed subset of protocols supported by the
libcurl build. Direct byte-oriented targets such as fuzz_url, fuzz_doh,
fuzz_netrc, and fuzz_bufq do not use the TLV harness.
Execution model
The format can describe both sides of one transfer: curl options and upload data configure the client, while response TLVs provide bytes for the simulated peer. A particular testcase need not contain every kind of record.
- A same-named source under
fuzzer_entrypoints/exportsLLVMFuzzerTestOneInput()and delegates toLegacyFuzzerTestOneInput(). Keeping a distinct entrypoint per executable lets Fuzz Introspector attribute coverage to the correct target. legacy_fuzzer.ccinitializes oneCURLeasy handle and askscurl_fuzzer_tlv.ccto parse every record. Known records configure curl, uploads, MIME data, or one of the two simulated connections.- The target applies its compile-time protocol allow-list and standard safety options. The generic target intersects its reviewed allow-list with the protocols advertised by the linked libcurl.
curl_fuzzer_callback.ccsupplies non-blocking Unixsocketpair()sockets in place of network sockets.fuzz_handle_transfer()drives the easy handle through the multi API and releases successive response records as curl writes requests.- All per-input handles, lists, MIME objects, and sockets are released before the next testcase.
The harness also directs connections to loopback, uses short transfer timeouts, limits output, and rejects protocols outside the target’s allow-list. Resolver-sensitive proxy, interface, FTP active-mode, and pre-proxy values are canonicalized before a transfer so an old or newly mutated corpus entry cannot introduce an external hostname lookup.
Structure-aware mutation
legacy_tlv_mutator.cc exports libFuzzer’s custom mutator and crossover
callbacks. Most mutations operate on complete records. They preserve known
value shapes, keep numeric values four bytes long, avoid duplicate scalar
options, and preserve existing URL and, except for the file target,
initial-response records. When either prerequisite is missing, successive
structured mutations add it before making other edits, provided the output
buffer has enough capacity.
When both parents are structurally valid and non-empty, crossover normally combines whole records while reserving space for those prerequisites. It falls back to bounded byte crossover for invalid or empty parents, or when the output buffer cannot hold the required records.
When the mutator seed is divisible by 16, mutation instead takes a byte-level
lane (LLVMFuzzerMutate when available). This continues to explore corrupt
lengths, unknown types, and other parser failures that structure-preserving
edits cannot create. The ordinary lane truncates malformed inputs to their
valid prefix and converges toward the transfer scaffolding needed to exercise
curl.
See Working with corpora for decoding, creating, and replaying inputs, or Extending the legacy fuzzers when adding coverage.
Source map
| Path | Responsibility |
|---|---|
fuzzer_entrypoints/curl_fuzzer*.cc | Per-binary libFuzzer entrypoints |
legacy_fuzzer.cc | Per-input lifecycle, curl setup, and transfer loop |
curl_fuzzer_tlv.cc | TLV parsing and mapping to curl operations |
curl_fuzzer_callback.cc | Simulated sockets and read/write callbacks |
curl_fuzzer.h | Wire IDs, shared state, and parser helpers |
legacy_protocol_allowlist.cc | Generic target’s reviewed protocol policy |
legacy_tlv_mutator.cc | Structure-aware mutation and crossover |
src/curl_fuzzer_tools/corpus.py | Python encoder, decoder, and ID map |
TLV wire format
A legacy testcase is a sequence of binary type-length-value (TLV) records. There is no file header or record count. Each record starts with this six-byte header:
| Offset | Size | Field | Encoding |
|---|---|---|---|
| 0 | 2 bytes | Type | Unsigned 16-bit integer, network byte order |
| 2 | 4 bytes | Length | Unsigned 32-bit integer, network byte order |
| 6 | Length bytes | Value | Meaning depends on the type |
For example, the CURLOPT_URL value http://127.0.0.1 is encoded as:
00 01 00 00 00 10 68 74 74 70 3a 2f 2f 31
32 37 2e 30 2e 30 2e 31
type length = 16 value
The C parser uses to_u16() and to_u32() to decode the fields. The Python
encoder and decoder use the equivalent !H and !L formats in
src/curl_fuzzer_tools/corpus.py.
Value forms
The type number determines how the value is interpreted:
- The Python encoder writes string option values as UTF-8 without a terminating NUL. The harness copies arbitrary value bytes and appends a terminator before calling libcurl, so a mutated value may still contain embedded NUL bytes.
- Integer option records contain exactly one four-byte unsigned integer in
network byte order.
FU32TLVpasses it as along;FU32TLV_OFF_Tpasses it as acurl_off_t. - Response and upload records contain uninterpreted bytes. Response 0 is sent when the simulated socket opens; later response records are sent as curl produces requests. A small second set of response types serves protocols such as FTP that can open another connection.
- A MIME-part record (type 13) contains a nested TLV stream. Only the MIME name and data records (types 14 and 15) are valid in that nested stream.
- Header, mail-recipient, and MIME-part records may be repeated. Most curl option records are singletons.
The authoritative C type IDs are the TLV_TYPE_* definitions in
curl_fuzzer.h. BaseType and BaseType.TYPEMAP in
src/curl_fuzzer_tools/corpus.py provide the corresponding Python IDs and
human-readable decoder names. Avoid copying the full list into documentation.
tests/test_tlv_constants_sync.py checks that the C and Python numeric ID sets
remain unique and synchronized.
Parser behavior
An input shorter than six bytes cannot contain a record and returns without a transfer. For each record, a declared value that extends beyond the input is a size error, and an unknown top-level type is rejected. Integer cases also reject values whose length is not four.
HSTS type 51 remains declared for corpus compatibility but has no enabled
parser case. POSTFIELDSIZE (212) and POSTFIELDSIZE_LARGE (322) are likewise
declared but deliberately disabled to avoid easy API misuse. The
structure-aware mutation lane excludes all three, although its periodic raw
byte mutation can still produce any bit pattern. MIME name and data (14 and
15) are valid only inside a MIME-part record, not at the top level.
The harness treats fewer than six bytes left after a complete record as the end of the stream. The custom mutator deliberately matches that historical behavior, although generated corpora should end exactly on a record boundary.
The structure-aware mutator applies stricter rules than the byte parser before performing record-level edits: it accepts eligible top-level IDs, valid four-byte integers, valid nested MIME framing, and no duplicate scalar records. This distinction lets the raw mutation lane continue to test parser behavior while ordinary mutations spend more executions inside libcurl.
Continue with Working with corpora to inspect or create this format.
Working with legacy corpora
Checked-in seeds live in corpora/<target>/. The active target list is
maintained in scripts/fuzz_targets; do not infer it from old corpus
directories that may remain for historical targets.
The commands below assume the repository’s Python package is installed in an active Python 3.10 or newer environment:
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e .
With uv, the same tools can be invoked as uv run read_corpus,
uv run generate_corpus, and uv run generate_decoder_html without manually
activating the environment.
Inspect a testcase
read_corpus prints one decoded record per line:
$ read_corpus corpora/curl_fuzzer_http/test_url_http
TLVContents(type='CURLOPT_URL' (1), length=16, data=b'http://127.0.0.1')
For interactive inspection, use the published corpus decoder. It recognizes legacy TLV inputs and reads the selected file entirely in the browser. To generate a standalone copy locally:
generate_decoder_html --output /tmp/curl-corpus-decoder.html
Generate a seed
generate_corpus always requires an output path and URL. Other flags add
responses, uploads, authentication, headers, MIME parts, and selected curl
options. Run generate_corpus --help for the implemented set.
The historical --hsts argument emits disabled TLV type 51 and should not be
used for a new seed; see Parser behavior.
This Bash example creates an HTTP seed with an initial server response and then checks its contents:
generate_corpus \
--output /tmp/http-seed \
--url http://127.0.0.1/ \
--rsp0 $'HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n'
read_corpus /tmp/http-seed
Responses can instead come from a binary file (--rsp0file) or the
<reply><data> section of a curl test (--rsp0test together with
--curl_test_dir). Repeated options such as --header, --mailrecipient, and
--mimepart name:value may be supplied more than once.
Before checking in a seed, put it under the matching active target directory, decode it, and replay it with that target. A protocol-specific URL is important: an HTTP-only executable will reject an FTP URL before reaching FTP code.
Build and replay
Build one target with the repository’s standalone replay engine:
./mainline.sh -t curl_fuzzer_http
The resulting runner accepts files and directories. A directory is walked recursively:
./build/curl_fuzzer_http corpora/curl_fuzzer_http/test_url_http
./build/curl_fuzzer_http corpora/curl_fuzzer_http/
Set FUZZ_VERBOSE=1 when replaying one testcase to show curl’s verbose trace
and the simulated peer traffic:
FUZZ_VERBOSE=1 ./build/curl_fuzzer_http /path/to/testcase
The default local build replays inputs; it does not perform mutation. Follow
the local mutation workflow to
build and run the target through OSS-Fuzz’s libFuzzer environment. LibFuzzer
then discovers LLVMFuzzerCustomMutator and LLVMFuzzerCustomCrossOver from
legacy_tlv_mutator.cc; see
Legacy TLV fuzzers for their policy.
Public OSS-Fuzz corpora
Download all currently published corpora with:
./scripts/download_public_corpus.sh
The script writes each archive beneath ossfuzz_corpus/<target>/, skips
targets without a published archive, and accepts -f to refresh existing
downloads. codecoverage.sh replays these directories alongside the checked-in
seeds when they are present.
Extending the legacy fuzzers
Legacy inputs are a persistent binary format. Treat an assigned type number as
part of the corpus compatibility contract: allocate a previously unused ID and
never reuse or renumber an existing one. The grouped ranges in
legacy_tlv_mutator.cc also make the value representation part of that choice.
Add a TLV type
- Add the
TLV_TYPE_*value tocurl_fuzzer.h. - Implement its behavior in
fuzz_parse_tlv()incurl_fuzzer_tlv.cc.FSINGLETONTLVhandles a string-valued curl option,FU32TLVhandles a four-byte option declared by libcurl aslong, andFU32TLV_OFF_Thandles a four-byte option declared ascurl_off_t. The numeric macros enforce these option families at compile time. List-like, raw-byte, or nested data needs an explicit switch case and cleanup inFUZZ_DATAwhere applicable. - Add the same numeric ID to
BaseTypeand a decoder label toTYPEMAPinsrc/curl_fuzzer_tools/corpus.py. If contributors should be able to seed it from the command line, add the corresponding argument and encoder call insrc/curl_fuzzer_tools/generate_corpus.py. - Audit
GetTypeInfo()and the type-selection bounds inlegacy_tlv_mutator.cc. The mutator must know whether the payload is bytes, a string, a four-byte integer, or nested MIME, and whether it may repeat. Extending an ID range can also require updating the maximum known ID, the dense ordinal mapping, and its compile-time synchronization assertions. - If the option can trigger name resolution or another external side effect, extend the mutator’s canonicalization/safety policy and its tests before making that type eligible for structured insertion.
- Add focused parser, generator, and mutator tests, then generate or update a seed that reaches the new behavior.
FUZZ_CURLOPT_TRACKER_SPACE is not a TLV-ID limit. FSET_OPTION and
FCHECK_OPTION_UNSET index the tracker with CURLOPTNAME % 1000; when adding a
tracked curl option, verify that this remainder is smaller than the allocated
tracker space. The tracker enforces singleton curl options; this size check
avoids introducing an out-of-bounds index.
tests/test_tlv_constants_sync.py rejects duplicate numeric IDs and missing or
extra values between curl_fuzzer.h and Python’s BaseType. It does not
replace the behavioral tests needed for the parser and mutator.
Add a protocol target
When a new target can use the existing TLV execution model:
- Add
fuzzer_entrypoints/<target>.ccfollowing an existing legacy wrapper. Its basename must match the executable so coverage and Fuzz Introspector attribution remain distinct. - Add
curl_add_fuzzer(<target> <TOKEN>)and the target to the aggregatefuzztarget inCMakeLists.txt. This definesFUZZ_PROTOCOLS_<TOKEN>for the shared sources. - Add or reuse the matching branch in
fuzz_set_allowed_protocols()inlegacy_fuzzer.cc. If this expands the generic target too, update the reviewed list inlegacy_protocol_allowlist.cc. Protocols that can bypass the fake-socket model require a safety review rather than automatic inclusion. - Teach
DefaultUrl(),DefaultResponse(), and, where appropriate,TargetNeedsResponse()inlegacy_tlv_mutator.cchow to create useful transfer scaffolding for the compile-time token. - Add the executable to
scripts/fuzz_targetsso packaging, public-corpus downloads, and replay tooling agree with CMake. - Create
corpora/<target>/with at least one decoded and replayed seed.
The generic allow-list deliberately excludes protocols such as TELNET when the legacy harness cannot safely isolate their I/O. A new protocol-specific target must preserve the harness’s isolation and must not introduce an unintended network or blocking-standard-input path.
Relevant checks
Run the focused Python checks after changing IDs, generation, entrypoints, or packaging:
python -m pip install -e '.[python-tests]'
python -m pytest \
tests/test_tlv_constants_sync.py \
tests/test_generate_corpus.py \
tests/test_fuzzer_entrypoints.py
After configuring a normal build, compile and run the C++ policy tests:
cmake --build build --target \
legacy_tlv_mutator_test legacy_protocol_allowlist_test
ctest --test-dir build --output-on-failure \
-R 'legacy_(tlv_mutator|protocol_allowlist)_test'
tests/legacy_tlv_mutator_test.cc covers framing, bounds, transfer-scaffold
repair, routing canonicalization, raw mutation, and record crossover.
tests/legacy_protocol_allowlist_test.cc verifies that the generic allow-list
is both supported by the linked libcurl and limited to reviewed protocols. A
full AddressSanitizer ./mainline.sh build also builds fuzzer_unit_tests and
runs CTest.
Structured fuzzers
The structured fuzzer suite models a curl transfer as a protobuf Scenario.
libprotobuf-mutator mutates the
message rather than an unstructured byte stream, then a target-specific policy
normalizes it before the harness runs curl against a bounded local peer.
The targets still consume binary files. The textproto files under
scenarios/curl_fuzzer_proto/ are readable seed sources; CMake encodes them as
binary protobuf corpus entries under build/generated_corpora/.
How an input reaches curl
flowchart TD
subgraph build["Build time"]
sources["Checked-in schema<br/>and selected curl.h"]
generator["Validate and stage the schema;<br/>generate the C++ option manifest"]
schema["build/schemas/curl_fuzzer.proto"]
messages["Generated C++ Scenario type"]
manifest["C++ option dispatch manifest"]
seeds["Checked-in .textproto seeds"]
encoder["protoc --encode"]
corpus["Per-target .scenario corpus"]
target["Structured fuzzer executable<br/>bound to a TargetProfile"]
sources --> generator
generator --> schema
generator --> manifest
schema --> messages
schema --> encoder
seeds --> encoder --> corpus
messages --> target
manifest --> target
end
subgraph runtime["Each fuzz or replay iteration"]
input["Binary Scenario input"]
lpm["libprotobuf-mutator<br/>decode; mutate and cross over when fuzzing"]
profile{"Fixed-profile target?"}
normalize["Normalize, prune, and bound"]
compatibility["Compatibility behavior"]
runner["Scenario runner"]
curl["curl"]
peer["Harness-owned local peer"]
input --> lpm --> profile
profile -->|yes| normalize --> runner
profile -->|curl_fuzzer_proto| compatibility --> runner
runner --> curl
runner --> peer
curl <-->|bounded protocol exchange| peer
end
corpus --> input
target --> input
The CurlOptionId values are checked in because they are part of the serialized
corpus format. During the build, the option-manifest generator
reads the active options between the schema’s CURL-OPTIONS markers, checks
their values against the selected curl checkout’s curl.h, stages the schema
under build/schemas/, and generates the C++ dispatch manifest. The remaining
message types describe request data, peer responses, and focused API-lifecycle
work.
Each thin entrypoint binds the shared runtime to one TargetProfile. Fixed
profiles select a protocol, remove fields and options that their peer cannot
use, and cap repeated fields and byte budgets. This prevents mutations from
spending most of an iteration on inert or unbounded data. The original
curl_fuzzer_proto target is the exception: it preserves its historical mixed
semantics and corpus without registering a profile postprocessor.
The peers are owned by the harness. Stream protocols generally use local socket pairs; TFTP and HTTP/3 use private loopback UDP endpoints. Baseline curl configuration disables ambient proxies, restricts protocols, redirects direct connections to the harness, and uses short timeouts. A new target or option must preserve those isolation guarantees.
Build and replay a scenario
Build one structured target and its generated seed corpus:
./mainline.sh -t curl_fuzzer_proto_http
Replay one seed with the standalone runner:
./build/curl_fuzzer_proto_http \
build/generated_corpora/curl_fuzzer_proto_http/basic_get.scenario
The standalone executable accepts files and directories, so an entire target-specific seed set can be replayed too:
./build/curl_fuzzer_proto_http \
build/generated_corpora/curl_fuzzer_proto_http/
Set FUZZ_VERBOSE=1 when a reproduction needs curl’s protocol trace. These
locally built executables replay inputs; OSS-Fuzz builds provide the active
libFuzzer mutation engine.
Continue with Writing and inspecting scenarios, then consult Target profiles before choosing a corpus or adding a field.
Writing and inspecting scenarios
The checked-in scenario sources live below scenarios/curl_fuzzer_proto/ and
use protobuf’s text format. They are the source of truth for seed inputs. Do
not check generated .scenario files into corpora/; CMake writes those
derived binary files to build/generated_corpora/<corpus-name>/. The corpus
name normally matches the target. The GnuTLS and mbedTLS HTTPS variants reuse
build/generated_corpora/curl_fuzzer_proto_https/; the canonical mapping is
in scripts/fuzz_corpus_helpers.sh.
A minimal scenario
scheme: SCHEME_HTTP
host_path: "127.0.0.1/basic"
connection {
initial_response: "HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
}
scheme and host_path form the request URL. Most byte-bearing fields use
protobuf bytes, so textproto escape sequences can represent arbitrary wire
data. SetOption entries use a checked-in CURLOPT_* identifier and a oneof
typed value. At most one value member can be present; authored seeds should use
the kind expected by that option:
options {
option_id: CURLOPT_NOBODY
bool_value: true
}
The main groups of fields are:
- Request configuration:
options,request_headers,mime_post,upload, andtelnet_options. - Peer work:
connection,subsequent_connections, structured WebSocket frames, andhttp3_plan. - Focused lanes:
api_plan,multi_plan,resolve_entries, TLS certificate selection,socks_proxy_mode,trace_ids, and filename-backed parser inputs.
Read the comments in schemas/curl_fuzzer.proto for the complete field
contract. Its checked-in CurlOptionId numbers are part of the serialized
corpus format. During the build, the option-manifest generator validates them
against the selected curl checkout’s curl.h, then stages an identical schema
at build/schemas/curl_fuzzer.proto. The entries between the schema’s
CURL-OPTIONS markers are the active options available to SetOption.
Peer scripts depend on the target
For the ordinary HTTP peer, connection.initial_response is sent when curl
opens the socket and each on_readable value is released on a later readable
turn. subsequent_connections supplies bounded scripts for redirects,
authentication retries, and other fresh HTTP sockets.
Other peers intentionally give those fields narrower meanings:
- Plain WebSocket scenarios use a peer that generates the
101handshake and accepts either raw chunks or structuredserver_frames. The WSS lane covers secure setup under a fixed scheme; it does not promise post-handshake frame coverage. - The dedicated HTTPS peer encrypts connection-script bytes as HTTP application data. The compatibility target preserves its older behavior and treats HTTPS script bytes as raw TLS records.
- TELNET preloads a bounded response before entering curl’s blocking protocol loop.
- FTP treats the primary connection as command-aligned control replies and follow-on connections as data streams.
- TFTP preserves packet boundaries: the initial response and each readable chunk are separate datagrams.
- HTTP/2 origin and proxy lanes treat connection bytes as raw HTTP/2 frames after a fixed TLS/ALPN setup.
- HTTP/3 uses ordered
http3_planactions after a valid QUIC/TLS handshake; the ordinary connection script is discarded.
Use a seed already assigned to the intended target as the closest example. A valid protobuf message can still be irrelevant to a lane if that lane’s policy removes the field or replaces its scheme.
Add a seed
- Put a descriptively named
.textprotofile in the closest protocol directory underscenarios/curl_fuzzer_proto/. Its filename stem must be unique across every source directory combined into the destination corpus, because generated corpora are flat. - Check the corpus declarations in
CMakeLists.txt. Directory membership is not the complete assignment rule: the fast HTTP set is explicit, the deep and compatibility sets combine selected directories, and the timing set is selected by abackpressurefilename pattern. - Build the destination target. CMake encodes the source with the staged
schema and creates
<name>.scenarioin the corpus assigned to that target. - Decode the generated file and replay it through the matching executable.
For example:
./mainline.sh -t curl_fuzzer_proto_http
uv run read_proto_corpus \
build/generated_corpora/curl_fuzzer_proto_http/basic_get.scenario
FUZZ_VERBOSE=1 ./build/curl_fuzzer_proto_http \
build/generated_corpora/curl_fuzzer_proto_http/basic_get.scenario
Adding a new fast-HTTP seed also requires updating its explicit list and count
check in CMakeLists.txt.
Decode a binary input
For interactive inspection, use the published
corpus decoder. It recognizes protobuf Scenario files
and reads the selected file entirely in the browser.
read_proto_corpus wraps protoc and prints named textproto fields using the
checked-in schema. It falls back to the staged build copy when the source
schema is unavailable:
uv run read_proto_corpus path/to/crash-input
uv run read_proto_corpus \
--proto-file build/schemas/curl_fuzzer.proto path/to/crash-input
The command requires protoc on PATH. It checks --proto-file, then
CURL_FUZZER_PROTO, the checkout’s schemas/curl_fuzzer.proto, and finally
build/schemas/curl_fuzzer.proto. Without a schema it falls back to
wire-level field numbers; request that form explicitly with --raw:
uv run read_proto_corpus --raw path/to/crash-input
Use the schema from the curl-fuzzer revision that produced the input when
investigating CURLOPT_* values.
Target profiles
All structured targets share the binary Scenario wire format and most of the
runtime. A target profile defines which part of that format is meaningful,
which peer is used, and how much work one mutation may create.
| Target | Profile and peer focus |
|---|---|
curl_fuzzer_proto | Compatibility target for the historical mixed HTTP, HTTPS, WebSocket, and TELNET corpus. It deliberately has no profile postprocessor. |
curl_fuzzer_proto_http | High-throughput plaintext HTTP. Retains cheap request options and raw response parsing; removes MIME, uploads, follow-on sockets, and timing controls. |
curl_fuzzer_proto_http_deep | Stateful HTTP coverage, including redirects, authentication, MIME, uploads, result APIs, and bounded cookie, Alt-Svc, HSTS, and netrc files. |
curl_fuzzer_proto_http2 | High-throughput HTTP/2 origin frames over plaintext prior knowledge, using the same structured and malformed frame grammar as the TLS lane. |
curl_fuzzer_proto_https | HTTP/1.1 through a real in-process TLS peer, including certificate, session, TLS result-state, and bounded CRL-input coverage. |
curl_fuzzer_proto_https_gnutls / curl_fuzzer_proto_https_mbedtls | The HTTPS profile with a GnuTLS or mbedTLS curl client; the local server side remains the harness TLS peer and both variants reuse the HTTPS generated seed corpus. |
curl_fuzzer_proto_https_h2 | Structured and malformed HTTP/2 origin frames after a verified TLS/ALPN handshake, plus push and upkeep probes. |
curl_fuzzer_proto_http3 | Structured or raw HTTP/3/QPACK work after a real local QUIC/TLS handshake. |
curl_fuzzer_proto_h2_proxy | An HTTP/1.1 origin request through a fixed trust-anchor-verified HTTPS/HTTP/2 CONNECT proxy; mutations control bounded raw proxy frames and origin request settings. |
curl_fuzzer_proto_socks4 | HTTP through an in-process SOCKS4 or SOCKS4A proxy. |
curl_fuzzer_proto_resolver | Localhost resolution and bounded CURLOPT_RESOLVE host-cache operations while harness callbacks retain transport control. |
curl_fuzzer_proto_ws | Plaintext WebSocket handshake, framing, callbacks, and manual-drive paths. |
curl_fuzzer_proto_wss | Secure-WebSocket setup under a fixed WSS scheme, with backpressure removed from this fast lane. |
curl_fuzzer_proto_telnet | Bounded TELNET negotiation and callback-backed input against a preloaded local peer. |
curl_fuzzer_proto_ftp | Plain FTP control plus passive or loopback-confined active data connections. |
curl_fuzzer_proto_tftp | Packet-preserving TFTP exchanges over private loopback UDP endpoints. |
curl_fuzzer_proto_gopher | Bounded Gopher or Gophers selectors through a stream peer, using the TLS peer for Gophers when available. |
curl_fuzzer_proto_api | Easy, share, multi, URL, connect-only, pause/resume, and typed result API lifecycles described by api_plan. |
curl_fuzzer_proto_multi | Two to four easy handles on one shared multi handle, with bounded scheduling actions, connection-cache controls, and a five-second packaged timeout. |
curl_fuzzer_proto_timing | Plain HTTP or WebSocket backpressure and timed-wait behavior; it guarantees a non-default bounded pressure configuration. |
The exact target inventory and platform gates are maintained in
scripts/fuzz_targets. Structured targets are excluded from i386 builds. The
GnuTLS, mbedTLS, and HTTP/3 variants are also excluded from MemorySanitizer
builds, and HTTP/3 is created only when its dependency variant is enabled.
MemorySanitizer omits OpenSSL and the TLS mock peer. The plaintext
curl_fuzzer_proto_http2 lane remains active in that build. In contrast,
curl_fuzzer_proto_https_h2 and curl_fuzzer_proto_h2_proxy return without
driving their peer-dependent scenarios, while curl_fuzzer_proto_https cannot
complete its ordinary verified-TLS path.
What policy application changes
Before a fixed target executes a loaded or newly mutated message, its postprocessor:
- fixes or narrows the scheme and, where required, canonicalizes the authority;
- retains only options that can affect that peer and transfer mode;
- removes protocol-specific plans, file inputs, and connection shapes that the lane cannot consume;
- caps options, headers, response chunks, connection count, upload data, MIME parts, API actions, and protocol-specific byte budgets; and
- canonicalizes small enums and transport settings onto useful, safe ranges.
The shared limits are in proto_fuzzer/scenario_limits.h; profile-specific
selection is in proto_fuzzer/target_policy.cc. Runtime code repeats important
bounds so callers that bypass mutation policy cannot create unchecked work.
The packaged libFuzzer configuration also caps each serialized structured
input at 32 KiB.
Fast lanes clear backpressure because a single mutated value would otherwise
move an ordinary case into a timed loop. The timing lane does the inverse and
forces bounded pressure. HTTP/3 discards the stream Connection entirely,
while FTP, TFTP, TELNET, and Gopher prune fields that their specialized peers
cannot observe.
The compatibility profile is intentionally different. Existing OSS-Fuzz
inputs are keyed to curl_fuzzer_proto, so changing its normalization would
change the meaning of accumulated corpus and crash files. New protocol enum
values and fields must therefore default to the old behavior or be gated so
the compatibility runner does not reinterpret historical wire data.
Choosing a target for a seed or crash
Use the narrowest target that owns the behavior:
- Prefer
httpfor cheap request/response parsing,http_deepfor stateful or file-backed work, andtimingfor intentional backpressure. - Use
https,http2,https_h2, orhttp3according to the actual transport rather than putting protocol frame setup into an HTTP seed. - Put lifecycle-only work in
apiormulti, keeping it out of protocol-hot loops. - Replay a crash with the binary named by OSS-Fuzz. Although fixed lanes share a wire format, their policies can transform the same bytes differently.
See Writing and inspecting scenarios for corpus generation and Extending the structured suite for the invariants a new lane must implement.
Extending the structured suite
Structured inputs work best when mutations reliably reach curl without making each iteration expensive. Extend the smallest existing surface that owns the behavior. Consider serialization compatibility, object lifetimes, target policy, and local transport isolation together.
Add a supported curl option
SetOption is for scalar values and copied or scenario-owned strings. To add
one:
- Add its stable numeric value in alphabetical order between the
CURL-OPTIONSmarkers in theCurlOptionIdenum inschemas/curl_fuzzer.proto. This block is the active option list. Existing values are part of the corpus wire format and must not be changed or reused. - If its
curl.htype does not determine the intended protobuf value kind, update the overrides insrc/curl_fuzzer_tools/generate_option_manifest.py. - Decide which fixed profiles may retain it. The fast HTTP, HTTP/2 proxy,
HTTP/3, TELNET, FTP, and TFTP lanes use explicit option allowlists in
proto_fuzzer/target_policy.cc. - Add a correlated textproto seed when reaching useful code requires other options or particular peer bytes.
At build time the generator reads the selected checkout’s curl.h, validates
the marker-delimited names and numbers against it, stages the schema in the
build tree, and emits the C++ value-kind dispatch table.
Callbacks, slists, files, and other pointer-bearing options usually need a schema-native field plus explicit harness-owned storage. Their backing objects must remain alive through transfer completion and easy-handle cleanup. Existing examples include request headers, MIME, upload state, TELNET options, resolver entries, and bounded anonymous parser files.
Keep filename-backed cookie, Alt-Svc, and HSTS options out of SetOption; the
baseline owns their fixed /dev/null sinks, while the enum exposes only their
in-memory controls. CURLOPT_FTPPORT and CURLOPT_FTP_USE_EPRT are safe
because the FTP peer confines active listeners to loopback. FTPS and the
slist-backed QUOTE options need protocol-specific transport and ownership
before they can join this surface. The TFTP controls reuse the schema’s upload
body and size fields to exercise option negotiation and raw RRQ/WRQ creation.
Some scalar options still need correlated seeds. The HTTP Message Signature key, key ID, header list, and algorithm options are one example: keep their values together in the deep HTTP corpus so mutations can reach both parsing and rejection paths.
Add or change a schema field
The binary corpus is persistent. Add new fields with unused field numbers and append new enum values; never renumber or reuse an existing tag. Choose a default that preserves the behavior of older serialized inputs.
Then update all parts of the contract:
- Add an appropriate complexity limit in
proto_fuzzer/scenario_limits.h. - Bound the runtime-visible value and remove it from profiles that cannot use
it in
proto_fuzzer/target_policy.cc. - Implement ownership and execution in the request-data, runner, or peer layer.
- Ensure the compatibility target does not silently acquire new semantics from bytes that older schemas treated as unknown.
- Add policy tests for retention, removal, canonicalization, and boundary values, plus runtime tests when the field owns resources or changes I/O.
Protobuf repeated and bytes fields have no intrinsic work bound. In fixed
lanes, a runtime that merely ignores an oversized suffix is not sufficient:
the target postprocessor must remove it too, or libprotobuf-mutator will
continue allocating and mutating data that cannot add coverage. The historical
compatibility lane cannot acquire a new postprocessor, so its runtime boundary
must remain safe without changing existing input semantics.
Add a target profile
A new lane normally requires all of the following:
- A
TargetProfileand a completeRunModeFormapping inproto_fuzzer/target_profile.h. - A policy branch in
proto_fuzzer/target_policy.ccthat fixes routing, retains only meaningful fields, and applies shared bounds. - A bounded peer or runner path. It must not consult ambient DNS, proxies, trust stores, or remote endpoints, and malformed scripts must terminate by operation or idle budgets rather than long wall-clock waits.
- A thin
fuzzer_entrypoints/curl_fuzzer_proto_<name>.ccbinding all three libFuzzer entrypoints to the same profile. - A CMake executable declaration, generated corpus declaration (or an
explicit reuse mapping in
scripts/fuzz_corpus_helpers.sh), and dependency from the executable to that corpus. - An
ossconfig/<target>.optionsfile and an entry inscripts/fuzz_targets, with architecture or sanitizer gates when required. - Entrypoint, target-policy, peer/runtime, packaging, and representative scenario tests.
- A row in Target profiles.
Keep throughput-sensitive parsing separate from expensive lifecycle, TLS, file
parser, or timed work. Existing http, http_deep, and timing profiles show
how one protocol surface can be split without making its fast lane pay for
every coverage feature.
Verify a change
Run the focused Python tests for schema generation and target wiring, then build the affected binary and the C++ unit-test aggregate:
uv run --extra python-tests pytest \
tests/test_generate_option_manifest.py \
tests/test_fuzzer_entrypoints.py
./mainline.sh -t curl_fuzzer_proto_http
cmake --build build --target fuzzer_unit_tests
ctest --test-dir build --output-on-failure
Replace the example target with the lane being changed. Finally, decode and replay each new generated seed as described in Writing and inspecting scenarios. For performance-sensitive policy changes, use the benchmarking guide; source coverage and executions per second answer different questions and should be checked together.
Python tools
The curl-fuzzer-tools package requires Python 3.10 or newer.
Install with uv
uv sync
uv run read_corpus corpora/curl_fuzzer_http/test_url_http
Add the Python test extra when developing:
uv sync --extra python-tests
Install with pip
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -e .
Commands
| Command | Purpose |
|---|---|
read_corpus | Decode one legacy TLV input |
read_proto_corpus | Decode one binary protobuf scenario using protoc |
generate_corpus | Generate a legacy TLV testcase |
tlv_to_proto | Convert a directory of legacy HTTP inputs to textproto |
generate_decoder_html | Build the standalone browser decoder for legacy TLV and protobuf Scenario inputs |
corpus_to_pcap | Convert response TLVs to a packet capture; requires Scapy from the development dependency group |
generate_matrix | Package built fuzzers into balanced artifact shards and produce the CI matrix |
prepare_fuzzer | Extract one fuzzer and its supporting files from a CI artifact shard |
generate_option_manifest | Validate and stage the protobuf schema, then generate its C++ curl-option manifest |
Run any command with --help for its complete interface.
Browser decoder
The published decoder
accepts legacy TLV and protobuf Scenario inputs. It detects the format
automatically, allows manual selection, and does not upload the selected file.
Empty, unknown-only, or damaged inputs may require selecting a format manually.
The generator bundles the locked protobuf.js runtime and the checked-in schema
into the HTML, so the resulting page does not load a decoder from a CDN.
Build the complete documentation site and decoder locally with:
npm ci --ignore-scripts --no-audit --no-fund
mdbook-mermaid install .
mdbook build
uv run generate_decoder_html --output _site/corpus-decoder/index.html
Then open _site/index.html for the book or
_site/corpus-decoder/index.html for the decoder. Build mdBook first because it
recreates the output directory.
Coverage
codecoverage.sh builds curl and the fuzzers with LLVM source-based coverage,
replays the available corpora, and limits the report to curl’s lib/ and
src/ trees.
./codecoverage.sh
The generated reports are:
build-coverage/coverage/summary.txt, anllvm-cov reportsummary;build-coverage/coverage/html/index.html, a browsable annotated report.
Measure a local curl checkout with:
./codecoverage.sh -c /path/to/curl
If ossfuzz_corpus/ exists, its downloaded inputs are replayed alongside the
checked-in or generated seeds. To rerun one target after an instrumented build:
BUILD_DIR="$PWD/build-coverage" \
TARGETS=curl_fuzzer_proto_http \
./scripts/run_coverage.sh
The GitHub Actions Coverage workflow is manual. It uploads both reports,
writes the text summary to the job summary, and reuses a public-corpus cache
whose key changes each ISO week.
For controlled A/B performance and source-coverage comparisons, see Benchmarking.
Reproducing fuzzer findings
OSS-Fuzz reports identify the target binary, fuzzing engine, sanitizer, and platform that found a failure. Reproduce with the closest available configuration: a crash found under UndefinedBehaviorSanitizer, for example, may not be visible in the default AddressSanitizer build.
These instructions assume that the testcase has been downloaded from the OSS-Fuzz report.
Inspect the testcase
Legacy TLV targets
Decode a legacy input with the Python tool:
read_corpus clusterfuzz-testcase-minimized-curl_fuzzer_http-<id>
For example, a URL-only input is displayed as:
TLVContents(type='CURLOPT_URL' (1), length=16, data=b'http://127.0.0.1')
The hosted corpus decoder provides the same kind of inspection in a browser without uploading the file. It recognizes legacy TLV inputs automatically.
Structured protobuf targets
The hosted corpus decoder also decodes
protobuf Scenario files entirely in the browser. It normally detects the
format, or you can select it manually for ambiguous inputs.
From a source checkout, read_proto_corpus uses the checked-in schema to print
field names. It can also use a staged copy under build/schemas/ when the
source schema is unavailable:
read_proto_corpus \
clusterfuzz-testcase-minimized-curl_fuzzer_proto_http-<id>
The command requires protoc. If it cannot find either schema, it falls back
to protoc --decode_raw; pass --proto-file to select one explicitly.
Published reproduction images also include the staged schema and a
decode-scenario command:
docker run --rm -i \
-v "$PWD/clusterfuzz-testcase-minimized-curl_fuzzer_proto_multi-<id>:/testcase:ro" \
curlfuzzer.azurecr.io/address-libfuzzer \
decode-scenario /testcase
It also accepts the testcase on standard input:
docker run --rm -i curlfuzzer.azurecr.io/address-libfuzzer decode-scenario \
< clusterfuzz-testcase-minimized-curl_fuzzer_proto_multi-<id>
Direct parser targets consume target-specific raw bytes and generally have no separate decoder.
Reproduce with a local standalone build
Build the target, optionally against a local curl checkout:
./mainline.sh -t curl_fuzzer_http
./mainline.sh -c /path/to/curl -t curl_fuzzer_http
The resulting binary is under build/. Set FUZZ_VERBOSE to enable detailed
libcurl logging:
FUZZ_VERBOSE=1 ./build/curl_fuzzer_http \
clusterfuzz-testcase-minimized-curl_fuzzer_http-<id>
The default build uses AddressSanitizer. It is suitable for many
libfuzzer_asan findings and provides a fast edit-build-replay loop. A matching
OSS-Fuzz container is preferable when the report uses another sanitizer,
engine, architecture, or dependency configuration.
Reproduce in OSS-Fuzz
Follow the upstream OSS-Fuzz reproduction guide for complete instructions. From an OSS-Fuzz checkout, an UndefinedBehaviorSanitizer HTTP reproduction is:
python3 infra/helper.py build_image curl
python3 infra/helper.py build_fuzzers --sanitizer undefined curl
python3 infra/helper.py reproduce \
curl curl_fuzzer_http \
/path/to/clusterfuzz-testcase-minimized-curl_fuzzer_http-<id>
Once reproduced, use the normal sanitizer stack trace, debugger, and targeted
logging to narrow the fault. For AddressSanitizer investigations in GDB, a
breakpoint on __asan::ReportGenericError can stop at the point where the
runtime reports the invalid access.
Comparing fuzzer coverage and speed
scripts/compare_fuzzers.py runs repeatable A/B measurements against
production-style libFuzzer binaries. It records libFuzzer coverage (cov),
features (ft), executions per second, corpus growth, peak RSS, and wall/user/
system time. Complete logs and crash artifacts are retained, and any failed or
unparseable run makes the command fail.
Build the baseline and candidate with the same curl revision, compiler, sanitizer, and OSS-Fuzz settings. For example, use two OSS-Fuzz output directories produced with the address sanitizer and libFuzzer. Then run:
scripts/compare_fuzzers.py \
--baseline-dir /path/to/baseline/out/curl \
--candidate-dir /path/to/candidate/out/curl \
--seconds 30 \
--repeats 5 \
--cpu 0 \
--output build/measurements/comparison.json
The defaults cover the legacy FTP/HTTP/HTTPS/TFTP/WS targets and the fixed
structured fast HTTP, deep HTTP, HTTPS, WS, WSS, TELNET, FTP, TFTP, API, and
timing lanes. The deep HTTP target
retains stateful authentication, redirect, upload, MIME, and result-probe
work so the ordinary HTTP lane can concentrate its CPU budget on single-request
URL and response parsing. The original mixed-semantics
curl_fuzzer_proto target is retained for OSS-Fuzz corpus and testcase
compatibility, but is not benchmarked by default because its variable scheme
and timing policy obscures lane-level throughput.
Repeat --target to choose a different same-name A/B set:
scripts/compare_fuzzers.py \
--baseline-dir /path/to/baseline/out/curl \
--candidate-dir /path/to/candidate/out/curl \
--target curl_fuzzer_proto_http \
--target curl_fuzzer_proto_http_deep \
--target curl_fuzzer_proto \
--target fuzz_url
To compare a legacy target with a differently named proto replacement, use a target pair:
scripts/compare_fuzzers.py \
--baseline-dir /path/to/legacy/out/curl \
--candidate-dir /path/to/proto/out/curl \
--target-pair curl_fuzzer_http=curl_fuzzer_proto_http \
--target-pair curl_fuzzer_https=curl_fuzzer_proto_https \
--target-pair curl_fuzzer_ftp=curl_fuzzer_proto_ftp \
--target-pair curl_fuzzer_tftp=curl_fuzzer_proto_tftp \
--target-pair curl_fuzzer_ws=curl_fuzzer_proto_ws
The label in the report is, for example,
curl_fuzzer_http=>curl_fuzzer_proto_http. Each side of a mapped pair receives its
own native corpus and seed archive; legacy TLV bytes are never passed to the
protobuf target. Same-name --target comparisons retain the stricter behavior
of sharing one content-identical snapshot. --corpus TARGET=PATH overrides are
keyed by the actual binary target name, so a mapped pair may supply one override
for each side.
For a same-name target, the input snapshot combines the checked-in corpus with
the baseline’s <target>_seed_corpus.zip, deduplicating by SHA-256. A mapped
pair resolves the checked-in corpus and seed archive independently from its
baseline and candidate layouts. Add a downloaded fleet corpus with
--public-corpus-root ossfuzz_corpus. Override a target’s inputs completely
with one or more --corpus TARGET=PATH arguments; PATH may be a directory,
zip archive, or individual input.
For every fixed proto lane, --public-corpus-root also includes the historical
curl_fuzzer_proto public corpus. The protobuf wire format is shared, and the
lane postprocessor normalizes scheme and timing controls before replay, so this
preserves accumulated fleet coverage while the new target corpora grow.
Every run gets a fresh copy of one content-addressed snapshot. Repeat N uses
the deterministic seed --seed-base + N - 1; baseline/candidate order
alternates between repeats to reduce ordering bias. The JSON records corpus and
binary hashes, exact managed arguments, host details, individual results, and
medians. Raw cov and ft values are only comparable when compiler
instrumentation and the curl revision are held constant. They remain recorded
for mapped targets, but their cross-harness deltas are deliberately reported as
null; use the source coverage comparison below for legacy-to-proto parity.
Compare curl source coverage
Raw libFuzzer cov and ft counters include each harness and cannot prove
coverage parity between a legacy target and a proto target. The comparison
helper can additionally replay the fixed native corpus through standalone LLVM
coverage builds and compare curl-relative function and code-region identities.
Create an instrumented build in each source worktree with codecoverage.sh.
The default build layout puts binaries under build-coverage/ and curl sources
under build-coverage/curl/src/curl_external, which the helper detects:
(cd /path/to/baseline-worktree && ./codecoverage.sh)
(cd /path/to/candidate-worktree && ./codecoverage.sh)
scripts/compare_fuzzers.py \
--baseline-dir /path/to/baseline-libfuzzer-out \
--candidate-dir /path/to/candidate-libfuzzer-out \
--target-pair curl_fuzzer_http=curl_fuzzer_proto_http \
--target-pair curl_fuzzer_http=curl_fuzzer_proto_http_deep \
--target-pair curl_fuzzer_http=curl_fuzzer_proto_timing \
--coverage-baseline-dir /path/to/baseline-worktree/build-coverage \
--coverage-candidate-dir /path/to/candidate-worktree/build-coverage \
--seconds 60 \
--repeats 5
The aggregate source-coverage comparison unions the three candidate lanes, so the fast target can intentionally omit stateful behavior without making the replacement-parity number look worse. Benchmark the fast lane separately when judging whether it can take over the legacy HTTP CPU allocation; the deep and timing targets exist to retain coverage, not to match its throughput.
When curl was supplied with codecoverage.sh -c, pass the source roots
explicitly:
scripts/compare_fuzzers.py \
... \
--baseline-curl-source-dir /path/to/baseline-curl \
--candidate-curl-source-dir /path/to/candidate-curl
Use --llvm-profdata and --llvm-cov for versioned executable names, and
--coverage-wall-timeout to change the per-target replay limit. The helper
validates all binaries and tools before fuzzing, requires byte-identical curl
lib/ and src/ source trees, and rejects a coverage binary that produces no
profile. This prevents line-number comparisons across different curl revisions
from looking meaningful.
The main JSON gains a source_coverage object with per-target and aggregate
summaries. Its comparison records baseline-retention percentages plus complete
baseline_only and candidate_only function/region lists. Raw profiles,
merged profiles, exports, and replay logs are preserved below
<log-dir>/source-coverage/. The source metric deliberately replays a fixed
input snapshot; discoveries from the timed fuzzing runs do not leak into it.
Timer and event-loop scheduling can still move an occasional region between
runs, so repeat the replay before treating a marginal one-region gap as real.
For a conventional combined HTML report, a single target can still be replayed after an instrumented build exists:
BUILD_DIR="$PWD/build-coverage" \
TARGETS=curl_fuzzer_proto_http \
scripts/run_coverage.sh
Use source coverage and libFuzzer features together when evaluating corpus minimization or harness changes: line coverage alone does not preserve value, counter, and indirect-call features used by the mutation scheduler.
Development
Run checks
Install the Python testing and development dependencies:
uv sync --extra python-tests
Run the Python tests and repository lint entrypoint:
uv run pytest tests/test_*.py
./lint.sh
mainline.sh builds the C++ tests and runs CTest for the normal AddressSanitizer
aggregate build. The MemorySanitizer lane compiles and links them but does not
execute against the host’s uninstrumented C++ runtime.
Documentation
Install the pinned mdBook and Mermaid preprocessor versions:
# renovate: datasource=crate depName=mdbook
MDBOOK_VERSION=0.5.4
# renovate: datasource=crate depName=mdbook-mermaid
MDBOOK_MERMAID_VERSION=0.17.1
cargo install mdbook --version "=${MDBOOK_VERSION}" --locked
cargo install mdbook-mermaid --version "=${MDBOOK_MERMAID_VERSION}" --locked
With Node.js 18 or newer, install the locked JavaScript dependencies used by the browser decoder:
npm ci --ignore-scripts --no-audit --no-fund
Then generate Mermaid’s JavaScript assets and build the documentation from the repository root:
mdbook-mermaid install .
mdbook build
uv run generate_decoder_html --output _site/corpus-decoder/index.html
The Mermaid assets are generated and ignored by Git. Delete them before
rerunning the installer after an mdbook-mermaid upgrade because the installer
does not overwrite existing assets. For live editing, run
mdbook serve --open. The browser decoder is generated separately and is
therefore not refreshed by the mdBook development server.
Run its optional browser tests for both legacy TLV and protobuf Scenario
inputs with:
uv sync --extra browser-tests
uv run playwright install chromium
uv run pytest tests/browser/test_corpus_decoder.py
The documentation CI builds the book and decoder together. The Pages workflow
publishes _site/ from master for https://fuzz.curl.se/. book.toml
records the intended hostname, but the custom domain and HTTPS enforcement must
also be configured in the repository’s Pages settings.
Generated files
Do not commit _site/, build/generated_corpora/, staged schema copies under
build/, or downloaded public corpora. Commit the schema under schemas/,
authored Markdown under docs/, legacy binary seeds under corpora/, and
structured textproto seeds under scenarios/curl_fuzzer_proto/.