Fuzzing Engine Integration
This page covers how the harness integrates with each supported fuzzing engine. For the harness internals (filter replacement, fake connections, input handling), see Harness Design.
LibFuzzer with libprotobuf-mutator
The framework uses LibFuzzer with libprotobuf-mutator (LPM) for structure-aware fuzzing. Instead of mutating raw bytes, LPM mutates protobuf messages that describe HTTP requests, then a converter translates each message into raw HTTP bytes before feeding it to Apache.
Why protobuf?
Raw byte mutation is bad at producing valid HTTP requests. Most mutations break the request line or headers, and Apache rejects them before reaching any module code. Protobuf-based mutation operates on structured fields (method, URI, headers, body) independently, producing syntactically valid requests that exercise deeper code paths.
Architecture
Each proto harness has three layers:
LibFuzzer -> LPM (mutates protobuf message) -> Converter (proto -> raw HTTP) -> fuzz_one_input()
%%{init: {"flowchart": { "nodeSpacing": 20, "rankSpacing": 30}}}%%
flowchart TD
Start["LibFuzzer starts harness"] --> Init["proto_harness_init()<br/>Apache initialization<br/>(once per process)"]
Init --> Loop["DEFINE_PROTO_FUZZER()<br/>called with mutated protobuf"]
Loop e1@==> Convert["Converter<br/>BuildHttpRequest() +<br/>module-specific transforms"]
Convert e2@==> Process["fuzz_one_input()<br/>inject into Apache pipeline"]
Process e3@==> Loop
e1@{ animate: true }
e2@{ animate: true }
e3@{ animate: true }
The proto harness entry point
LPM provides the DEFINE_PROTO_FUZZER macro which replaces LibFuzzer’s LLVMFuzzerTestOneInput. It automatically handles deserialization and structure-aware mutation:
DEFINE_PROTO_FUZZER(const SessionCryptoRequest &request)
{
if (!proto_harness_init())
return;
std::string raw = BuildHttpRequest(request.http());
ApplySessionCrypto(request.cookie(), request.route(), raw);
fuzz_one_input(raw.data(), raw.size());
}
proto_harness_init()- initializes Apache once (config parsing, module hooks, memory pools). ReadsFUZZ_CONFandFUZZ_ROOTenvironment variables.BuildHttpRequest()- converts the protobufHttpRequestmessage into a raw HTTP request string (method line, headers, body).Module-specific transforms (e.g.
ApplySessionCrypto()) - apply module-specific mutations like encrypting session cookies, constructing multipart boundaries, or injecting rewrite-targeted URIs.fuzz_one_input()- injects the raw bytes into Apache’s bucket brigade and runs the full request pipeline.
Proto schemas
Each harness declares its proto dependencies via @protos and @converters tags (see Protobuf Harness Compilation in the building chapter). Available schemas:
Proto |
Message |
Used by |
|---|---|---|
|
|
All harnesses (base HTTP fields) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Seeds
LPM accepts seeds in .textproto (human-readable) or binary protobuf format. Text seeds are easier to write and review:
# fuzz-seeds/basic.textproto
http {
method: "GET"
uri: "/"
headers { key: "Host" value: "localhost" }
}
Binaries
The build produces two binaries:
fuzz_harness_libfuzzer- linked against the-lftree with SanCov instrumentation. Used for fuzzing.fuzz_harness_coverage- linked against the-covtree with LLVM coverage instrumentation. Used for crash triage and coverage reports.
AFL++ (deprecated - removed in v0.2.0-alpha)
Important
AFL++ support was removed in v0.2.0-alpha. The section below is kept for reference but is not functional. AFL++ support may be re-added in a future release.
The harness used AFL++’s persistent mode for maximum throughput:
Process startup: Apache initialization (config parsing, module hooks, memory pools) - expensive, happens once.
Fork server:
__AFL_INIT()establishes the fork server. AFL++ uses the initialized process as a template.Persistent loop:
__AFL_LOOP(10000)reuses the same forked process for 10,000 inputs before exiting and forking fresh.
The implementation used shared-memory test cases to eliminate file I/O overhead:
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(10000)) {
int len = __AFL_FUZZ_TESTCASE_LEN;
fuzz_one_input((const char *)buf, len);
}
#endif
Configuration
The harness loads Apache configuration using the same mechanism as regular httpd:
Server root (
FUZZ_ROOTenv var or-dflag): Base directory for relative paths in the configConfig file (
FUZZ_CONFenv var or-fflag): The Apache configuration to loadStatic modules: All modules are compiled into the binary, so no
LoadModuledirectives are needed for built-in modules
Minimal fuzzing config:
ServerName localhost:80
HttpProtocolOptions Unsafe # Relax strict HTTP parsing for fuzz input
RequestReadTimeout handshake=0 header=0 body=0 # No timeouts (no real socket)
DocumentRoot "/tmp/htdocs"
<Directory "/">
Require all granted # No authentication checks
</Directory>
HttpProtocolOptions Unsafe is important - without it, Apache’s strict HTTP parser rejects many fuzz inputs before they reach any module code. Since we’re fuzzing for memory safety bugs (not protocol compliance), relaxing the parser maximizes code coverage.
ASan Integration
When built with AddressSanitizer, the harness needs special handling:
Signal handler restoration: ASan installs its own signal handlers (SIGSEGV, SIGBUS, etc.) but Apache overwrites them during initialization. The harness saves and restores ASan’s handlers after Apache init so crashes are properly reported.
Pool debug mode:
--enable-pool-debug=yesmakesapr_palloc()use directmalloc()so ASan can track individual allocations. See the memory pools chapter for details.Coverage flush:
fuzz_exit()calls__llvm_profile_write_file()before_exit()to flush coverage data, since Apache’smod_watchdogthreads can deadlock during normalatexitcleanup.