mirror of
https://github.com/AFLplusplus/AFLplusplus.git
synced 2025-06-12 10:08:07 +00:00
* Pure Python (3.6) port of benchmark.sh as benchmark.py, no other changes * Test standard and persistent modes separately * Add support for multi-core benchmarking * Save the results to a json file * Allow config of all experiment params, average across runs * Add start_time_of_run and total_execs_per_sec, cleanup for PR * benchmark: cleanup, add results, add a data exploration notebook * benchmark: add a README, lower default runs from 5 to 3 * benchmark: notebook wording tweaks * copy 'detect_leaks=0' from ASAN to LSAN fix for issue #1733, set "detect_leaks=0" when ASAN_OPTIONS contains it and LSAN_OPTIONS are not set. * fix of fix: make sure ASAN_OPTIONS and LSAN_OPTIONS agree on leak detection * fix lsan fix * clang-format 16->17 * Add missing initialisation for havoc_queued during the custom mutator's stage. * fix dictionary and cmin * Use direct call to write to OpenBSD The linker on OpenBSD emits a warning when linking this file: warning: syscall() may go away, please rewrite code to use direct calls * Fix possible doc inconsistency for custom mutator's queue_get function. * update todos * benchmark: Add support for COMPARISON file * benchmark: show the number of cores used in COMPARISON * benchmark: lower minimum Python version to 3.8 * benchmark: use afl's execs/s; increase CPU model width * benchmark: disallow duplicate entries for the same CPU in COMPARISON * Update benchmark.py * fix inf in stats * Fix benchmark.py * missing closing parenthesis * Update benchmark.py * benchmark: remove self-calculation of execs/sec * benchmark: update COMPARISON * benchmark: Update Jupyter notebook and results file. * benchmark: rename afl_execs_per_sec to execs_per_sec * benchmark: update README * update * add benchmark * nits * add benchmarks * Update unicornafl ref * Pass correct Nyx ID when creating a Nyx runner * Fix typo in docker pull command, add exampe to mount current dir as volume (#1914) * mini fix * add custom_post_run.c * update afl-fuzz-run * update python module * format code * update * merge function * changes * code format * improve cmplog * nit * nit * fix * fix * Stop hardcoding the path /usr/local/lib/afl in afl-ld-lto.c and respect the configured PREFIX. * Add benchmark for Raspberry Pi 5 * ryzen 5950 benchmark * add missing raspery5 * comparison -> comparison.md * removing options "-Wl,-rpath" "LLVM_LIBDIR" when using gcc * fixing -Wl,-rpath=<LLVM_LIBDIR> * nits * fix * afl-cc fixes * nit * add n_fuzz to ignore_timeouts * fix * Fix #1927 * in-depth blog post * add AFL_FUZZER_LOOPCOUNT * AFL_FUZZER_LOOPCOUNT * fix 2 mutation bugs * v4.09c release * v4.10a init * switch to explore powerschedule as default * fix MUT_INSERTASCIINUM * fix MUT_STRATEGY_ARRAY_SIZE * fix bad fix for MUT_STRATEGY_ARRAY_SIZE * remove afl-network-client on uninstall * update nyx * Improve binary-only related docs * llvm 18 build fixes. * code format * Fix custom_send link Add a leading '/' to walk in the repo root instead of current dir. * Use ../ instead * initial simple injection detection support * inject docs * fix for issue #1916, iLLVM crash in split-floatingpoint-compares * LLVM 17 bug workaround * finish injection implementation * remove tmp todo * update changelog * forgot to add the injection pass * Output afl-clang-fast stuffs only if necessary (#1912) * afl-cc header * afl-cc common declarations - Add afl-cc-state.c - Strip includes, find_object, debug/be_quiet/have_*/callname setting from afl-cc.c - Use debugf_args in main - Modify execvp stuffs to fit new aflcc struct * afl-cc show usage * afl-cc mode selecting 1. compiler_mode by callname in argv[0] 2. compiler_mode by env "AFL_CC_COMPILER" 3. compiler_mode/instrument_mode by command line options "--afl-..." 4. instrument_mode/compiler_mode by various env vars including "AFL_LLVM_INSTRUMENT" 5. final checking steps 6. print "... - mode: %s-%s\n" 7. determine real argv[0] according to compiler_mode * afl-cc macro defs * afl-cc linking behaviors * afl-cc fsanitize behaviors * afl-cc misc * afl-cc body update * afl-cc all-in-one formated with custom-format.py * nits --------- Co-authored-by: vanhauser-thc <vh@thc.org> * changelog * update grammar mutator * lto llvm 12+ * docs(custom_mutators): fix missing ':' (#1953) * Fix broken LTO mode and response file support (#1948) * Strip `-Wl,-no-undefined` during compilation (#1952) Make the compiler wrapper stripping `-Wl,-no-undefined` in addition to `-Wl,--no-undefined`. Both versions of the flag are accepted by clang and, therefore, used by building systems in the wild (e.g., samba will not build without this fix). * Remove dead code in write_to_testcase (#1955) The custom_mutators_count check in if case is duplicate with if condition. The else case is custom_mutators_count == 0, neither custom_mutator_list iteration nor sent check needed. Signed-off-by: Xeonacid <h.dwwwwww@gmail.com> * update qemuafl * WIP: Add ability to generate drcov trace using QEMU backend (#1956) * Document new drcov QEMU plugin * Add link to lightkeeper for QEMU drcov file loading --------- Co-authored-by: Jean-Romain Garnier <jean-romain.garnier@airbus.com> * code format * changelog * sleep on uid != 0 afl-system-config * fix segv about skip_next, warn on unsupported cases of linking options (#1958) * todos * ensure afl-cc only allows available compiler modes * update grammar mutator * disable aslr on apple * fix for arm64 * help selective instrumentation * typos * macos * add compiler test script * apple fixes --------- Signed-off-by: Xeonacid <h.dwwwwww@gmail.com> Co-authored-by: Chris Ball <chris@printf.net> Co-authored-by: hexcoder <hexcoder-@users.noreply.github.com> Co-authored-by: hexcoder- <heiko@hexco.de> Co-authored-by: Manuel Carrasco <m.carrasco@imperial.ac.uk> Co-authored-by: Jasper Lievisse Adriaanse <j@jasper.la> Co-authored-by: ifyGecko <26214995+ifyGecko@users.noreply.github.com> Co-authored-by: Dominik Maier <domenukk@gmail.com> Co-authored-by: Christian Holler (:decoder) <choller@mozilla.com> Co-authored-by: Carlo Maragno <ste.maragno@gmail.com> Co-authored-by: yangzao <yangzaocn@outlook.com> Co-authored-by: Romain Geissler <romain.geissler@amadeus.com> Co-authored-by: Jakob Lell <jakob@jakoblell.com> Co-authored-by: vincenzo MEZZELA <vincenzo.mezzela@amadeus.com> Co-authored-by: Andrea Fioraldi <andreafioraldi@gmail.com> Co-authored-by: Bet4 <0xbet4@gmail.com> Co-authored-by: David Carlier <devnexen@gmail.com> Co-authored-by: Xeonacid <h.dwwwwww@gmail.com> Co-authored-by: Sonic <50692172+SonicStark@users.noreply.github.com> Co-authored-by: Nils Bars <nils.bars@rub.de> Co-authored-by: Jean-Romain Garnier <7504819+JRomainG@users.noreply.github.com> Co-authored-by: Jean-Romain Garnier <jean-romain.garnier@airbus.com>
282 lines
14 KiB
Python
Executable File
282 lines
14 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Part of the aflplusplus project, requires Python 3.8+.
|
|
# Author: Chris Ball <chris@printf.net>, ported from Marc "van Hauser" Heuse's "benchmark.sh".
|
|
import argparse, asyncio, json, multiprocessing, os, platform, re, shutil, sys
|
|
from dataclasses import asdict, dataclass
|
|
from decimal import Decimal
|
|
from enum import Enum, auto
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
blue = lambda text: f"\033[1;94m{text}\033[0m"; gray = lambda text: f"\033[1;90m{text}\033[0m"
|
|
green = lambda text: f"\033[0;32m{text}\033[0m"; red = lambda text: f"\033[0;31m{text}\033[0m"
|
|
yellow = lambda text: f"\033[0;33m{text}\033[0m"
|
|
|
|
class Mode(Enum):
|
|
multicore = auto()
|
|
singlecore = auto()
|
|
|
|
@dataclass
|
|
class Target:
|
|
source: Path
|
|
binary: Path
|
|
|
|
@dataclass
|
|
class Run:
|
|
execs_per_sec: float
|
|
execs_total: float
|
|
fuzzers_used: int
|
|
|
|
@dataclass
|
|
class Config:
|
|
afl_persistent_config: bool
|
|
afl_system_config: bool
|
|
afl_version: Optional[str]
|
|
comment: str
|
|
compiler: str
|
|
target_arch: str
|
|
|
|
@dataclass
|
|
class Hardware:
|
|
cpu_fastest_core_mhz: float
|
|
cpu_model: str
|
|
cpu_threads: int
|
|
|
|
@dataclass
|
|
class Results:
|
|
config: Optional[Config]
|
|
hardware: Optional[Hardware]
|
|
targets: Dict[str, Dict[str, Optional[Run]]]
|
|
|
|
all_modes = [Mode.singlecore, Mode.multicore]
|
|
all_targets = [
|
|
Target(source=Path("../utils/persistent_mode/test-instr.c").resolve(), binary=Path("test-instr-persist-shmem")),
|
|
Target(source=Path("../test-instr.c").resolve(), binary=Path("test-instr"))
|
|
]
|
|
modes = [mode.name for mode in all_modes]
|
|
targets = [str(target.binary) for target in all_targets]
|
|
cpu_count = multiprocessing.cpu_count()
|
|
env_vars = {
|
|
"AFL_DISABLE_TRIM": "1", "AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES": "1", "AFL_FAST_CAL": "1",
|
|
"AFL_NO_UI": "1", "AFL_TRY_AFFINITY": "1", "PATH": f'{str(Path("../").resolve())}:{os.environ["PATH"]}',
|
|
}
|
|
|
|
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
parser.add_argument("-b", "--basedir", help="directory to use for temp files", type=str, default="/tmp/aflpp-benchmark")
|
|
parser.add_argument("-d", "--debug", help="show verbose debugging output", action="store_true")
|
|
parser.add_argument("-r", "--runs", help="how many runs to average results over", type=int, default=3)
|
|
parser.add_argument("-f", "--fuzzers", help="how many afl-fuzz workers to use", type=int, default=cpu_count)
|
|
parser.add_argument("-m", "--mode", help="pick modes", action="append", default=modes, choices=modes)
|
|
parser.add_argument("-c", "--comment", help="add a comment about your setup", type=str, default="")
|
|
parser.add_argument("--cpu", help="override the detected CPU model name", type=str, default="")
|
|
parser.add_argument("--mhz", help="override the detected CPU MHz", type=str, default="")
|
|
parser.add_argument(
|
|
"-t", "--target", help="pick targets", action="append", default=["test-instr-persist-shmem"], choices=targets
|
|
)
|
|
args = parser.parse_args()
|
|
# Really unsatisfying argparse behavior: we want a default and to allow multiple choices, but if there's a manual choice
|
|
# it should override the default. Seems like we have to remove the default to get that and have correct help text?
|
|
if len(args.target) > 1:
|
|
args.target = args.target[1:]
|
|
if len(args.mode) > 2:
|
|
args.mode = args.mode[2:]
|
|
|
|
chosen_modes = [mode for mode in all_modes if mode.name in args.mode]
|
|
chosen_targets = [target for target in all_targets if str(target.binary) in args.target]
|
|
results = Results(config=None, hardware=None, targets={
|
|
str(t.binary): {m.name: None for m in chosen_modes} for t in chosen_targets}
|
|
)
|
|
debug = lambda text: args.debug and print(blue(text))
|
|
|
|
async def clean_up_tempfiles() -> None:
|
|
shutil.rmtree(f"{args.basedir}/in")
|
|
for target in chosen_targets:
|
|
target.binary.unlink()
|
|
for mode in chosen_modes:
|
|
shutil.rmtree(f"{args.basedir}/out-{mode.name}-{str(target.binary)}")
|
|
|
|
async def check_afl_persistent() -> bool:
|
|
with open("/proc/cmdline", "r") as cmdline:
|
|
return "mitigations=off" in cmdline.read().strip().split(" ")
|
|
|
|
async def check_afl_system() -> bool:
|
|
sysctl = next((s for s in ["sysctl", "/sbin/sysctl"] if shutil.which(s)), None)
|
|
if sysctl:
|
|
(returncode, stdout, _) = await run_command([sysctl, "kernel.randomize_va_space"])
|
|
return returncode == 0 and stdout.decode().rstrip().split(" = ")[1] == "0"
|
|
return False
|
|
|
|
async def prep_env() -> None:
|
|
Path(f"{args.basedir}/in").mkdir(exist_ok=True, parents=True)
|
|
with open(f"{args.basedir}/in/in.txt", "wb") as seed:
|
|
seed.write(b"\x00" * 10240)
|
|
|
|
async def compile_target(source: Path, binary: Path) -> None:
|
|
print(f" [*] Compiling the {binary} fuzzing harness for the benchmark to use.")
|
|
(returncode, stdout, stderr) = await run_command(
|
|
[str(Path("../afl-clang-lto").resolve()), "-o", str(Path(binary.resolve())), str(Path(source).resolve())]
|
|
)
|
|
if returncode == 0:
|
|
return
|
|
print(yellow(f" [*] afl-clang-lto was unable to compile; falling back to afl-cc."))
|
|
(returncode, stdout, stderr) = await run_command(
|
|
[str(Path("../afl-cc").resolve()), "-o", str(Path(binary.resolve())), str(Path(source).resolve())]
|
|
)
|
|
if returncode != 0:
|
|
sys.exit(red(f" [*] Error: afl-cc is unable to compile: {stderr.decode()} {stdout.decode()}"))
|
|
|
|
async def run_command(cmd: List[str]) -> Tuple[Optional[int], bytes, bytes]:
|
|
debug(f"Launching command: {cmd} with env {env_vars}")
|
|
p = await asyncio.create_subprocess_exec(
|
|
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env_vars
|
|
)
|
|
stdout, stderr = await p.communicate()
|
|
debug(f"Output: {stdout.decode()} {stderr.decode()}")
|
|
return (p.returncode, stdout, stderr)
|
|
|
|
async def check_deps() -> None:
|
|
if not (plat := platform.system()) == "Linux": sys.exit(red(f" [*] {plat} is not supported by this script yet."))
|
|
if not os.access(Path("../afl-fuzz").resolve(), os.X_OK) and os.access(Path("../afl-cc").resolve(), os.X_OK) and (
|
|
os.path.exists(Path("../SanitizerCoveragePCGUARD.so").resolve())):
|
|
sys.exit(red(" [*] Compile AFL++: we need afl-fuzz, afl-clang-fast and SanitizerCoveragePCGUARD.so built."))
|
|
|
|
(returncode, stdout, stderr) = await run_command([str(Path("../afl-cc").resolve()), "-v"])
|
|
if returncode != 0:
|
|
sys.exit(red(f" [*] Error: afl-cc -v returned: {stderr.decode()} {stdout.decode()}"))
|
|
compiler = ""
|
|
target_arch = ""
|
|
for line in stderr.decode().split("\n"):
|
|
if "clang version" in line:
|
|
compiler = line
|
|
elif m := re.match(r"^Target: (.*)", line):
|
|
target_arch = m.group(1)
|
|
|
|
# Pick some sample settings from afl-{persistent,system}-config to try to see whether they were run.
|
|
afl_pc = await check_afl_persistent()
|
|
afl_sc = await check_afl_system()
|
|
if not afl_pc:
|
|
print(yellow(f" [*] afl-persistent-config did not run; run it to improve performance (and decrease security)."))
|
|
if not afl_sc:
|
|
print(yellow(f" [*] afl-system-config did not run; run it to improve performance (and decrease security)."))
|
|
results.config = Config(afl_persistent_config=afl_pc, afl_system_config=afl_sc, afl_version="",
|
|
comment=args.comment, compiler=compiler, target_arch=target_arch)
|
|
|
|
async def colon_values(filename: str, searchKey: str) -> List[str]:
|
|
"""Return a colon-separated value given a key in a file, e.g. 'cpu MHz : 4976.109')"""
|
|
with open(filename, "r") as fh:
|
|
kv_pairs = (line.split(": ", 1) for line in fh if ": " in line)
|
|
v_list = [v.rstrip() for k, v in kv_pairs if k.rstrip() == searchKey]
|
|
return v_list
|
|
|
|
async def describe_afl_config() -> str:
|
|
if results.config is None:
|
|
return "unknown"
|
|
elif results.config.afl_persistent_config and results.config.afl_system_config:
|
|
return "both"
|
|
elif results.config.afl_persistent_config:
|
|
return "persistent"
|
|
elif results.config.afl_system_config:
|
|
return "system"
|
|
else:
|
|
return "none"
|
|
|
|
async def save_benchmark_results() -> None:
|
|
"""Append a single row to the benchmark results in JSON Lines format (which is simple to write and diff)."""
|
|
with open("benchmark-results.jsonl", "a") as jsonfile:
|
|
json.dump(asdict(results), jsonfile, sort_keys=True)
|
|
jsonfile.write("\n")
|
|
print(blue(f" [*] Results have been written to the {jsonfile.name} file."))
|
|
with open("COMPARISON.md", "r+") as comparisonfile:
|
|
described_config = await describe_afl_config()
|
|
aflconfig = described_config.ljust(12)
|
|
if results.hardware is None:
|
|
return
|
|
cpu_model = results.hardware.cpu_model.ljust(51)
|
|
if cpu_model in comparisonfile.read():
|
|
print(blue(f" [*] Results have not been written to the COMPARISON.md file; this CPU is already present."))
|
|
return
|
|
cpu_mhz = str(round(results.hardware.cpu_fastest_core_mhz)).ljust(5)
|
|
if not "test-instr-persist-shmem" in results.targets or \
|
|
not "multicore" in results.targets["test-instr-persist-shmem"] or \
|
|
not "singlecore" in results.targets["test-instr-persist-shmem"] or \
|
|
results.targets["test-instr-persist-shmem"]["singlecore"] is None or \
|
|
results.targets["test-instr-persist-shmem"]["multicore"] is None:
|
|
return
|
|
single = str(round(results.targets["test-instr-persist-shmem"]["singlecore"].execs_per_sec)).ljust(10)
|
|
multi = str(round(results.targets["test-instr-persist-shmem"]["multicore"].execs_per_sec)).ljust(9)
|
|
cores = str(args.fuzzers).ljust(7)
|
|
comparisonfile.write(f"{cpu_model} | {cpu_mhz} | {cores} | {single} | {multi} | {aflconfig} |\n")
|
|
print(blue(f" [*] Results have been written to the COMPARISON.md file."))
|
|
with open("COMPARISON.md", "r") as comparisonfile:
|
|
print(comparisonfile.read())
|
|
|
|
|
|
async def main() -> None:
|
|
try:
|
|
await clean_up_tempfiles()
|
|
except FileNotFoundError:
|
|
pass
|
|
await check_deps()
|
|
if args.mhz:
|
|
cpu_mhz = float(args.mhz)
|
|
else:
|
|
cpu_mhz_str = await colon_values("/proc/cpuinfo", "cpu MHz")
|
|
if len(cpu_mhz_str) == 0:
|
|
cpu_mhz_str.append("0")
|
|
cpu_mhz = max([float(c) for c in cpu_mhz_str]) # use the fastest CPU MHz for now
|
|
if args.cpu:
|
|
cpu_model = [args.cpu]
|
|
else:
|
|
cpu_model = await colon_values("/proc/cpuinfo", "model name") or [""]
|
|
results.hardware = Hardware(cpu_fastest_core_mhz=cpu_mhz, cpu_model=cpu_model[0], cpu_threads=cpu_count)
|
|
await prep_env()
|
|
print(f" [*] Ready, starting benchmark...")
|
|
for target in chosen_targets:
|
|
await compile_target(target.source, target.binary)
|
|
binary = str(target.binary)
|
|
for mode in chosen_modes:
|
|
if mode == Mode.multicore:
|
|
print(blue(f" [*] Using {args.fuzzers} fuzzers for multicore fuzzing "), end="")
|
|
print(blue("(use --fuzzers to override)." if args.fuzzers == cpu_count else f"(the default is {cpu_count})"))
|
|
execs_per_sec, execs_total = ([] for _ in range(2))
|
|
for run_idx in range(0, args.runs):
|
|
print(gray(f" [*] {mode.name} {binary} run {run_idx+1} of {args.runs}, execs/s: "), end="", flush=True)
|
|
fuzzers = range(0, args.fuzzers if mode == Mode.multicore else 1)
|
|
outdir = f"{args.basedir}/out-{mode.name}-{binary}"
|
|
cmds = []
|
|
for fuzzer_idx, afl in enumerate(fuzzers):
|
|
name = ["-o", outdir, "-M" if fuzzer_idx == 0 else "-S", str(afl)]
|
|
cmds.append(["afl-fuzz", "-i", f"{args.basedir}/in"] + name + ["-s", "123", "-V10", "-D", f"./{binary}"])
|
|
# Prepare the afl-fuzz tasks, and then block while waiting for them to finish.
|
|
fuzztasks = [run_command(cmds[cpu]) for cpu in fuzzers]
|
|
await asyncio.gather(*fuzztasks)
|
|
afl_versions = await colon_values(f"{outdir}/0/fuzzer_stats", "afl_version")
|
|
if results.config:
|
|
results.config.afl_version = afl_versions[0]
|
|
# Our score is the sum of all execs_per_sec entries in fuzzer_stats files for the run.
|
|
sectasks = [colon_values(f"{outdir}/{afl}/fuzzer_stats", "execs_per_sec") for afl in fuzzers]
|
|
all_execs_per_sec = await asyncio.gather(*sectasks)
|
|
execs = sum([Decimal(count[0]) for count in all_execs_per_sec])
|
|
print(green(execs))
|
|
execs_per_sec.append(execs)
|
|
# Also gather execs_total and total_run_time for this run.
|
|
exectasks = [colon_values(f"{outdir}/{afl}/fuzzer_stats", "execs_done") for afl in fuzzers]
|
|
all_execs_total = await asyncio.gather(*exectasks)
|
|
execs_total.append(sum([Decimal(count[0]) for count in all_execs_total]))
|
|
|
|
# (Using float() because Decimal() is not JSON-serializable.)
|
|
avg_afl_execs_per_sec = round(Decimal(sum(execs_per_sec) / len(execs_per_sec)), 2)
|
|
afl_execs_total = int(sum([Decimal(execs) for execs in execs_total]))
|
|
run = Run(execs_per_sec=float(avg_afl_execs_per_sec), execs_total=afl_execs_total, fuzzers_used=len(fuzzers))
|
|
results.targets[binary][mode.name] = run
|
|
print(f" [*] Average execs/sec for this test across all runs was: {green(avg_afl_execs_per_sec)}")
|
|
if (((max(execs_per_sec) - min(execs_per_sec)) / avg_afl_execs_per_sec) * 100) > 15:
|
|
print(yellow(" [*] The difference between your slowest and fastest runs was >15%, maybe try again?"))
|
|
|
|
await clean_up_tempfiles()
|
|
await save_benchmark_results()
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|
|
|