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 is an ordered oneof whose field names and protobuf types
identify the curl options directly. At most one option can be selected in each
entry, and oneof presence preserves explicit false, zero, and empty values:
options {
nobody: true
}
The typed field carries build-time metadata naming CURLOPT_NOBODY and listing
the profiles allowed to retain it. There is no separate option ID or value-kind
field in authored input. Repeating options preserves setopt order and
duplicates.
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_planand its closed state-probe profile,resolve_entries,happy_eyeballs,doh, TLS certificate selection, HTTPS/H2 key-exchange selection, bounded HTTPS client-credential and handshake profiles,pop3_starttls_filter_allocation_failure,ftp_auth_tls_filter_allocation_failure,gopher_probe_mode,mqtt_probe_mode,smtp_probe_mode,socks_proxy_mode,haproxy_protocol,http_transport_mode,proxy_socket_option_mode,trace_ids,probe_multi_pollset,resolver_mode, filename-backed parser inputs, andfail_allocations.
Read the comments in schemas/curl_fuzzer.proto for the complete field
contract. It is the annotated authoring schema: typed option fields declare
their native curl identity and profile membership, and structured fields
declare their own eligibility. During the build, the schema-eligibility
generator validates those declarations against the selected curl checkout’s
curl.h, emits restricted target roots, and stages a metadata-free authoring
view at build/schemas/curl_fuzzer.proto for ordinary protoc and browser
tools.
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.
The deep HTTP lane can instead select a fixed plaintext forward proxy, a fixed
HTTP/1.1 CONNECT proxy, or a fixed abstract Unix-domain origin with
http_transport_mode. Proxy traffic is always directed to a numeric loopback
endpoint and every socket is replaced by the in-process peer; Unix mode uses a
harness-owned abstract name and creates no filesystem entry. Use
request-triggered on_readable chunks for these modes. A CONNECT script puts
the tunnel response first and the origin response second. The correlated
haproxy_protocol CONNECT profile instead sends the bounded request headers to
both the proxy and origin and applies fixed bounded send pressure. The long
valid CONNECT request, small peer drains, and repeated pollset probes make the
pending fixed IPv6 HAProxy v1 setup state reachable without allowing the
scenario to select either endpoint.
The deep HTTP lane’s proxy_socket_option_mode selects a closed set of option
paths. Fixed hostname, IPv4-CIDR, and IPv6-CIDR lists exercise
CURLOPT_NOPROXY; a scoped http_proxy override points only at a fixed
numeric loopback endpoint intercepted by the socketpair callback and restores
the previous environment afterwards. The interface/keepalive mode returns an
unconnected INET socket only for its owned ephemeral loopback listener, binds
it with host!127.0.0.1, and applies fixed TCP keepalive values. Mutations
cannot supply an environment value, interface, bind address, proxy endpoint,
or destination port.
Other peers intentionally give those fields narrower meanings:
- WebSocket scenarios use a peer that generates the
101handshake and accepts either raw chunks or structuredserver_frames. The WSS lane runs that same Upgrade, framing, callback, and manual-drive behavior after a verified in-process TLS handshake. ForCURLOPT_CONNECT_ONLY=2, the peer coalesces the first bounded frame with the101response so libcurl must preserve already-read payload forcurl_ws_recv; later chunks are delivered by the manual-drive tail. - The dedicated HTTPS peer encrypts connection-script bytes as HTTP
application data. Its optional
tls_client_credential_profileselects only harness-owned malformed blobs or fixed/dev/nullengine/provider probes; protobuf bytes never become a filename, engine name, or provider name.pinned_public_key_fileinstead exposes bounded protobuf bytes through a session-owned anonymous descriptor and installs only its/proc/self/fdpath, allowing curl’s filename-backed PEM pin parser to run without host-file access. The separatetls_handshake_profilecan replace the OpenSSL peer with a fixed pre-handshakeclose_notifyor empty HelloRequest record. It covers curl’s terminal error translation and message classification without accepting fuzz-selected TLS bytes; GnuTLS and mbedTLS clients treat those profiles as the ordinary verified peer. The compatibility target preserves its older behavior and treats HTTPS script bytes as raw TLS records. These controls deliberately leaveCurl_ssl_cfilter_addto a protocol peer capable of an in-place STARTTLS upgrade. Process-lifecycle probes forossl_cleanup,Curl_ssl_cleanup,Curl_ssl_version, andCurl_tls_keylog_closebelong in the one-shot API lane instead of a per-input HTTPS path. - 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. An
SCHEME_FTPSscenario runs the identical command and passive-data exchange over implicit TLS: the control connection and each EPSV/PASV data connection complete a verified handshake, and the script must answer curl’sPBSZ 0andPROT Pcommands after login. Its fixed authority istls.test, matching the checked-in trust anchor, and the handshakes carry no ALPN extension. Active mode is not available in this lane because it would require wrapping a real TCP listener rather than a socketpair;CURLOPT_FTPPORTis removed from FTPS inputs. The plaintext-onlyftp_auth_tls_filter_allocation_failureselector accepts AUTH SSL and fails the adjacent TLS-filter allocation without relying on a build-specific allocation ordinal. - TFTP preserves packet boundaries: the initial response and each readable chunk are separate datagrams.
- SMTP uses the primary connection as command-aligned replies. The initial
response is the 220 greeting, each readable chunk answers the next command,
and after a 354 DATA reply the peer drains the dot-stuffed message body up to
its
\r\n.\r\nend marker before releasing the final 250. Envelope recipients come from the schema-nativemail_rcptlist becauseCURLOPT_MAIL_RCPTis acurl_slist; the URL path supplies curl’s EHLO domain rather than the recipient in current libcurl. AnSCHEME_SMTPSscenario runs the identical exchange over implicit TLS; its fixed authority istls.test, matching the checked-in trust anchor, and the handshake carries no ALPN extension. Plaintext SMTP may select a closedsmtp_probe_modefor optional or required STARTTLS, the adjacent TLS-filter allocation failure, or one fixed command-send backpressure shape. Accepted STARTTLS probes end the plaintext peer with bounded EOF; implicit SMTPS remains the lane’s real successful TLS carrier. - IMAP uses the primary connection as command-aligned replies. The initial
response is the
* OKgreeting, each readable chunk answers the next command, and a status line beginning withOK,NO,BAD, orPREAUTHreceives curl’s generated command tag. A{N}literal copies the next N bytes verbatim, so FETCH bodies and their tagged completion can share one chunk. An accepted APPEND continuation drains exactly the announced literal size. AnSCHEME_IMAPSscenario runs the identical exchange over implicit TLS; its fixed authority istls.test, matching the checked-in trust anchor, and the handshake carries no ALPN extension. IMAP may retain a bounded receive-buffer size to exercise long-command short writes; drain throttling is always cleared. - POP3 also uses command-aligned replies, but its
+OK/-ERRstatus lines need no rewriting. Multi-line CAPA, LIST, and RETR replies include their terminating dot line and are written unchanged. AnSCHEME_POP3Sscenario runs the identical exchange over implicit TLS against the sametls.testauthority and also negotiates no ALPN protocol. POP3 may retain the same recv-buffer-only pressure. Its semantic STLS selector arms one failure immediately after the peer accepts STLS, so it targets filter installation without depending on earlier allocation ordinals. - Gopher normally writes one bounded selector through the stream peer. Its closed probe selector can split that write with no operation deadline, fill the harness-owned socket before a five-millisecond timeout, or select a fixed Gophers authority that reaches the TLS session-key size limit. No protobuf bytes control the environment value, timeout, or oversized host.
- The proxy-transport lane uses request-triggered chunks for each SOCKS reply
and then for tunneled HTTP. SOCKS5 advertises deterministic no-auth and
username/password methods; the scripted method-selection byte chooses the
path.
haproxy_protocolinserts a protocol-v1 prefix after tunnel setup with fixed loopback metadata, never a fuzz-selected endpoint. - SMTP, IMAP, POP3, FTP, FTPS, MQTT, MQTTS, the proxy-transport lane, and deep
HTTP CONNECT retain the opt-in
probe_multi_pollsetfield. When it is true, their drivers call zero-timeoutcurl_multi_pollafter successful perform turns while both the transfer and protocol connection are live. This reaches protocol/filter pollset callbacks without delaying the default false path. Other fixed target policies, including direct, forward-proxy, and Unix-domain deep HTTP, clear the field until their loops have a useful intermediate state to probe. - MQTT treats the primary connection as a binary packet stream. The initial
response is the CONNACK, while each readable chunk is an opaque
server-initiated packet (for example PUBLISH followed by a server
DISCONNECT) released after the peer acknowledges curl’s SUBSCRIBE. Client
packets are delimited by MQTT’s fixed header and base-128 remaining length,
not by newlines; the URL path becomes the publish/subscribe topic and
CURLOPT_POSTFIELDSsupplies the publish payload. AnSCHEME_MQTTSscenario runs the identical packet exchange over implicit TLS; its fixed authority istls.test, matching the checked-in trust anchor, and the handshake carries no ALPN extension. Its closed probe selector can publish a fixed 16 KiB body through a small, incrementally drained socket or install a fixed 65,536-byte username or password. This reaches queued-send and 16-bit credential-limit handling without placing oversized values in the protobuf corpus. - DICT uses the primary connection for one request/response exchange. curl
writes a
CLIENTidentification line, a command derived from the URL path (DEFINEfrom/d:word,MATCHfrom/match:word:db,SHOWfrom/show:db, or a raw command with colons replaced by spaces), andQUITin a single send. It then reads the response as an unframed text stream, so the first non-CLIENT/non-QUITon_readableentry is the complete body and the peer half-closes after writing it. Include the RFC 2229.\r\nterminator in the seed when a well-formed body is desired; curl does not parse it and treats EOF as end-of-transfer.initial_responseis normally empty because DICT has no server greeting. - FILE has no peer. Its generated schema omits
host_pathand retains boundedfile_sourcebytes plus upload scripts and their scalar controls. The runtime places the source in a session-owned anonymous regular file and constructs the completefile:///proc/self/fd/NURL. Mutations therefore exercise curl’s local read, write, range, resume, time-condition, metadata, and upload rate-limit paths without reading or writing an arbitrary host path. - LDAP is BER/ASN.1, not line-oriented.
initial_response, when present, is the scripted bind reply and is released only after curl’s bindRequest with its envelope messageID rewritten to the request id; an empty value asks the peer to synthesize a successful bind. Each readable chunk is one complete server LDAPMessage (for example a searchResEntry) forwarded after the searchRequest and likewise renumbered. A scripted searchResDone ends the search; otherwise the peer synthesizes one. When login options request SASL, the peer recognizes OpenLDAP’s root-DSEsupportedSASLMechanismssearch, advertises bounded PLAIN support, and preserves the readable script for the later URL-derived search. The URL path carries the base DN, attributes, scope, and filter, because this curl revision’s OpenLDAP handler does not consultCURLOPT_CUSTOMREQUEST. AnSCHEME_LDAPSscenario runs the identical BER exchange over implicit TLS. In this curl revision OpenLDAP itself is built without TLS:lib/openldap.cdrives curl’s SSL connection filter and routes OpenLDAP’s sockbuf I/O through curl’s send/receive callbacks, so the in-process peer terminates ordinary TLS records and then parses decrypted LDAPMessages. The fixed authority istls.test, matching the checked-in trust anchor, and the handshake carries no ALPN extension. A plaintextSCHEME_LDAPscenario may instead setldap_starttls: the peer recognizes OpenLDAP’s ExtendedRequest and returns a successful correlated ExtendedResponse before curl installs its TLS filter. The peer intentionally closes the unimplemented upgraded carrier, keeping this transition bounded. - RTSP is request-aligned and has no server greeting. The peer consumes each
request start line and header block, releases the next
on_readableentry only after the terminating empty line, and drains any announcedContent-Lengthbody exactly. A response chunk may carry the status line, headers, an SDP/parameter body, and RFC 2326 interleaved RTP frames ($, channel, 16-bit length, payload).CURLOPT_RTSP_REQUESTselects the method, and the response’s own CSeq stays mutation-controlled so mismatches reach curl’sCURLE_RTSP_CSEQ_ERRORpath. Closed probe modes repeat OPTIONS first at the same origin and then at a fixed second origin to exercise cached-connection and learned-Session handling, or split an invalid RTP channel at a fixed receive boundary and fail only the resulting one-byte body write. - IPFS and IPNS are gateway schemes rather than wire protocols. libcurl has no
ipfs://handler; curl’s command-line tool rewritesipfs://<cid>andipns://<name>to<gateway>/ipfs/<cid>or<gateway>/ipns/<name>before issuing an ordinary HTTP request. The structured lane mirrors that rewrite against a fixed loopback gateway and the in-process HTTP peer.host_pathholds a pinned CID/name followed by the mutation-controlled gateway path, query, and fragment; the runner prepends the gateway authority. - The resolver lane’s
dohsettings bind a loopback DoH responder and pointCURLOPT_DOH_URLat it.transport_modepreserves the historical HTTP endpoint by default or selects an HTTPS endpoint backed by a dedicated trust-anchor-verified HTTP/1.1 TLS peer. Normal response mode answers address and HTTPS queries; its HTTPS record carriesalpn=h2,h3, so the separate outer HTTPS transfer completes through the in-process TLS/ALPN h2 peer usinghttp2_plan. Malformed-DNS mode returns HTTP 200 with a one-byte DNS body, HTTP-error mode returns a non-success status before curl can decode DNS bytes, and transport-error mode closes only after receiving a complete DoH request so curl records the internal transfer failure. Non-defaultoption_probe_modevalues force HTTPS and install one closed, harness-owned parent-option fixture: a non-global diagnostic stream, procfs-backed CAINFO, CAPATH, and CRLFILE pathnames, or a no-op SSL_CTX callback and data pointer. Separate modes keep the deliberately invalid pathname fixture from masking the callback’s successful end-to-end propagation.CURLOPT_RESOLVEis cleared because it cannot carry SVCB/HTTPS records. A paired coverage seed repeats that exact exchange withtrace_config: "all"; tracing is reset at the input boundary and does not become a resolver-lane default. An ECH seed releases the address answers first and delays the HTTPS answer by one deterministic drive turn, covering TLS setup while the record is pending without relying on scheduling or wall-clock timing. - The resolver lane’s non-default
resolver_modevalues select fixed, target-owned states. Positive and negative threaded lookups, plus a scope-less link-local result and AF_UNSPEC retry, use only the resolver binary’s localgetaddrinfobackend. The threaded modes receive bounded poll/wait turns; the partial backend intentionally staggers A behind AAAA so curl consumes a live partial result instead of relying on scheduler luck. Queued-cancellation mode limits the public resolver pool to one worker, holds its first lookup while a duplicated easy queues later work, cancels that duplicate, then observes the worker’s publicTHREADSidle trace after releasing the gate. The observation proves entry to the configured timed condition wait without using elapsed time as the coverage contract. Unix-socket mode passes/curl-fuzzer-resolver.sockto curl’s address builder, but the open-socket callback replaces it with an anonymous peer and never opens that filesystem path. DoH error settings enable silent typed diagnostics, distinguishing DNS decode failures from HTTP failures without writing per-input logs. The global-lifecycle mode finishes a localhost transfer, calls the public global cleanup entrypoint only after all resolver workers are gone, and reinstalls the harness allocator callbacks before another input runs. - HTTP/2 origin lanes treat connection bytes as raw HTTP/2 frames after a
fixed prior-knowledge or TLS/ALPN setup. The HTTPS CONNECT proxy does the
same when
connection.proxy_protocolis zero; its explicit HTTP/1 profile instead treats the script as a bounded CONNECT response and always uses a tunnel. The H2 profile can select either CONNECT tunnelling or direct absolute-form forwarding withh2_forward_proxy. The fast HTTP lane retains the HTTP/1.1 h2c Upgrade request and101transition instead. - Direct HTTP/3 uses ordered
http3_planactions after a valid QUIC/TLS handshake and discards the ordinary connection script. Settinghttp3_plan.use_h1_connect_udp_proxyinstead usesconnectionfor a local HTTP/1.1 CONNECT-UDP upgrade and capsule bytes; direct HTTP/3 actions are cleared.
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.
Select allocation failures
All structured targets can reject selected libcurl allocation attempts while driving a transfer:
fail_allocations: 5
fail_allocations: 9
Ordinals are one-based and count positive-size calls on the fuzzer thread
through the callbacks installed by curl_global_init_mem. Scenario parsing
stays outside the sequence; libcurl setup, transfer driving, result probes,
and cleanup inside the shared scenario runner are included except for narrow
harness ownership scopes. The COPYPOSTFIELDS ownership commit and
pushed-handle cleanup enumeration neither fail nor advance the ordinal counter,
so failures remain pending for subsequent injectable allocations. Fixed target
policies retain the four smallest distinct values after folding them into
1..4096; the compatibility target applies that bound at execution time without
rewriting the message. State-dependent probes still require the preceding
transfer entrypoint to establish their documented runtime state.
In the API target, a scenario that combines API_DRIVE_EASY_EVENTS with an
allocation plan is normalized to API_DRIVE_EASY_PERFORM. After its
heap-backed socket-monitor allocation fails, curl’s current debug event adapter
returns a status outside the socket-callback contract; event-based driving
remains available when no allocation failure is selected.
An active plan can combine a MIME body with CURLOPT_CRLF or
CURLOPT_TRANSFERTEXT, allowing allocation injection across curl’s composed
MIME and line-conversion reader stack. curl clears partially installed readers
when that setup fails.
Multiple failures describe one evolving execution. In the example, 9 means the ninth allocation attempt on the path produced after attempt 5 failed, not the ninth attempt from a failure-free baseline. Keep failure seeds with the target that produced them: allocation sequences can change with the target, curl revision, build configuration, and TLS backend. Resolver and peer worker threads deliberately do not inherit the calling thread’s plan.
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 full authoring
Scenariowith ordinaryprotoc --encodeand creates<name>.scenarioin the corpus assigned to that target. The target’s generated root preserves retained field tags and discards fields outside its profile when it parses the file. - 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. The decoder embeds the
metadata-free full authoring view prepared from the annotated schema, so it can
inspect any current profile payload without loading eligibility annotations.
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. The typed
option cutover changed the existing format in place: pre-cutover generic-option
binaries can parse misleadingly under a current schema and are not detected
automatically. Reproduce them with their original revision and the target that
produced them. Current checked-in seeds are ordinary authoring
textprotos encoded directly by protoc; there is no separate per-profile seed
encoder.