yams/tools/fuzzing/asan_cluster.py

91 lines
2.3 KiB
Python

#!/usr/bin/env python3
import argparse
import re
import sys
from collections import defaultdict
from pathlib import Path
SUMMARY_RE = re.compile(r"^SUMMARY: AddressSanitizer: (.+)$")
FRAME_RE = re.compile(r"^#0\s+0x[0-9a-fA-F]+\s+in\s+(.+)$")
def classify(summary: str) -> str:
s = summary.lower()
if "use-after-free" in s:
return "UAF"
if "double-free" in s:
return "DoubleFree"
if "heap-buffer-overflow" in s or "stack-buffer-overflow" in s:
return "OOB"
if "global-buffer-overflow" in s:
return "OOB"
if "container-overflow" in s:
return "OOB"
if "out-of-bounds" in s:
return "OOB"
if "null" in s and "dereference" in s:
return "NULL"
if "unknown-crash" in s:
return "Unknown"
return "Other"
def parse_one(text: str) -> tuple[str, str]:
summary = "(no SUMMARY line found)"
top = "(no #0 frame found)"
for line in text.splitlines():
m = SUMMARY_RE.match(line.strip())
if m:
summary = m.group(1)
break
for line in text.splitlines():
m = FRAME_RE.match(line.strip())
if m:
top = m.group(1)
break
return summary, top
def main() -> int:
ap = argparse.ArgumentParser(
description="Cluster ASAN traces by summary + top frame"
)
ap.add_argument("paths", nargs="+", help="ASAN log files (text)")
args = ap.parse_args()
groups: dict[tuple[str, str], list[Path]] = defaultdict(list)
for p in args.paths:
path = Path(p)
try:
text = path.read_text(errors="replace")
except OSError as e:
print(f"error: {path}: {e}", file=sys.stderr)
continue
summary, top = parse_one(text)
groups[(summary, top)].append(path)
ranked = sorted(
groups.items(),
key=lambda kv: (-len(kv[1]), kv[0][0], kv[0][1]),
)
for (summary, top), files in ranked:
sev = classify(summary)
print(f"[{len(files):3d}] {sev:10s} | {summary}")
print(f" top: {top}")
for f in files[:5]:
print(f" - {f}")
if len(files) > 5:
print(f" ... +{len(files) - 5} more")
print("")
return 0
if __name__ == "__main__":
raise SystemExit(main())