usbip-rs/flake.nix
Davíð Steinn Geirsson bb3c603172 feat(fuzz): add AFL++ with cargo-afl fuzzing support
Add a second fuzzing engine alongside the existing libFuzzer/cargo-fuzz
setup. AFL++ runs with persistent mode (afl::fuzz! macro), LLVM plugins
(CmpLog, IJON), and a SymCC concolic companion for hybrid fuzzing.

- cargo-afl built from afl.rs with a patch for CARGO_AFL_DIR /
  CARGO_AFL_LLVM_DIR env-var overrides
- AFL++ built with LLVM 22 plugins to match rust-nightly
- Persistent-mode fuzz targets in lib/fuzz-afl/
- --jobs N parallel fuzzing: main instance in foreground, secondaries
  and SymCC companion as systemd transient units in a slice
- Ctrl+c / exit cleans up all background processes via slice stop
- AFL_AUTORESUME=1 for clean restarts after previous runs
- fuzz-clean-afl collects crashes from all instance directories
- Shared harness logic in lib/src/fuzz_harness.rs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 00:33:34 +00:00

443 lines
17 KiB
Nix

{
description = "USB/IP server library and CLI tool";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
nativeBuildInputs = with pkgs; [
rustc
cargo
rustfmt
clippy
pkg-config
];
buildInputs = with pkgs; [
libusb1
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
udev
];
usbip-rs = pkgs.rustPlatform.buildRustPackage {
pname = "usbip-rs";
version = "0.8.0";
src = self;
cargoHash = "sha256-ynwLW2FfZxr16KHaDgVUk8DlrXu5dKwS4pk1Rdo2jso=";
inherit nativeBuildInputs buildInputs;
buildFeatures = [ "usbip-rs/serde" ];
meta = with pkgs.lib; {
description = "USB/IP server library and CLI tool";
homepage = "https://github.com/jiegec/usbip";
license = licenses.mit;
mainProgram = "usbip-rs";
};
};
rust-nightly = rust-overlay.packages.${system}.rust-nightly;
in
{
packages = {
default = usbip-rs;
inherit usbip-rs;
# AFL++ built with LLVM 22 plugins to match rust-nightly's LLVM.
# Plugins (CmpLog, IJON, SanitizerCoveragePCGUARD, etc.) are LLVM
# passes loaded via rustc's -Z llvm-plugins flag and must be
# compiled against the same LLVM major version.
aflplusplus = (pkgs.aflplusplus.override {
clang = pkgs.llvmPackages_22.clang;
llvm = pkgs.llvmPackages_22.libllvm.dev;
llvmPackages = pkgs.llvmPackages_22;
}).overrideAttrs (old: {
version = "4.35c-unstable-2026-03-26";
src = pkgs.fetchFromGitHub {
owner = "AFLplusplus";
repo = "AFLplusplus";
rev = "e5a8ba39ecf97d05e286fdd4e01da96554dbf64f";
hash = "sha256-QtGazGShjybvjOONoWjqSg/c+l5sPpaFuuTI2S85YQM=";
};
# The performance test script exits non-zero when skipped in
# sandboxed builds. Disable installCheck since we only need
# the binaries and plugins.
doInstallCheck = false;
});
cargo-afl = pkgs.rustPlatform.buildRustPackage {
pname = "cargo-afl";
version = "0.17.1-unstable-2026-03-26";
src = pkgs.fetchFromGitHub {
owner = "rust-fuzz";
repo = "afl.rs";
rev = "644c06a7dd8330db92d987bf1efa9d7a6cf2e3c1";
hash = "sha256-wxUL++xRhTIRQ6v0acuJ9OZQkem5HhQllfulKId85X4=";
};
cargoHash = "sha256-aCz6zG9PwhoyEXNo+qvuiBIVIQ14XEs6mTj2dbHvNYY=";
patches = [ ./nix/cargo-afl-env-paths.patch ];
cargoBuildFlags = [ "-p" "cargo-afl" ];
doCheck = false;
};
symcc = pkgs.symcc.overrideAttrs (old: {
version = "1.0-unstable-2026-03-26";
src = pkgs.fetchFromGitHub {
owner = "eurecom-s3";
repo = "symcc";
rev = "3b8acabf06c83b92facccde7f6dfb191b1a163b3";
hash = "sha256-k87bwp2dAFga/5ui8fepJ/ZIHl4WTyVFhfgvTLBxP6c=";
fetchSubmodules = true;
};
});
};
checks = {
default = usbip-rs;
};
devShells = {
default = pkgs.mkShell {
inherit nativeBuildInputs buildInputs;
};
fuzz = pkgs.mkShell {
buildInputs = [
rust-nightly
pkgs.cargo-fuzz
pkgs.libusb1
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
pkgs.udev
];
nativeBuildInputs = [ pkgs.stdenv.cc pkgs.pkg-config ];
};
fuzz-afl = pkgs.mkShell {
buildInputs = [
rust-nightly
self.packages.${system}.cargo-afl
self.packages.${system}.symcc
pkgs.libusb1
] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [
pkgs.udev
];
nativeBuildInputs = [ pkgs.stdenv.cc pkgs.pkg-config ];
CARGO_AFL_DIR = "${self.packages.${system}.aflplusplus}";
CARGO_AFL_LLVM_DIR = "${self.packages.${system}.aflplusplus}/lib/afl";
};
};
apps = let
fuzz-env = ''
export PATH="${rust-nightly}/bin:${pkgs.cargo-fuzz}/bin:${pkgs.stdenv.cc}/bin:${pkgs.pkg-config}/bin:${pkgs.coreutils}/bin:$PATH"
export PKG_CONFIG_PATH="${pkgs.libusb1.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '':${pkgs.udev.dev}/lib/pkgconfig''}
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/lib"
'';
fuzz-cargo = pkgs.writeShellScriptBin "fuzz-cargo" ''
set -euo pipefail
${fuzz-env}
if [ $# -eq 0 ]; then
cargo fuzz list --fuzz-dir fuzz-cargo
else
target="$1"
shift
fork=0
args=()
for arg in "$@"; do
case "$arg" in
--fork=*) fork=''${arg#--fork=} ;;
*) args+=("$arg") ;;
esac
done
if [ "$fork" -gt 0 ]; then
while true; do
cargo fuzz run --fuzz-dir fuzz-cargo "$target" -- -max_len=1048576 "-fork=$fork" "''${args[@]}" || true
echo "--- fuzzer exited, restarting (artifacts saved) ---"
done
else
cargo fuzz run --fuzz-dir fuzz-cargo "$target" -- -max_len=1048576 "''${args[@]}"
fi
fi
'';
fuzz-clean-cargo = pkgs.writeShellScriptBin "fuzz-clean-cargo" ''
set -euo pipefail
${fuzz-env}
if [ $# -eq 0 ]; then
echo "Usage: fuzz-clean-cargo <target>"
echo "Available targets:"
cargo fuzz list --fuzz-dir fuzz-cargo
exit 1
fi
target="$1"
dir="fuzz-cargo/artifacts/$target"
if [ ! -d "$dir" ]; then
echo "No artifacts directory: $dir"
exit 0
fi
shopt -s nullglob
files=("$dir"/crash-* "$dir"/oom-* "$dir"/timeout-*)
if [ ''${#files[@]} -eq 0 ]; then
echo "No artifacts to test."
exit 0
fi
echo "Building $target..."
if ! cargo fuzz build --fuzz-dir fuzz-cargo "$target" 2>&1; then
echo "Build failed not touching artifacts."
exit 1
fi
echo "Testing ''${#files[@]} artifacts for $target..."
removed=0
kept=0
for f in "''${files[@]}"; do
if timeout 30 cargo fuzz run --fuzz-dir fuzz-cargo "$target" "$f" -- -max_len=1048576 >/dev/null 2>&1; then
rm "$f"
removed=$((removed + 1))
else
kept=$((kept + 1))
fi
echo -ne "\r tested $((removed + kept))/''${#files[@]}, removed $removed, kept $kept"
done
echo ""
echo "Done: removed $removed fixed, kept $kept still-crashing."
'';
gen-fuzz-corpus = pkgs.writeShellScriptBin "gen-fuzz-corpus" ''
set -euo pipefail
export PATH="${pkgs.stdenv.cc}/bin:$PATH"
root="$(${pkgs.git}/bin/git rev-parse --show-toplevel)"
${pkgs.rustc}/bin/rustc "$root/lib/fuzz-cargo/gen_corpus.rs" -o /tmp/gen-fuzz-corpus
cd "$root/lib/fuzz-cargo"
/tmp/gen-fuzz-corpus
cd "$root/lib/fuzz-afl"
/tmp/gen-fuzz-corpus
'';
afl-env = ''
export CARGO_AFL_DIR="${self.packages.${system}.aflplusplus}"
export CARGO_AFL_LLVM_DIR="${self.packages.${system}.aflplusplus}/lib/afl"
export PATH="${self.packages.${system}.cargo-afl}/bin:${rust-nightly}/bin:${self.packages.${system}.symcc}/bin:${pkgs.stdenv.cc}/bin:${pkgs.pkg-config}/bin:${pkgs.coreutils}/bin:$PATH"
export PKG_CONFIG_PATH="${pkgs.libusb1.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"${pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '':${pkgs.udev.dev}/lib/pkgconfig''}
root="$(${pkgs.git}/bin/git rev-parse --show-toplevel)"
'';
symcc-companion = pkgs.writeShellScriptBin "symcc-companion" ''
set -uo pipefail
output_dir="$1"
symcc_binary="$2"
symcc_workdir="$output_dir/symcc"
logfile="$symcc_workdir/companion.log"
mkdir -p "$symcc_workdir/queue" "$symcc_workdir/tmp"
log() { echo "[symcc $(date +%H:%M:%S)] $*" >> "$logfile"; }
log "companion started (pid $$)"
# Wait for any AFL++ instance to create a queue directory
while ! ls -d "$output_dir"/*/queue >/dev/null 2>&1; do sleep 1; done
log "AFL++ queue detected"
processed="$symcc_workdir/.processed"
touch "$processed"
shopt -s nullglob
while true; do
# Process queue entries from all AFL++ instances
for f in "$output_dir"/*/queue/id:*; do
[ -f "$f" ] || continue
# Track by full path to avoid collisions across instances
grep -qxF "$f" "$processed" 2>/dev/null && continue
rm -rf "$symcc_workdir/tmp"/*
SYMCC_OUTPUT_DIR="$symcc_workdir/tmp" \
${pkgs.coreutils}/bin/timeout 30 "$symcc_binary" < "$f" >/dev/null 2>&1 || true
new_count=0
for new in "$symcc_workdir/tmp"/*; do
[ -f "$new" ] || continue
# Write to symcc/queue/ AFL++ auto-syncs from all
# instance directories under the output dir
cp "$new" "$symcc_workdir/queue/id:symcc_''${RANDOM}_''${RANDOM}" 2>/dev/null || true
new_count=$((new_count + 1))
done
log "processed $(basename "$f") -> $new_count new inputs"
echo "$f" >> "$processed"
done
sleep 5
done
'';
fuzz-afl = pkgs.writeShellScriptBin "fuzz-afl" ''
set -euo pipefail
${afl-env}
manifest="$root/lib/fuzz-afl/Cargo.toml"
if [ $# -eq 0 ]; then
echo "Available targets:"
${pkgs.gawk}/bin/awk '/^\[\[bin\]\]/{found=1} found && /^name = "/{gsub(/"/, "", $3); print " " $3; found=0}' "$manifest"
exit 0
fi
target="$1"
shift
# Parse --jobs=N or --jobs N from arguments
jobs=1
afl_args=()
skip_next=false
for arg in "$@"; do
if $skip_next; then jobs="$arg"; skip_next=false; continue; fi
case "$arg" in
--jobs=*) jobs="''${arg#--jobs=}" ;;
--jobs) skip_next=true ;;
*) afl_args+=("$arg") ;;
esac
done
afl_target_dir="$root/lib/fuzz-afl/target/afl"
symcc_target_dir="$root/lib/fuzz-afl/target/symcc"
echo "Building $target with cargo-afl (plugins + CmpLog)..."
CARGO_TARGET_DIR="$afl_target_dir" \
cargo afl build --manifest-path "$manifest" --release --bin "$target"
echo "Building $target with SymCC instrumentation..."
CC=symcc CXX=sym++ RUSTFLAGS="-C linker=symcc -Clink-arg=$CARGO_AFL_LLVM_DIR/afl-compiler-rt.o" \
CARGO_TARGET_DIR="$symcc_target_dir" \
cargo build --manifest-path "$manifest" --release --bin "$target"
corpus_dir="$root/lib/fuzz-afl/corpus/$target"
output_dir="$root/lib/fuzz-afl/output/$target"
mkdir -p "$corpus_dir" "$output_dir"
# Ensure corpus has at least one seed
if [ -z "$(ls -A "$corpus_dir" 2>/dev/null)" ]; then
echo "Warning: corpus dir is empty, creating minimal seed"
printf '\x00' > "$corpus_dir/seed-minimal"
fi
# Use a systemd slice for proper process management.
# All background fuzzers and the SymCC companion run as
# transient systemd units; stopping the slice kills them all.
slice="fuzz_afl_$target"
${pkgs.systemd}/bin/systemctl --user stop "$slice.slice" 2>/dev/null || true
# Clean stale AFL++ lock files from killed previous runs
rm -f "$output_dir"/*/is_main_node "$output_dir"/*/.cur_input 2>/dev/null || true
trap '${pkgs.systemd}/bin/systemctl --user stop "$slice.slice" 2>/dev/null || true' EXIT
# SymCC companion
${pkgs.systemd}/bin/systemd-run --user --collect \
--slice="$slice" --unit="$slice-symcc" --quiet \
${symcc-companion}/bin/symcc-companion \
"$output_dir" "$symcc_target_dir/release/$target"
# Secondary AFL++ instances (--jobs > 1)
for i in $(seq 2 "$jobs"); do
${pkgs.systemd}/bin/systemd-run --user --collect \
--slice="$slice" --unit="$slice-s$i" --quiet \
--setenv=AFL_AUTORESUME=1 \
"$CARGO_AFL_DIR/bin/afl-fuzz" \
-S "secondary_$i" -p fast \
-i "$corpus_dir" -o "$output_dir" \
-- "$afl_target_dir/release/$target"
done
# Determine main instance flags
if [ "$jobs" -gt 1 ]; then
main_flag="-M main"
else
main_flag=""
fi
echo "Starting AFL++ on $target..."
echo " jobs : $jobs''${jobs:+ (main + $((jobs-1)) secondaries)}"
echo " persistent mode : yes (afl::fuzz! macro)"
echo " CmpLog : -c 0 -l 2AT"
echo " power schedule : -p fast"
echo " LLVM plugins : loaded"
echo " SymCC companion : running (log: $output_dir/symcc/companion.log)"
echo " stop all : systemctl --user stop $slice.slice"
echo ""
# Main instance runs in foreground so the user sees the TUI.
# shellcheck disable=SC2086
AFL_AUTORESUME=1 "$CARGO_AFL_DIR/bin/afl-fuzz" \
$main_flag \
-c 0 -l 2AT -p fast \
-i "$corpus_dir" \
-o "$output_dir" \
"''${afl_args[@]}" \
-- "$afl_target_dir/release/$target"
'';
fuzz-clean-afl = pkgs.writeShellScriptBin "fuzz-clean-afl" ''
set -euo pipefail
${afl-env}
manifest="$root/lib/fuzz-afl/Cargo.toml"
if [ $# -eq 0 ]; then
echo "Usage: fuzz-clean-afl <target>"
echo "Available targets:"
${pkgs.gawk}/bin/awk '/^\[\[bin\]\]/{found=1} found && /^name = "/{gsub(/"/, "", $3); print " " $3; found=0}' "$manifest"
exit 1
fi
target="$1"
afl_target_dir="$root/lib/fuzz-afl/target/afl"
# Collect crashes from all AFL++ instance directories
shopt -s nullglob
files=()
for d in "$root/lib/fuzz-afl/output/$target"/*/crashes; do
files+=("$d"/id:*)
done
if [ ''${#files[@]} -eq 0 ]; then
echo "No crash files to test."
exit 0
fi
echo "Building $target with cargo-afl..."
CARGO_TARGET_DIR="$afl_target_dir" \
cargo afl build --manifest-path "$manifest" --release --bin "$target"
echo "Testing ''${#files[@]} crash files for $target..."
removed=0
kept=0
for f in "''${files[@]}"; do
if timeout 30 "$afl_target_dir/release/$target" < "$f" >/dev/null 2>&1; then
rm "$f"
removed=$((removed + 1))
else
kept=$((kept + 1))
fi
echo -ne "\r tested $((removed + kept))/''${#files[@]}, removed $removed, kept $kept"
done
echo ""
echo "Done: removed $removed fixed, kept $kept still-crashing."
'';
in {
fuzz-cargo = {
type = "app";
program = "${fuzz-cargo}/bin/fuzz-cargo";
};
fuzz-clean-cargo = {
type = "app";
program = "${fuzz-clean-cargo}/bin/fuzz-clean-cargo";
};
fuzz-afl = {
type = "app";
program = "${fuzz-afl}/bin/fuzz-afl";
};
fuzz-clean-afl = {
type = "app";
program = "${fuzz-clean-afl}/bin/fuzz-clean-afl";
};
gen-fuzz-corpus = {
type = "app";
program = "${gen-fuzz-corpus}/bin/gen-fuzz-corpus";
};
};
});
}