3
0
Fork 0
mirror of https://github.com/Z3Prover/z3 synced 2026-03-17 02:30:01 +00:00
z3/.github/skills/memory-safety/scripts/memory_safety.py

287 lines
9.3 KiB
Python

#!/usr/bin/env python3
"""
memory_safety.py: run sanitizer checks on Z3 test suite.
Usage:
python memory_safety.py --sanitizer asan
python memory_safety.py --sanitizer ubsan --debug
"""
import argparse
import logging
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "shared"))
from z3db import Z3DB, setup_logging
logger = logging.getLogger("z3agent")
SANITIZER_FLAGS = {
"asan": "-fsanitize=address -fno-omit-frame-pointer",
"ubsan": "-fsanitize=undefined -fno-omit-frame-pointer",
}
ASAN_ERROR = re.compile(r"ERROR:\s*AddressSanitizer:\s*(\S+)")
UBSAN_ERROR = re.compile(r":\d+:\d+:\s*runtime error:\s*(.+)")
LEAK_ERROR = re.compile(r"ERROR:\s*LeakSanitizer:")
LOCATION = re.compile(r"(\S+\.(?:cpp|c|h|hpp)):(\d+)")
def check_dependencies():
"""Fail early if required build tools are not on PATH."""
missing = []
if not shutil.which("cmake"):
missing.append(("cmake", "sudo apt install cmake"))
if not shutil.which("make"):
missing.append(("make", "sudo apt install build-essential"))
cc = shutil.which("clang") or shutil.which("gcc")
if not cc:
missing.append(("clang or gcc", "sudo apt install clang"))
if missing:
print("required tools not found:", file=sys.stderr)
for tool, install in missing:
print(f" {tool}: {install}", file=sys.stderr)
sys.exit(1)
def find_repo_root() -> Path:
d = Path.cwd()
for _ in range(10):
if (d / "CMakeLists.txt").exists() and (d / "src").is_dir():
return d
parent = d.parent
if parent == d:
break
d = parent
logger.error("could not locate Z3 repository root")
sys.exit(1)
def build_is_configured(build_dir: Path, sanitizer: str) -> bool:
"""Check whether the build directory already has a matching cmake config."""
cache = build_dir / "CMakeCache.txt"
if not cache.is_file():
return False
expected = SANITIZER_FLAGS[sanitizer].split()[0]
return expected in cache.read_text()
def configure(build_dir: Path, sanitizer: str, repo_root: Path) -> bool:
"""Run cmake with the requested sanitizer flags."""
flags = SANITIZER_FLAGS[sanitizer]
build_dir.mkdir(parents=True, exist_ok=True)
cmd = [
"cmake", str(repo_root),
f"-DCMAKE_C_FLAGS={flags}",
f"-DCMAKE_CXX_FLAGS={flags}",
f"-DCMAKE_EXE_LINKER_FLAGS={flags}",
"-DCMAKE_BUILD_TYPE=Debug",
"-DZ3_BUILD_TEST=ON",
]
logger.info("configuring %s build in %s", sanitizer, build_dir)
logger.debug("cmake command: %s", " ".join(cmd))
proc = subprocess.run(cmd, cwd=build_dir, capture_output=True, text=True)
if proc.returncode != 0:
logger.error("cmake failed:\n%s", proc.stderr)
return False
return True
def compile_tests(build_dir: Path) -> bool:
"""Compile the test-z3 target."""
nproc = os.cpu_count() or 4
cmd = ["make", f"-j{nproc}", "test-z3"]
logger.info("compiling test-z3 (%d parallel jobs)", nproc)
proc = subprocess.run(cmd, cwd=build_dir, capture_output=True, text=True)
if proc.returncode != 0:
logger.error("compilation failed:\n%s", proc.stderr[-2000:])
return False
return True
def run_tests(build_dir: Path, timeout: int) -> dict:
"""Execute test-z3 under sanitizer runtime and capture output."""
test_bin = build_dir / "test-z3"
if not test_bin.is_file():
logger.error("test-z3 not found at %s", test_bin)
return {"stdout": "", "stderr": "binary not found", "exit_code": -1,
"duration_ms": 0}
env = os.environ.copy()
env["ASAN_OPTIONS"] = "detect_leaks=1:halt_on_error=0:print_stacktrace=1"
env["UBSAN_OPTIONS"] = "print_stacktrace=1:halt_on_error=0"
cmd = [str(test_bin), "/a"]
logger.info("running: %s", " ".join(cmd))
start = time.monotonic()
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=timeout,
cwd=build_dir, env=env,
)
except subprocess.TimeoutExpired:
ms = int((time.monotonic() - start) * 1000)
logger.warning("test-z3 timed out after %dms", ms)
return {"stdout": "", "stderr": "timeout", "exit_code": -1,
"duration_ms": ms}
ms = int((time.monotonic() - start) * 1000)
logger.debug("exit_code=%d duration=%dms", proc.returncode, ms)
return {
"stdout": proc.stdout,
"stderr": proc.stderr,
"exit_code": proc.returncode,
"duration_ms": ms,
}
def parse_findings(output: str) -> list:
"""Extract sanitizer error reports from combined stdout and stderr."""
findings = []
lines = output.split("\n")
for i, line in enumerate(lines):
entry = None
m = ASAN_ERROR.search(line)
if m:
entry = {"category": "asan", "message": m.group(1),
"severity": "high"}
if not entry:
m = LEAK_ERROR.search(line)
if m:
entry = {"category": "leak",
"message": "detected memory leaks",
"severity": "high"}
if not entry:
m = UBSAN_ERROR.search(line)
if m:
entry = {"category": "ubsan", "message": m.group(1),
"severity": "medium"}
if not entry:
continue
file_path, line_no = None, None
window = lines[max(0, i - 2):i + 5]
for ctx in window:
loc = LOCATION.search(ctx)
if loc and "/usr/" not in loc.group(1):
file_path = loc.group(1)
line_no = int(loc.group(2))
break
entry["file"] = file_path
entry["line"] = line_no
entry["raw"] = line.strip()
findings.append(entry)
return findings
def deduplicate(findings: list) -> list:
"""Remove duplicate reports at the same category, file, and line."""
seen = set()
result = []
for f in findings:
key = (f["category"], f["file"], f["line"], f["message"])
if key in seen:
continue
seen.add(key)
result.append(f)
return result
def main():
parser = argparse.ArgumentParser(prog="memory-safety")
parser.add_argument("--sanitizer", choices=["asan", "ubsan", "both"],
default="asan",
help="sanitizer to enable (default: asan)")
parser.add_argument("--build-dir", default=None,
help="path to build directory")
parser.add_argument("--timeout", type=int, default=600,
help="seconds before killing test run")
parser.add_argument("--skip-build", action="store_true",
help="reuse existing instrumented build")
parser.add_argument("--db", default=None,
help="path to z3agent.db")
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
setup_logging(args.debug)
check_dependencies()
repo_root = find_repo_root()
sanitizers = ["asan", "ubsan"] if args.sanitizer == "both" else [args.sanitizer]
all_findings = []
db = Z3DB(args.db)
for san in sanitizers:
if args.build_dir:
build_dir = Path(args.build_dir)
else:
build_dir = repo_root / "build" / f"sanitizer-{san}"
run_id = db.start_run("memory-safety", f"sanitizer={san}")
db.log(f"sanitizer: {san}, build: {build_dir}", run_id=run_id)
if not args.skip_build:
needs_configure = not build_is_configured(build_dir, san)
if needs_configure and not configure(build_dir, san, repo_root):
db.finish_run(run_id, "error", 0, exit_code=1)
print(f"FAIL: cmake configuration failed for {san}")
continue
if not compile_tests(build_dir):
db.finish_run(run_id, "error", 0, exit_code=1)
print(f"FAIL: compilation failed for {san}")
continue
result = run_tests(build_dir, args.timeout)
combined = result["stdout"] + "\n" + result["stderr"]
findings = deduplicate(parse_findings(combined))
for f in findings:
db.log_finding(
run_id,
category=f["category"],
message=f["message"],
severity=f["severity"],
file=f["file"],
line=f["line"],
details={"raw": f["raw"]},
)
status = "clean" if not findings else "findings"
if result["exit_code"] == -1:
status = "timeout" if "timeout" in result["stderr"] else "error"
db.finish_run(run_id, status, result["duration_ms"], result["exit_code"])
all_findings.extend(findings)
print(f"{san}: {len(findings)} finding(s), {result['duration_ms']}ms")
if all_findings:
print(f"\nTotal: {len(all_findings)} finding(s)")
for f in all_findings:
loc = f"{f['file']}:{f['line']}" if f["file"] else "unknown location"
print(f" [{f['severity']}] {f['category']}: {f['message']} at {loc}")
db.close()
sys.exit(1)
else:
print("\nNo sanitizer findings.")
db.close()
sys.exit(0)
if __name__ == "__main__":
main()