test: Improve AFL++ fuzzing infrastructure#6618
Conversation
There was a problem hiding this comment.
Pull request overview
This PR significantly improves Dragonfly's AFL++ fuzzing infrastructure to make it more effective at finding bugs and easier to reproduce crashes. The changes implement a RESP-aware custom mutator, expand the seed corpus from 17 to 79 files covering all command families, improve crash replay capabilities, and optimize AFL++ configuration for better coverage stability and crash reproducibility.
Changes:
- Added custom RESP mutator that operates at command/argument level instead of random bytes
- Expanded seed corpus to 79 files covering all Redis command families (string, list, hash, set, zset, stream, JSON, search, bloom, geo, HLL, bitops, scripting, ACL, pub/sub, transactions, server ops)
- Improved AFL++ configuration with persistent record for full crash replay, optimized bitmap size, and better system configuration
- Added crash replay tooling (
replay_crash.py,package_crash.sh) for reproducing and sharing crashes - Reduced fuzz client socket timeout from 2s to 200ms and added MSG_NOSIGNAL to prevent SIGPIPE
- Enhanced dictionary with 200+ tokens while removing duplicates and oversized entries
- Updated documentation with comprehensive fuzzing instructions
Reviewed changes
Copilot reviewed 69 out of 69 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/server/dfly_main.cc | Reduced socket timeout to 200ms, added MSG_NOSIGNAL flag |
| fuzz/resp_mutator.py | AFL++ custom mutator for RESP protocol with 150+ commands |
| fuzz/run_fuzzer.sh | AFL++ wrapper script with system configuration and environment setup |
| fuzz/replay_crash.py | Tool to replay crashes from AFL_PERSISTENT_RECORD files |
| fuzz/package_crash.sh | Script to package crashes for sharing with developers |
| fuzz/seeds/resp/* | 79 seed files covering all command families |
| fuzz/dict/resp.dict | Enhanced dictionary with duplicates removed |
| fuzz/FUZZING.md | Comprehensive fuzzing documentation |
| """ | ||
|
|
||
| import random | ||
| import struct |
There was a problem hiding this comment.
Import of 'struct' is not used.
| import struct |
| except Exception: | ||
| pass | ||
|
|
||
| try: | ||
| s.recv(4096) | ||
| except Exception: | ||
| pass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| pass | |
| try: | |
| s.recv(4096) | |
| except Exception: | |
| pass | |
| except Exception as e: | |
| sys.stderr.write(f"\033[0;31m[WARN]\033[0m Failed to send data: {e}\n") | |
| try: | |
| s.recv(4096) | |
| except Exception as e: | |
| sys.stderr.write(f"\033[0;31m[WARN]\033[0m Failed to receive response: {e}\n") |
| except Exception: | ||
| pass | ||
|
|
||
| try: | ||
| s.recv(4096) | ||
| except Exception: | ||
| pass |
There was a problem hiding this comment.
'except' clause does nothing but pass and there is no explanatory comment.
| except Exception: | |
| pass | |
| try: | |
| s.recv(4096) | |
| except Exception: | |
| pass | |
| except OSError as exc: | |
| # Ignore send errors: the fuzzer only needs best-effort delivery. | |
| print(f"\033[0;33m[WARN]\033[0m Send failed: {exc}", file=sys.stderr) | |
| try: | |
| s.recv(4096) | |
| except OSError as exc: | |
| # Ignore receive errors: crash reproduction only depends on what was sent. | |
| print(f"\033[0;33m[WARN]\033[0m Receive failed: {exc}", file=sys.stderr) |
🤖 Augment PR SummarySummary: This PR expands Dragonfly’s AFL++ fuzzing infrastructure to improve coverage and make crashes easier to reproduce/share. Changes:
Technical Notes: The focus is on higher-quality inputs (RESP-aware mutations), more stable coverage (1 thread + larger map), and reproducible stateful crashes via persistent RECORD replay. 🤖 Was this summary useful? React with 👍 or 👎 |
| KEYS | ||
| $1 | ||
| * | ||
| *3 |
There was a problem hiding this comment.
This seed appears to contain invalid RESP array counts (e.g., the SCAN request starts with *3 but includes 5 bulk strings), which likely makes the seed rejected by the RESP parser and reduces initial corpus usefulness.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| $3 | ||
| val | ||
| *2 | ||
| $2 |
| tk | ||
| $3 | ||
| val | ||
| *1 |
| (b"MULTI", 0, 0), (b"EXEC", 0, 0), (b"DISCARD", 0, 0), | ||
| (b"WATCH", 1, 3), (b"UNWATCH", 0, 0), | ||
| # Script | ||
| (b"EVAL", 2, 6), (b"EVALSHA", 2, 6), (b"EVALRO", 2, 6), |
There was a problem hiding this comment.
Dragonfly registers the readonly scripting commands as EVAL_RO/EVALSHA_RO, but the mutator uses EVALRO and doesn’t include EVALSHA_RO; this likely generates unknown commands and reduces scripting coverage.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| if (connect(s, (struct sockaddr*)&a, sizeof(a)) == 0) { | ||
| send(s, data, len, 0); | ||
| // Just read once - don't wait for full response | ||
| send(s, data, len, MSG_NOSIGNAL); |
Expand seed corpus, add RESP-aware custom mutator, and improve fuzzing tooling.
Changes:
Custom mutator (
fuzz/resp_mutator.py):Seed corpus (
fuzz/seeds/resp/):Fuzzing scripts (
fuzz/run_fuzzer.sh):AFL_PERSISTENT_RECORDwithafl_loop_limit(default 10000) for full crash replayAFL_MAP_SIZE) to reduce hash collisionsDictionary (
fuzz/dict/resp.dict):Crash tooling:
replay_crash.py— replays RECORD files against a running instancepackage_crash.sh— packages crash + RECORD files into a self-contained archiveMinor (
src/server/dfly_main.cc):MSG_NOSIGNALto avoid SIGPIPECloses: #5713
Closes: #5710
Closes: #5708