diff --git a/scripts/smite-orchestrator.py b/scripts/smite-orchestrator.py new file mode 100755 index 00000000..f867cadd --- /dev/null +++ b/scripts/smite-orchestrator.py @@ -0,0 +1,1012 @@ +#!/usr/bin/env python3 +""" +Smite Fuzzing Campaign Orchestrator + +This script automates the execution of parallel fuzzing trials across multiple targets +for rigorous A/B coverage evaluation. It queues jobs, maps cores to trials for strict +CPU isolation via `taskset`, and launches `afl-fuzz` against Nyx sharedirs. + +`smitebot` is used for one-time setup steps (`smitebot build` to build Docker workload +images, and `smitebot doctor` to validate the host). Similar to `smitebot start`, this +script handles building `libsmite_ir_mutator.so`, preparing the Nyx sharedir, and +launching `afl-fuzz` with the right strategy flags and environment variables directly. + +A live Rich TUI dashboard monitors coverage (Edges and Execs/s) of all active cores in +real-time. Press Ctrl+C to stop scheduling new jobs and safely kill active fuzzers. + +Requirements: + pip install rich + smitebot (must be available in your PATH: `cargo install --path smitebot`) + +Generated Directory Structure: + The script creates an output directory populated with isolated trial runs based on + your defined configuration labels: + + / + ├── .default-seeds/ # Fallback '\x00' seed (if --seed-dir omitted) + ├── / # e.g., 'baseline' + │ ├── / # e.g., 'cln' + │ │ ├── trial-01/ + │ │ │ ├── afl-fuzz.log # Raw afl-fuzz stdout/stderr for this trial + │ │ │ ├── sharedir/ # Nyx sharedir (deleted on cleanup) + │ │ │ └── afl-out/default/ # Fuzzer output (stats, plot_data, bitmap) + │ │ ├── trial-02/ + │ │ └── ... + │ └── / # e.g., 'lnd' + └── / # e.g., 'experimental' + ├── / + └── / + +Usage: + python smite-orchestrator.py \ + --out-dir OUT_DIR \ + --configs LABEL:SMITE_DIR[,LABEL:SMITE_DIR...] \ + --scenario SCENARIO \ + --targets TARGET[,TARGET...] \ + --cores CORE[,CORE...] \ + --afl-dir AFL_DIR \ + [--trials N | --trial-ids ID[,ID...]] \ + [--timeout SECONDS] \ + [--seed-dir SEED_DIR] + +Examples: + # Standard 24-hour evaluation (4 isolated cores, 30 trials per target/config) + python smite-orchestrator.py \ + --out-dir ./eval-results \ + --configs baseline:~/smite,experimental:~/smite-new-mutator \ + --scenario ir \ + --targets cln,lnd,ldk,eclair \ + --cores 0,1,2,3 \ + --afl-dir ~/AFLplusplus + + # Fast exploratory test run (1-hour timeout, 5 trials, with seed corpus) + python smite-orchestrator.py \ + --out-dir ./eval-results \ + --configs control:~/smite,test:~/smite-new-mutator \ + --scenario ir \ + --targets cln \ + --cores 4,5,6,7,8 \ + --trials 5 \ + --timeout 3600 \ + --afl-dir ~/AFLplusplus \ + --seed-dir ./my_seeds + + # Targeted re-run of specific failed trials (preserves all other data) + python smite-orchestrator.py \ + --out-dir ./eval-results \ + --configs baseline:~/smite \ + --scenario ir \ + --targets lnd \ + --cores 0,1 \ + --trial-ids 1,15,20 \ + --afl-dir ~/AFLplusplus +""" + +import argparse +import collections +import json +import os +import shutil +import signal +import subprocess +import sys +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from queue import Empty, Queue +from typing import Optional + +from rich.console import Console, Group +from rich.live import Live +from rich.panel import Panel +from rich.table import Table + +# ──────────────────────────── CONFIGURATION ──────────────────────────── + + +def testcache_size_mb() -> Optional[int]: + """Suggested AFL_TESTCACHE_SIZE (MB) from available RAM. + + Mirrors smitebot's conservative thresholds (50/250/500 MB), since + machines here are shared across several concurrently-fuzzing cores. + """ + try: + meminfo = Path("/proc/meminfo").read_text() + except OSError: + return None + + for line in meminfo.splitlines(): + if line.startswith("MemAvailable:"): + try: + free_mb = int(line.split()[1]) // 1024 + except (IndexError, ValueError): + return None + if free_mb > 32_000: + return 500 + elif free_mb > 8_000: + return 250 + else: + return 50 + return None + + +@dataclass(frozen=True) +class TrialConfig: + """Immutable configuration and derived path/command resolution for a single + fuzzing trial.""" + + core: int + label: str + target: str + trial_num: int + scenario: str + out_dir: Path + smite_dir: Path + afl_dir: Path + timeout: int + seed_dir: Path + + @property + def task_name(self) -> str: + """Human-readable identifier shown in the dashboard and event log.""" + return f"{self.label}/{self.target}/trial-{self.trial_num:02d}" + + @property + def trial_dir(self) -> Path: + """Per-trial output directory: /