151 lines
5.1 KiB
Python
Executable file
151 lines
5.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Generate small random C programs and compile them with the W65816
|
|
backend. Catches crashes / lowering gaps / verifier failures.
|
|
|
|
Each generated program is small (~10-50 lines), uses combinations of
|
|
features the compiler should handle:
|
|
- integer arithmetic (i8, i16, i32, i64)
|
|
- control flow (if, while, for, switch)
|
|
- structs and pointer derefs
|
|
- function calls (recursive, multi-arg)
|
|
- casts and bit operations
|
|
- arrays (small)
|
|
|
|
For each program, we just compile to .o. If clang exits non-zero or
|
|
crashes, we save the offending source for inspection.
|
|
|
|
Optionally MAME-runs each program for additional runtime checks (off
|
|
by default — slow).
|
|
|
|
Usage: fuzzCompile.py [-n COUNT] [-s SEED] [--keep-failures DIR]
|
|
"""
|
|
|
|
import argparse, os, random, subprocess, sys, tempfile, hashlib
|
|
from pathlib import Path
|
|
|
|
CLANG = Path(__file__).parent.parent / "tools/llvm-mos-build/bin/clang"
|
|
|
|
# --- generators ---
|
|
|
|
def gen_expr(rng, depth=0):
|
|
"""Generate a random arithmetic expression returning int."""
|
|
if depth > 3 or rng.random() < 0.3:
|
|
return rng.choice([
|
|
str(rng.randint(0, 100)),
|
|
f"({rng.randint(0, 5)} + {rng.randint(0, 5)})",
|
|
"x",
|
|
])
|
|
op = rng.choice(["+", "-", "*", "&", "|", "^", "<<", ">>"])
|
|
lhs = gen_expr(rng, depth + 1)
|
|
rhs = rng.choice(["1", "2", "3", "4", str(rng.randint(0, 10))])
|
|
if op in ("<<", ">>"):
|
|
rhs = str(rng.randint(0, 7))
|
|
return f"({lhs} {op} {rhs})"
|
|
|
|
|
|
def gen_stmt(rng, varCount, depth=0):
|
|
"""Generate a random statement."""
|
|
kind = rng.choice(["assign", "if", "while", "loop"])
|
|
if depth > 2:
|
|
kind = "assign"
|
|
if kind == "assign":
|
|
v = f"v{rng.randint(0, varCount - 1)}"
|
|
return f"{v} = {gen_expr(rng)};"
|
|
if kind == "if":
|
|
cond = f"{gen_expr(rng)} {rng.choice(['<', '>', '==', '!='])} {rng.randint(0, 30)}"
|
|
body = gen_stmt(rng, varCount, depth + 1)
|
|
return f"if ({cond}) {{ {body} }}"
|
|
if kind == "while":
|
|
cnt = rng.randint(2, 5)
|
|
body = gen_stmt(rng, varCount, depth + 1)
|
|
return f"{{ int j = {cnt}; while (j-- > 0) {{ {body} }} }}"
|
|
if kind == "loop":
|
|
v = f"v{rng.randint(0, varCount - 1)}"
|
|
return f"for (int i = 0; i < {rng.randint(2, 6)}; i++) {{ {v} += i; }}"
|
|
return ";"
|
|
|
|
|
|
def gen_function(rng, name, varCount):
|
|
"""Generate a function `int name(int x)` with random body."""
|
|
decls = "\n ".join(f"int v{i} = {rng.randint(0, 50)};" for i in range(varCount))
|
|
stmts = "\n ".join(gen_stmt(rng, varCount) for _ in range(rng.randint(3, 8)))
|
|
ret = "v0"
|
|
if varCount > 1:
|
|
ret = " + ".join(f"v{i}" for i in range(min(varCount, 3)))
|
|
return f"""int {name}(int x) {{
|
|
{decls}
|
|
{stmts}
|
|
return {ret};
|
|
}}"""
|
|
|
|
|
|
def gen_program(rng):
|
|
funcCount = rng.randint(1, 3)
|
|
parts = []
|
|
for i in range(funcCount):
|
|
varCount = rng.randint(1, 5)
|
|
parts.append(gen_function(rng, f"f{i}", varCount))
|
|
parts.append(f"int call_all(int x) {{ return " +
|
|
" + ".join(f"f{i}(x)" for i in range(funcCount)) + "; }")
|
|
return "\n\n".join(parts) + "\n"
|
|
|
|
|
|
# --- driver ---
|
|
|
|
def compile_one(source, keepDir=None, idx=0):
|
|
"""Compile source bytes; return (ok, msg)."""
|
|
with tempfile.NamedTemporaryFile(suffix=".c", delete=False, mode="w") as f:
|
|
f.write(source); cFile = f.name
|
|
oFile = cFile + ".o"
|
|
try:
|
|
r = subprocess.run(
|
|
[str(CLANG), "-target", "w65816", "-O2",
|
|
"-ffunction-sections", "-c", cFile, "-o", oFile],
|
|
capture_output=True, timeout=60
|
|
)
|
|
if r.returncode != 0:
|
|
if keepDir:
|
|
tag = hashlib.sha256(source.encode()).hexdigest()[:8]
|
|
kept = Path(keepDir) / f"fail_{idx:03d}_{tag}.c"
|
|
kept.write_text(source)
|
|
kept.with_suffix(".c.stderr").write_bytes(r.stderr)
|
|
return False, r.stderr.decode("utf-8", errors="replace")
|
|
return True, ""
|
|
except subprocess.TimeoutExpired:
|
|
return False, "timeout (60s)"
|
|
finally:
|
|
for p in (cFile, oFile):
|
|
try: os.unlink(p)
|
|
except FileNotFoundError: pass
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("-n", "--count", type=int, default=20)
|
|
ap.add_argument("-s", "--seed", type=int, default=42)
|
|
ap.add_argument("--keep-failures", default=None,
|
|
help="directory to save sources of failing inputs")
|
|
ap.add_argument("-q", "--quiet", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
if args.keep_failures:
|
|
Path(args.keep_failures).mkdir(parents=True, exist_ok=True)
|
|
|
|
rng = random.Random(args.seed)
|
|
fails = 0
|
|
for i in range(args.count):
|
|
src = gen_program(rng)
|
|
ok, msg = compile_one(src, args.keep_failures, i)
|
|
if not ok:
|
|
fails += 1
|
|
if not args.quiet:
|
|
print(f"[fuzz] FAIL #{i}: {msg.splitlines()[0] if msg else '?'}")
|
|
elif not args.quiet:
|
|
print(f"[fuzz] OK #{i}")
|
|
print(f"fuzz: {args.count - fails}/{args.count} passed ({fails} fails)")
|
|
sys.exit(1 if fails else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|