File size: 6,183 Bytes
4f0bd4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | """
Retroactively run dynamic validation on every BMC-confirmed adjacent bug
in a judge-dir output directory.
For each confirmed bug:
1. Re-parse the target function's source file (so we have FunctionInfo)
2. Ask the LLM (gpt-5 by default) to produce a C reproducer using
libarchive's public API + the attacker_scenario the judge produced
3. Compile with gcc -fsanitize=address,undefined linked against the
built libarchive .so
4. Run with timeout; record signal / sanitizer output if any
5. Persist the outcome alongside the original judge_*.json record
Usage:
OPENROUTER_API_KEY=... \
BMC_AGENT_LLM_MODEL=openai/gpt-5 \
python scripts/dynamic_validate_confirmed.py <judge_output_dir>
"""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO))
from bmc_agent.config import Config
from bmc_agent.llm import LLMClient
from bmc_agent.parser import parse_c_file
from bmc_agent.preprocessor import preprocess
from bmc_agent.judge_pipeline import (
_dynamic_validate_bug,
_DEFAULT_LIBARCHIVE_BUILD,
_DEFAULT_LIBARCHIVE_INC,
)
CORPUS = Path("/tmp/libarchive_seedhunt_full")
BUILD_INC = "/tmp/libarchive_bench/libarchive/build"
LIBA_INC = "/tmp/libarchive_bench/libarchive/libarchive"
def main():
if len(sys.argv) < 2:
print(__doc__)
return 1
out_root = Path(sys.argv[1])
if not out_root.is_dir():
print(f"ERROR: not a dir: {out_root}")
return 1
# OpenAI env wiring (route via OpenRouter)
if os.environ.get("OPENROUTER_API_KEY") and not os.environ.get("BMC_AGENT_LLM_API_KEY"):
os.environ["BMC_AGENT_LLM_API_KEY"] = os.environ["OPENROUTER_API_KEY"]
os.environ.setdefault("BMC_AGENT_LLM_BASE_URL", "https://openrouter.ai/api/v1")
os.environ.setdefault("BMC_AGENT_LLM_PROVIDER", "openai")
os.environ.setdefault("BMC_AGENT_LLM_MODEL", "openai/gpt-5")
config = Config.from_env()
config.llm_request_timeout_s = 600.0
llm = LLMClient(config)
# Cache parsed files
parsed_cache: dict[str, "ParsedCFile"] = {}
def _get_parsed(stem: str):
path = CORPUS / f"{stem}.c"
if not path.exists():
return None
if path.as_posix() not in parsed_cache:
try:
expanded = preprocess(
path, include_dirs=[BUILD_INC, LIBA_INC],
defines=["HAVE_CONFIG_H"],
)
parsed_cache[path.as_posix()] = parse_c_file(path, source_text=expanded)
except Exception as exc:
print(f" parse failed for {path}: {exc}")
return None
return parsed_cache[path.as_posix()]
# Walk judge_*.json, find confirmed adjacents
summary = []
for jf in sorted(out_root.rglob("judge_*.json")):
try:
d = json.load(open(jf))
except Exception:
continue
src_fn = jf.parent.name
stem = jf.parts[-3] # file_stem
confirmations = d.get("adjacent_confirmations") or []
any_confirmed = any(c.get("confirmed") for c in confirmations)
if not any_confirmed:
continue
parsed = _get_parsed(stem)
if parsed is None:
print(f"\n=== SKIP {jf} (no parsed file for stem {stem}) ===")
continue
for idx, cf in enumerate(confirmations):
if not cf.get("confirmed"):
continue
tgt = cf.get("target_function")
if not tgt or tgt not in parsed.functions:
print(f" skip: target {tgt!r} not in parsed file")
continue
target_func = parsed.get_function_info(tgt)
adj = cf.get("adjacent") or {}
scenario = (
cf.get("attacker_scenario")
or adj.get("attacker_scenario")
or ""
)
print(f"\n=== Dynamic-validating {stem}/{src_fn} → {tgt} ===")
print(f" bug_type: {(adj.get('bug_type') or '')[:90]}")
out_dir = jf.parent / "dynamic" / tgt
try:
result = _dynamic_validate_bug(
func=target_func,
attacker_scenario=scenario,
parsed_file=parsed,
libarchive_build_dir=_DEFAULT_LIBARCHIVE_BUILD,
libarchive_include_dir=_DEFAULT_LIBARCHIVE_INC,
llm=llm,
out_dir=out_dir,
)
except Exception as exc:
result = {"outcome": "exception", "reason": str(exc)}
print(f" outcome={result.get('outcome')} signal={result.get('signal_name')}")
if result.get("stderr_excerpt"):
# Show first ASan/UBSan summary line if present
for line in result["stderr_excerpt"].splitlines():
if "Sanitizer" in line or "runtime error" in line:
print(f" !! {line[:200]}")
break
cf["dynamic_validation"] = result
summary.append({
"source_judge": str(jf),
"src_fn": src_fn,
"target_fn": tgt,
"bug_type": adj.get("bug_type"),
"outcome": result.get("outcome"),
"signal": result.get("signal_name"),
})
# Persist back the updated record
with open(jf, "w") as f:
json.dump(d, f, indent=2)
# Top-line
print()
print("=" * 80)
print(f"Summary: {len(summary)} confirmed bugs dynamic-validated")
print("=" * 80)
print(f"{'src_fn':28s} {'target_fn':28s} {'outcome':22s} {'signal':10s}")
print("-" * 90)
counts = {}
for s in summary:
oc = s["outcome"]
counts[oc] = counts.get(oc, 0) + 1
print(f"{s['src_fn'][:28]:28s} {(s['target_fn'] or '?')[:28]:28s} "
f"{oc:22s} {(s['signal'] or '-')[:10]:10s}")
print()
print(f"Outcome tallies: {counts}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|