yams/tools/fuzzing/fuzz.sh
trvon 2a63eb1af8
test(fuzz): add a direct-P2P fuzz lane and a topology codec harness
New libFuzzer/AFL++ harnesses, each trapping on an oracle violation:
p2p_json (nesting bound, round trip), p2p_connstr (endpoint round trip,
pin idempotence, no silent re-pin), p2p_handshake (validators and the
full acceptor bind the TLS peer), p2p_delta_stream (batch, session and
bootstrap receive paths stay inside count and byte budgets), p2p_inbound
(the manager inbound session through registry update) and
topology_codec (decoded snapshots re-encode losslessly). p2p_delta now
draws its exchange limits from the input.

p2p_seeds.py writes framed-session seeds for FuzzedDataProvider layouts,
dicts/p2p.dict carries the frame vocabulary, generate_corpus.sh accepts a
local seedgen, and fuzz.sh no longer fails on macOS bash 3.2 when a
target has no dictionary. The generated corpus stays untracked.
2026-09-22 13:55:04 -06:00

168 lines
5.4 KiB
Bash
Executable file

#!/bin/bash
# SPDX-License-Identifier: GPL-3.0-or-later
# Wrapper script for AFL++ fuzzing with Docker
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
IMAGE_NAME="yams-fuzz"
usage() {
cat <<EOF
Usage: $0 <command> [args...]
Commands:
build Build Docker image with AFL++ fuzzers
fuzz <target> Run AFL++ fuzzer in Docker (e.g., p2p_protocol, p2p_delta, ipc_protocol)
local <target> [libFuzzer args...]
Run a local libFuzzer build over data/fuzz/corpus/<target>
exec <command> Run a command inside the fuzzing container
shell Open interactive shell in container
clean Remove Docker image
Local libFuzzer build (clang with the libFuzzer runtime, e.g. Homebrew or apt LLVM; Apple clang
does not ship it). Every project object gets -fsanitize=fuzzer-no-link plus ASan/UBSan:
CC=clang CXX=clang++ meson setup build/fuzz -Dbuild-fuzzers=true \\
-Dbuild-tests=false -Dbuild-plugins=false -Dbuild-cli=false
meson compile -C build/fuzz
./tools/fuzzing/generate_corpus.sh build/fuzz/tools/fuzzing/seedgen
$0 local p2p_handshake -max_total_time=120
On macOS with Homebrew LLVM, also export BOOST_ROOT=\$(brew --prefix) before meson setup.
FUZZ_BUILD_DIR selects another build directory (default: build/fuzz). New corpus units and
crash-* artifacts land in <build-dir>/fuzz-runs/<target>/. A dictionary is passed automatically
when tools/fuzzing/dicts/<target>.dict exists (p2p_* targets fall back to dicts/p2p.dict).
Examples:
$0 build
AFL_FUZZ_SECONDS=60 $0 fuzz p2p_protocol
$0 local p2p_json -max_total_time=60
$0 shell
EOF
exit 1
}
# Print the dictionary for a target (relative to tools/fuzzing), or nothing when none exists.
dict_for_target() {
local target="$1"
if [[ -f "${SCRIPT_DIR}/dicts/${target}.dict" ]]; then
echo "dicts/${target}.dict"
elif [[ "$target" == p2p_* && -f "${SCRIPT_DIR}/dicts/p2p.dict" ]]; then
echo "dicts/p2p.dict"
fi
}
cmd_build() {
echo "Building fuzzer Docker image..."
docker build \
-f "${SCRIPT_DIR}/Dockerfile" \
-t "${IMAGE_NAME}" \
"${PROJECT_ROOT}"
}
cmd_fuzz() {
local target="${1:-}"
if [[ -z "$target" ]]; then
echo "Error: target required (example: ipc_protocol, ipc_roundtrip, proto_serializer)"
exit 1
fi
# AFL++ uses a per-fuzzer subdirectory inside -o; without -M/-S it defaults to "default".
# If you have multiple terminals, "default" will collide. Use a unique ID by default.
local fuzzer_id="${AFL_FUZZER_ID:-$(hostname)-$$}"
local fuzzer_bin="/src/build/fuzzing/tools/fuzzing/fuzz_${target}"
local corpus_dir="/fuzz/corpus/${target}"
local findings_dir="/fuzz/findings/${target}"
local duration_args=()
if [[ -n "${AFL_FUZZ_SECONDS:-}" ]]; then
if [[ ! "${AFL_FUZZ_SECONDS}" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: AFL_FUZZ_SECONDS must be a positive integer"
exit 1
fi
duration_args=(-V "${AFL_FUZZ_SECONDS}")
fi
local tty_arg=""
if [[ -t 0 && -t 1 ]]; then
tty_arg="-it"
fi
# Ensure mounted host directories exist for AFL input/output.
mkdir -p "${PROJECT_ROOT}/data/fuzz/corpus/${target}"
mkdir -p "${PROJECT_ROOT}/data/fuzz/findings/${target}"
local dict_args=()
local dict
dict="$(dict_for_target "$target")"
if [[ -n "$dict" ]]; then
dict_args=(-x "/src/tools/fuzzing/${dict}")
fi
echo "Running AFL++ fuzzer for target: ${target}"
docker run --rm ${tty_arg:+"${tty_arg}"} \
-v "${PROJECT_ROOT}/data/fuzz:/fuzz" \
-e AFL_AUTORESUME=1 \
"${IMAGE_NAME}" \
afl-fuzz -S "${fuzzer_id}" -i "${corpus_dir}" -o "${findings_dir}" -m none \
${dict_args[@]+"${dict_args[@]}"} "${duration_args[@]}" "${fuzzer_bin}"
}
cmd_local() {
local target="${1:-}"
if [[ -z "$target" ]]; then
echo "Error: target required (example: p2p_json, p2p_handshake, topology_codec)"
exit 1
fi
shift
local build_dir="${FUZZ_BUILD_DIR:-${PROJECT_ROOT}/build/fuzz}"
local fuzzer_bin="${build_dir}/tools/fuzzing/fuzz_${target}"
if [[ ! -x "$fuzzer_bin" ]]; then
echo "Error: ${fuzzer_bin} is not built; see '$0' usage for the libFuzzer build"
exit 1
fi
local seeds="${PROJECT_ROOT}/data/fuzz/corpus/${target}"
local run_dir="${build_dir}/fuzz-runs/${target}"
mkdir -p "${run_dir}/corpus" "${run_dir}/artifacts" "${seeds}"
local dict_args=()
local dict
dict="$(dict_for_target "$target")"
if [[ -n "$dict" ]]; then
dict_args=("-dict=${SCRIPT_DIR}/${dict}")
fi
echo "Running libFuzzer target ${target} (seeds: ${seeds})"
"$fuzzer_bin" ${dict_args[@]+"${dict_args[@]}"} "-artifact_prefix=${run_dir}/artifacts/" "$@" \
"${run_dir}/corpus" "${seeds}"
}
cmd_exec() {
if [[ $# -eq 0 ]]; then
echo "Error: command required"
exit 1
fi
docker run --rm -ti \
-v "${PROJECT_ROOT}/data/fuzz:/fuzz" \
"${IMAGE_NAME}" "$@"
}
cmd_shell() {
docker run --rm -ti "${IMAGE_NAME}" /bin/bash
}
cmd_clean() {
docker rmi "${IMAGE_NAME}"
}
[[ $# -eq 0 ]] && usage
case "$1" in
build) cmd_build ;;
fuzz) shift; cmd_fuzz "$@" ;;
local) shift; cmd_local "$@" ;;
exec) shift; cmd_exec "$@" ;;
shell) cmd_shell ;;
clean) cmd_clean ;;
*) usage ;;
esac