{ 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-gsxlVaaWhuAQ2cRMap3QRESutsK656x1RQPpyBnBQa0="; 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 " 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 " 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"; }; }; }); }