bajia/tools/fuzz.py

209 lines
6.9 KiB
Python
Raw Permalink Normal View History

2026-08-28 00:22:49 +01:00
#!/usr/bin/env python3
"""build and run the libFuzzer harness for the .rc config parser.
usage:
python3 tools/fuzz.py [--seconds N] [--build-only] [--corpus DIR]
[--dict PATH|--no-dict] [--no-detect-leaks]
[--quarantine-mb N]
defaults to 60 seconds of fuzzing against a small seed corpus. Seed corpus
files land in build/fuzz-corpus (recreated only when empty so you can add your
own); crashing inputs are saved under build/fuzz- and reported at the end.
Leak detection is on by default (tools/lsan.supp silences a torsocks
LD_PRELOAD false positive on the dev host); --no-detect-leaks disables it.
ASan's freed-memory quarantine (the fuzz peak-RSS driver) defaults to 64MiB;
override with --quarantine-mb.
requires clang++ (libFuzzer's -fsanitize=fuzzer is a clang feature).
"""
from __future__ import annotations
import argparse
import glob
import os
import shutil
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
TESTS = ROOT / "tests"
BUILD = ROOT / "build"
OUT = BUILD / "fuzz_config"
CORPUS = BUILD / "fuzz-corpus"
DICT = BUILD / "fuzz.dict"
# restart wildcard entries are not allowed in a dict; keep tokens plain ASCII.
SEED_DICT = r"""# bajia rc grammar: libFuzzer dictionary (-dict), regenerated only if missing
"service"
"on"
"@import"
"oneshot"
"disabled"
"console"
"user"
"group"
"class"
"respawn"
"crash-threshold"
"crash-window"
"setenv"
"cwd"
"seclabel"
"="
"start"
"stop"
"restart"
"exec"
"mkdir"
"chmod"
"chown"
"write"
"symlink"
"mount"
"log"
"early-init"
"init"
"boot"
"shutdown"
"always"
"never"
"on-failure"
"root"
"nobody"
"daemon"
"/bin/true"
"/bin/false"
"/bin/sh"
"/usr/sbin/sshd"
"/etc/bajia/init.rc"
"/proc"
"/sys"
"/dev"
"/tmp"
"0"
"0755"
"#"
";"
# __bajia_seed_dict__
"""
SEEDS = {
"tiny.rc": b"on boot\n log hi\n",
"service.rc": b"service sshd /usr/sbin/sshd -D\n user = root\n respawn = always\n console\n",
"alloptions.rc": (
b"service s /bin/true\n"
b" user = nobody\n group = nobody daemon\n"
b" class = main\n oneshot\n disabled\n console\n"
b" respawn = never\n crash-threshold = 4\n crash-window = 30\n"
b" setenv = FOO=bar\n cwd = /tmp\n seclabel = system_u:object_r:root_t:s0\n"
),
"quotes.rc": b"write /tmp/x \"a b\\\"c\"\non boot\n exec /bin/sh -c \"echo quoted\"\n",
"import.rc": b"@import /etc/bajia/init.rc\non init\n log imported\n",
"nested.rc": (
b"on early-init\n mount proc /proc proc\n mount sysfs /sys sysfs\n"
b"on init\n write /proc/sys/kernel/hostname bajia\n"
b"on boot\n start console-serial\n"
),
}
def find_clang() -> str | None:
explicit = os.environ.get("BAJIA_FUZZ_CXX")
if explicit and shutil.which(explicit):
return explicit
if shutil.which("clang++"):
return "clang++"
return None
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--seconds", type=int, default=60, help="fuzz time budget")
ap.add_argument("--corpus", type=Path,
help="corpus dir (default: build/fuzz-corpus)")
ap.add_argument("--dict", type=Path,
help="custom fuzzer dictionary (default: build/fuzz.dict, "
"seeded from the rc grammar on first use)")
ap.add_argument("--no-dict", action="store_true",
help="run without a dictionary")
ap.add_argument("--build-only", action="store_true",
help="build the harness and stop")
ap.add_argument("--detect-leaks", action="store_true",
help=argparse.SUPPRESS) # historical no-op; already default
ap.add_argument("--no-detect-leaks", action="store_true",
help="disable leak detection (default on: exits nonzero on "
"leaks; tools/lsan.supp silences a torsocks "
"LD_PRELOAD false positive on the dev host)")
ap.add_argument("--quarantine-mb", type=int, default=64, metavar="N",
help="ASan freed-memory quarantine in MiB (default 64; "
"ASan's own default of 256 dominates fuzz peak RSS. "
"0 disables quarantine, which recycles freed memory "
"immediately but weakens use-after-free detection)")
args = ap.parse_args()
clang = find_clang()
if not clang:
sys.exit("clang++ not found: libFuzzer (-fsanitize=fuzzer) requires clang")
BUILD.mkdir(exist_ok=True)
subprocess.run([
clang, "-std=c++20", "-O1", "-g",
"-fsanitize=fuzzer,address,undefined", "-fno-omit-frame-pointer",
str(TESTS / "fuzz_config.cpp"), "-o", str(OUT),
], check=True)
print("built:", OUT)
if args.build_only:
return 0
corpus = args.corpus or CORPUS
if corpus.is_dir() and any(corpus.iterdir()):
print("using existing corpus:", corpus)
else:
corpus.mkdir(parents=True, exist_ok=True)
for name, blob in SEEDS.items():
(corpus / name).write_bytes(blob)
print("seeded corpus:", corpus)
prefix = BUILD / "fuzz-"
prefix.mkdir(exist_ok=True)
flags = [f"-artifact_prefix={prefix}/", "-print_final_stats=1"]
if not args.no_dict:
if args.dict:
dict_path = args.dict
else:
if not DICT.is_file() or "__bajia_seed_dict__" not in DICT.read_text(errors="ignore"):
DICT.write_text(SEED_DICT) # (re)seed a missing or stale copy
dict_path = DICT
if not dict_path.is_file():
sys.exit(f"--dict: file not found: {dict_path}")
flags.append(f"-dict={dict_path}")
print("dictionary:", dict_path)
env = dict(os.environ)
asan = f"quarantine_size_mb={args.quarantine_mb}"
if args.no_detect_leaks:
flags.append("-detect_leaks=0")
asan = "detect_leaks=0:" + asan
else:
asan = "detect_leaks=1:" + asan
supp = ROOT / "tools" / "lsan.supp"
env["LSAN_OPTIONS"] = f"suppressions={supp}" + (":" + env["LSAN_OPTIONS"]
if env.get("LSAN_OPTIONS") else "")
print("leak detection on (suppression:", supp, ")")
env["ASAN_OPTIONS"] = asan
cmd = [str(OUT), f"-max_total_time={args.seconds}"] + flags + [str(corpus)]
print("$", " ".join(cmd))
r = subprocess.run(cmd, env=env)
crashes = sorted(glob.glob(str(prefix) + "crash-*"))
leaks = sorted(glob.glob(str(prefix) + "leak-*"))
timeouts = sorted(glob.glob(str(prefix) + "timeout-*"))
if crashes or leaks or timeouts:
for art in crashes + leaks + timeouts:
print("artifact:", art)
print("!! fuzzer found problems (see artifact files above)")
return 1
print("no crashes in", args.seconds, "seconds")
return 0
if __name__ == "__main__":
sys.exit(main())