import argparse
import json
import os
import random
import re
import subprocess
import sys
from pathlib import Path
LEAK_PATTERNS = [
("FUN_", re.compile(r'\bFUN_[0-9a-f]+\b')),
("func_", re.compile(r'\bfunc_[0-9a-f]+\b')),
("DAT_", re.compile(r'\bDAT_[0-9a-f]+\b')),
("lVar", re.compile(r'\blVar[0-9]+\b')),
("iVar", re.compile(r'\biVar[0-9]+\b')),
("uVar", re.compile(r'\buVar[0-9]+\b')),
("field_", re.compile(r'->field_[0-9a-f]+')),
("local_", re.compile(r'\blocal_[0-9a-f]+\b')),
("tmp_", re.compile(r'\btmp_[0-9a-f]+\b')),
("?", re.compile(r'(?<![a-zA-Z0-9_])\?(?![a-zA-Z0-9_])')),
]
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--binary", required=True)
p.add_argument("--rsleigh", required=True)
p.add_argument("--ghidra", required=True, help="ghidra_output.json")
p.add_argument("--sample", type=int, default=50)
p.add_argument("--out", required=True)
p.add_argument("--worst-leakers", action="store_true",
help="emit worst-leakers JSON (for /fix-leaker loop)")
p.add_argument("-n", type=int, default=10,
help="number of worst leakers to emit with --worst-leakers")
return p.parse_args()
def detect_base_delta(ghidra_data, rsleigh, binary):
try:
out = subprocess.run([rsleigh, binary], capture_output=True, text=True, timeout=120).stdout
except subprocess.TimeoutExpired:
return 0
rs_addrs = set()
rs_by_name = {}
for line in out.splitlines():
m = re.match(r'\s+0x([0-9a-f]+)\s+(\S.*)$', line)
if m:
a = int(m.group(1), 16)
rs_addrs.add(a)
rs_by_name[m.group(2)] = a
if not rs_addrs:
return 0
gh_addrs = [int(str(b["address"]).rstrip("L"), 16) for b in ghidra_data.values()]
if not gh_addrs:
return 0
candidates = {0, 0x100000, 0x400000, 0x10000}
for name, body in ghidra_data.items():
gh_addr = int(str(body["address"]).rstrip("L"), 16)
if name in rs_by_name:
candidates.add(gh_addr - rs_by_name[name])
san = re.sub(r'[^A-Za-z0-9]+', "_", name).strip("_")
if san in rs_by_name:
candidates.add(gh_addr - rs_by_name[san])
best = (0, 0) sample_gh = gh_addrs[: min(500, len(gh_addrs))]
for delta in candidates:
matches = sum(1 for a in sample_gh if (a - delta) in rs_addrs)
if matches > best[1]:
best = (delta, matches)
return best[0]
def count_leaks(text):
return {label: len(pat.findall(text)) for label, pat in LEAK_PATTERNS}
def run_rsleigh(rsleigh, binary, addr, timeout=30):
try:
r = subprocess.run(
[rsleigh, binary, hex(addr)],
capture_output=True, text=True, timeout=timeout,
)
return r.stdout
except subprocess.TimeoutExpired:
return ""
def body_is_empty(text):
sigs = 0
stmts = 0
for l in text.splitlines():
t = l.strip()
if not t: continue
if t.startswith("//"): continue
if t in ("{", "}"): continue
if t.endswith("{") or t.startswith("}"): continue
if "(" in t and ")" in t and "{" in t and "return" not in t:
sigs += 1; continue
stmts += 1
return stmts <= 1
CONTROL_PATTERNS = {
"if": re.compile(r'\bif\s*\('),
"while": re.compile(r'\bwhile\s*\('),
"for": re.compile(r'\bfor\s*\('),
"do": re.compile(r'\bdo\s*\{'),
"switch": re.compile(r'\bswitch\s*\('),
}
def normalize_control_text(text):
lines = text.splitlines()
out = []
i = 0
while i < len(lines):
line = lines[i]
if line.lstrip().startswith("if") and (
"__stack_chk_guard" in line or "stack_chk_guard" in line
):
depth = line.count("{") - line.count("}")
i += 1
while i < len(lines) and depth > 0:
depth += lines[i].count("{") - lines[i].count("}")
i += 1
continue
if "__stack_chk_fail" in line or "stack_chk_guard" in line:
i += 1
continue
out.append(line)
i += 1
return "\n".join(out)
def has_control_flow(text):
norm = normalize_control_text(text)
return any(p.search(norm) for p in CONTROL_PATTERNS.values())
def control_flow_counts(text):
norm = normalize_control_text(text)
return {label: len(pattern.findall(norm)) for label, pattern in CONTROL_PATTERNS.items()}
def control_similarity(rs_text, gh_text):
rs = control_flow_counts(rs_text)
gh = control_flow_counts(gh_text)
diff = 0
total = 0
for k in rs:
diff += abs(rs[k] - gh[k])
total += max(rs[k], gh[k])
if total == 0:
return 1.0
return max(0.0, 1.0 - diff / total)
def main():
args = parse_args()
out_dir = Path(args.out)
out_dir.mkdir(parents=True, exist_ok=True)
with open(args.ghidra) as f:
ghidra = json.load(f)
delta = detect_base_delta(ghidra, args.rsleigh, args.binary)
if not args.worst_leakers:
print(f"[score] image-base delta: 0x{delta:x}")
gh_items = []
for name, body in ghidra.items():
addr = int(str(body["address"]).rstrip("L"), 16)
src = body.get("pseudocode", "")
gh_items.append((name, addr, src))
random.seed(42)
picks = [g for g in gh_items if 10 < g[2].count("\n") < 200]
random.shuffle(picks)
picks = picks[: args.sample]
per_func = []
rs_total_lines = 0
gh_total_lines = 0
rs_total_leaks = 0
gh_total_leaks = 0
rs_empty = 0
rs_missing = 0
rs_control_matches = 0
cflow_sim_total = 0.0
for name, gh_addr, gh_src in picks:
rs_addr = gh_addr - delta
rs_src = run_rsleigh(args.rsleigh, args.binary, rs_addr)
rs_lines = rs_src.count("\n")
gh_lines = gh_src.count("\n")
rs_leaks = sum(count_leaks(rs_src).values())
gh_leaks = sum(count_leaks(gh_src).values())
empty = body_is_empty(rs_src)
missing = rs_src.strip() == "" or "no instructions" in rs_src
control_match = has_control_flow(rs_src) == has_control_flow(gh_src)
rs_cflow = control_flow_counts(rs_src)
gh_cflow = control_flow_counts(gh_src)
rs_total_lines += rs_lines
gh_total_lines += gh_lines
rs_total_leaks += rs_leaks
gh_total_leaks += gh_leaks
if empty: rs_empty += 1
if missing: rs_missing += 1
if control_match: rs_control_matches += 1
cflow_sim_total += control_similarity(rs_src, gh_src)
per_func.append({
"name": name,
"gh_addr": f"0x{gh_addr:x}",
"rs_addr": f"0x{rs_addr:x}",
"rs_lines": rs_lines,
"gh_lines": gh_lines,
"rs_leaks": rs_leaks,
"gh_leaks": gh_leaks,
"empty": empty,
"missing": missing,
"control_match": control_match,
"rs_cflow": rs_cflow,
"gh_cflow": gh_cflow,
})
n = len(picks) or 1
avg_rs_lines = rs_total_lines / n
avg_gh_lines = gh_total_lines / n
avg_rs_leaks = rs_total_leaks / n
avg_gh_leaks = gh_total_leaks / n
line_ratio = 0.0
if avg_gh_lines > 0:
if avg_rs_lines >= avg_gh_lines:
line_ratio = 1.0 - abs(avg_rs_lines - avg_gh_lines) / avg_gh_lines
else:
if avg_rs_leaks <= avg_gh_leaks:
line_ratio = 1.0
else:
line_ratio = 1.0 - abs(avg_rs_lines - avg_gh_lines) / avg_gh_lines
line_ratio = max(0.0, min(1.0, line_ratio))
leak_ratio = 1.0
if avg_rs_leaks > 0:
leak_ratio = 1.0 - max(0.0, (avg_rs_leaks - avg_gh_leaks)) / avg_rs_leaks
leak_ratio = max(0.0, min(1.0, leak_ratio))
empty_ratio = 1.0 - (rs_empty / n)
control_ratio = rs_control_matches / n
cflow_sim = cflow_sim_total / n
discovery = 1.0 - (rs_missing / n)
score = (
discovery * 25 +
cflow_sim * 25 +
leak_ratio * 20 +
line_ratio * 15 +
empty_ratio * 15
)
report = {
"binary": args.binary,
"delta": f"0x{delta:x}",
"ghidra_funcs": len(ghidra),
"sample_size": n,
"avg_rs_lines": round(avg_rs_lines, 1),
"avg_gh_lines": round(avg_gh_lines, 1),
"avg_rs_leaks": round(avg_rs_leaks, 1),
"avg_gh_leaks": round(avg_gh_leaks, 1),
"rs_empty_bodies": rs_empty,
"rs_missing": rs_missing,
"control_flow_matches": rs_control_matches,
"scores": {
"discovery_coverage": round(discovery, 3),
"cflow_similarity": round(cflow_sim, 3),
"leak_parity": round(leak_ratio, 3),
"line_parity": round(line_ratio, 3),
"empty_rate": round(empty_ratio, 3),
"control_flow_binary": round(control_ratio, 3),
},
"composite_score": round(score, 1),
"per_func": per_func,
}
with open(out_dir / "report.json", "w") as f:
json.dump(report, f, indent=2)
if args.worst_leakers:
def classify(f):
if f["missing"]: return "missing"
if f["empty"]: return "empty"
line_gap = f["gh_lines"] - f["rs_lines"]
leak_gap = f["rs_leaks"] - f["gh_leaks"]
if leak_gap > 5: return "leak"
if line_gap > 15: return "line-gap"
if not f["control_match"]: return "cflow"
return "noise"
ranked = []
for f in per_func:
mode = classify(f)
score = 0
if mode == "missing": score = 1000
elif mode == "empty": score = 500
elif mode == "leak": score = max(0, f["rs_leaks"] - f["gh_leaks"])
elif mode == "line-gap": score = max(0, f["gh_lines"] - f["rs_lines"])
elif mode == "cflow": score = 50
ranked.append({**f, "failure_mode": mode, "severity": score})
ranked.sort(key=lambda x: -x["severity"])
out = {
"binary": args.binary,
"delta": f"0x{delta:x}",
"targets": ranked[: args.n],
}
print(json.dumps(out, indent=2))
return
md = []
md.append(f"# rsleigh vs Ghidra — {Path(args.binary).name}")
md.append("")
md.append(f"**Composite score:** `{score:.1f} / 100`")
md.append("")
md.append("## Aggregates")
md.append("")
md.append(f"| metric | rsleigh | ghidra |")
md.append(f"|------------------------|---------|--------|")
md.append(f"| avg lines / func | {avg_rs_lines:.1f} | {avg_gh_lines:.1f} |")
md.append(f"| avg leaks / func | {avg_rs_leaks:.1f} | {avg_gh_leaks:.1f} |")
md.append(f"| ghidra funcs total | — | {len(ghidra)} |")
md.append(f"| sample size | {n} | {n} |")
md.append(f"| rsleigh empty bodies | {rs_empty}/{n} | — |")
md.append(f"| rsleigh missing | {rs_missing}/{n} | — |")
md.append(f"| control-flow matches | {rs_control_matches}/{n} | — |")
md.append("")
md.append("## Score breakdown")
md.append("")
md.append("| component | weight | value | contrib |")
md.append("|--------------------|--------|-------|---------|")
md.append(f"| discovery_coverage | 25 | {discovery:.3f} | {discovery*25:.1f} |")
md.append(f"| cflow_similarity | 25 | {cflow_sim:.3f} | {cflow_sim*25:.1f} |")
md.append(f"| leak_parity | 20 | {leak_ratio:.3f} | {leak_ratio*20:.1f} |")
md.append(f"| line_parity | 15 | {line_ratio:.3f} | {line_ratio*15:.1f} |")
md.append(f"| empty_rate | 15 | {empty_ratio:.3f} | {empty_ratio*15:.1f} |")
md.append(f"| **total** | **100** | — | **{score:.1f}** |")
md.append("")
md.append("## Worst 10 (by rsleigh leak count)")
md.append("")
md.append("| fn | rs_lines | gh_lines | rs_leaks | gh_leaks |")
md.append("|----|----------|----------|----------|----------|")
worst = sorted(per_func, key=lambda x: -x["rs_leaks"])[:10]
for w in worst:
md.append(f"| `{w['name'][:50]}` | {w['rs_lines']} | {w['gh_lines']} | {w['rs_leaks']} | {w['gh_leaks']} |")
with open(out_dir / "report.md", "w") as f:
f.write("\n".join(md) + "\n")
bar = "=" * 62
print()
print(bar)
print(f" rsleigh vs Ghidra — {Path(args.binary).name}")
print(bar)
print(f" image-base delta: 0x{delta:x}")
print(f" ghidra funcs total: {len(ghidra)}")
print(f" sample size: {n}")
print()
print(f" {'metric':<24} {'rsleigh':>10} {'ghidra':>10}")
print(f" {'-'*24} {'-'*10} {'-'*10}")
print(f" {'avg lines / func':<24} {avg_rs_lines:>10.1f} {avg_gh_lines:>10.1f}")
print(f" {'avg leaks / func':<24} {avg_rs_leaks:>10.1f} {avg_gh_leaks:>10.1f}")
print(f" {'empty bodies':<24} {rs_empty:>4}/{n:<5} {'—':>10}")
print(f" {'missing/no-instr':<24} {rs_missing:>4}/{n:<5} {'—':>10}")
print(f" {'control-flow matches':<24} {rs_control_matches:>4}/{n:<5} {'—':>10}")
print()
print(f" {'component':<22} {'weight':>6} {'value':>6} {'pts':>6}")
print(f" {'-'*22} {'-'*6} {'-'*6} {'-'*6}")
print(f" {'discovery_coverage':<22} {25:>6} {discovery:>6.3f} {discovery*25:>6.1f}")
print(f" {'cflow_similarity':<22} {25:>6} {cflow_sim:>6.3f} {cflow_sim*25:>6.1f}")
print(f" {'leak_parity':<22} {20:>6} {leak_ratio:>6.3f} {leak_ratio*20:>6.1f}")
print(f" {'line_parity':<22} {15:>6} {line_ratio:>6.3f} {line_ratio*15:>6.1f}")
print(f" {'empty_rate':<22} {15:>6} {empty_ratio:>6.3f} {empty_ratio*15:>6.1f}")
print(f" {'-'*22} {'-'*6} {'-'*6} {'-'*6}")
print(f" {'COMPOSITE':<22} {100:>6} {'':>6} {score:>6.1f}")
print(bar)
verdict = "EXCELLENT" if score >= 80 else \
"GOOD" if score >= 60 else \
"FAIR" if score >= 40 else \
"POOR"
print(f" VERDICT: {verdict} ({score:.1f}/100)")
print(bar)
print(f" reports: {out_dir}/report.md , report.json")
print()
if __name__ == "__main__":
main()