Propagate "writes-unbounded into pointer-param k" across call hops so a buffer passed through a wrapper (2+ levels) is still flagged — no third-party call graph needed; the raw P-Code stream already gives resolved call edges. - pcode_summary: PointerWriteSummary gains forwarding edges — a Call that passes one of the function's own pure-input pointer params straight through to a callee argument (my_param → callee:arg), detected via in_def + paramIndexOf. Forwards are computed regardless of the loop/size-bound gates (a non-looping wrapper still forwards). - pcode_router: expose annotateCallTargets(ssa, raw, names) — factor the raw→site→SSA name correlation so the summary pre-pass can resolve callee names for forwarding edges. - binary_pcode_scan: the pre-pass now annotates each function's SSA call targets (via a raw lift) before summarizing, collects direct + forwarding summaries, then runs a worklist fixpoint: a function forwarding a pure-param pointer to an unbounded-writer parameter becomes an unbounded writer on that param. The closure feeds both the interprocedural detector and the write-loop prototype set (so multi-hop callers recover the buffer arg). Validated: baby_bof (AArch64 + x86-64) still fires; corpus BOF counts unchanged (3/1/1); the x86-64 static stays at 1 finding (the transitive closure adds no FP). Unit: test_pcode_summary +forwarding cases (14 assertions). meson test 77/77. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
6.2 KiB
C++
151 lines
6.2 KiB
C++
// Unit tests for summarizePointerWrites — the pure interprocedural write-loop
|
|
// summary. Builds synthetic ir::pcode::Function inputs (no r2/Ghidra) and checks
|
|
// which pointer params are flagged as unbounded-written.
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <vector>
|
|
|
|
#include "fold/ir/pcode.hpp"
|
|
#include "fold/symbolic/pcode_summary.hpp"
|
|
|
|
namespace {
|
|
using namespace fold::ir::pcode;
|
|
using fold::symbolic::summarizePointerWrites;
|
|
|
|
Varnode x(int n) { return {Space::Register, 0x4000u + 0x8u * static_cast<unsigned>(n), 8}; }
|
|
Varnode uniq(std::uint64_t off, std::uint32_t sz = 8) { return {Space::Unique, off, sz}; }
|
|
Varnode con(std::uint64_t v, std::uint32_t sz = 8) { return {Space::Constant, v, sz}; }
|
|
|
|
PcodeOp mk(Op op, Varnode out, std::vector<Varnode> in) {
|
|
PcodeOp p;
|
|
p.op = op;
|
|
p.out = out;
|
|
p.in = std::move(in);
|
|
return p;
|
|
}
|
|
PcodeOp mkNoOut(Op op, std::vector<Varnode> in) {
|
|
PcodeOp p;
|
|
p.op = op;
|
|
p.in = std::move(in);
|
|
return p;
|
|
}
|
|
|
|
// Loop marker: an SSA phi defining the index slot (uniq 0x10).
|
|
PcodeOp loopPhi() { return mk(Op::MultiEqual, uniq(0x10, 4), {con(0, 4), con(1, 4)}); }
|
|
|
|
bool marks(const Function& fn, int param) {
|
|
return summarizePointerWrites(fn).writesUnbounded(param);
|
|
}
|
|
} // namespace
|
|
|
|
TEST_CASE("summary: store through param1 + loop index is unbounded write", "[summary]") {
|
|
// out = INT_ADD(x1, idx) ; STORE [out], v with a loop present.
|
|
Function fn;
|
|
fn.name = "decoder";
|
|
fn.ops.push_back(loopPhi());
|
|
fn.ops.push_back(mk(Op::IntAdd, uniq(0x100), {x(1), uniq(0x10, 4)}));
|
|
fn.ops.push_back(mkNoOut(Op::Store, {con(1), uniq(0x100), con(0x41, 1)}));
|
|
REQUIRE(marks(fn, 1));
|
|
REQUIRE_FALSE(marks(fn, 0));
|
|
}
|
|
|
|
TEST_CASE("summary: address via CAST chain is still resolved", "[summary]") {
|
|
Function fn;
|
|
fn.name = "decoder_cast";
|
|
fn.ops.push_back(loopPhi());
|
|
fn.ops.push_back(mk(Op::IntAdd, uniq(0x100), {x(0), uniq(0x10, 4)}));
|
|
fn.ops.push_back(mk(Op::Cast, uniq(0x200), {uniq(0x100)}));
|
|
fn.ops.push_back(mkNoOut(Op::Store, {con(1), uniq(0x200), con(0x41, 1)}));
|
|
REQUIRE(marks(fn, 0));
|
|
}
|
|
|
|
TEST_CASE("summary: a constant-index store (param + const) is NOT flagged", "[summary]") {
|
|
// STORE [x0 + 8] is a single fixed write, not an unbounded fill — even with a
|
|
// loop present, a constant subscript is not a write-loop overflow.
|
|
Function fn;
|
|
fn.name = "fixed_field";
|
|
fn.ops.push_back(loopPhi());
|
|
fn.ops.push_back(mk(Op::IntAdd, uniq(0x100), {x(0), con(8)}));
|
|
fn.ops.push_back(mkNoOut(Op::Store, {con(1), uniq(0x100), con(0x41, 1)}));
|
|
REQUIRE(summarizePointerWrites(fn).empty());
|
|
}
|
|
|
|
TEST_CASE("summary: a scratch-reused (written) param register base is NOT flagged", "[summary]") {
|
|
// x0 is overwritten (used as scratch), so it is not a pure-input pointer —
|
|
// the store base must be the parameter as passed, not a recomputed value.
|
|
Function fn;
|
|
fn.name = "scratch_base";
|
|
fn.ops.push_back(loopPhi());
|
|
fn.ops.push_back(mk(Op::IntAdd, x(0), {uniq(0x300), con(1)})); // x0 written (scratch)
|
|
fn.ops.push_back(mk(Op::IntAdd, uniq(0x100), {x(0), uniq(0x10, 4)}));
|
|
fn.ops.push_back(mkNoOut(Op::Store, {con(1), uniq(0x100), con(0x41, 1)}));
|
|
REQUIRE(summarizePointerWrites(fn).empty());
|
|
}
|
|
|
|
TEST_CASE("summary: a straight-line store (no loop) is NOT flagged", "[summary]") {
|
|
Function fn;
|
|
fn.name = "no_loop";
|
|
fn.ops.push_back(mk(Op::IntAdd, uniq(0x100), {x(1), con(8)}));
|
|
fn.ops.push_back(mkNoOut(Op::Store, {con(1), uniq(0x100), con(0x41, 1)}));
|
|
REQUIRE(summarizePointerWrites(fn).empty());
|
|
}
|
|
|
|
TEST_CASE("summary: x86-64 SysV store through RSI param + scratch index is flagged", "[summary]") {
|
|
// Mirrors x86-64 -O0 lifting: the buffer pointer (output = RSI) is added to a
|
|
// scratch-reused index in RAX, and the store goes through RAX. Real SSA
|
|
// (in_def) distinguishes the RSI *input* from RAX the *scratch* def.
|
|
using fold::symbolic::ParamAbi;
|
|
const Varnode rax{Space::Register, 0x0, 8}; // scratch
|
|
const Varnode rsi{Space::Register, 0x30, 8}; // SysV arg1 (output param)
|
|
Function fn;
|
|
fn.name = "decode_x64";
|
|
fn.ops.push_back(loopPhi()); // op0: loop marker
|
|
PcodeOp sext = mk(Op::IntSext, rax, {uniq(0x10, 4)}); // op1: RAX = sext(idx)
|
|
sext.in_def = {-1};
|
|
fn.ops.push_back(sext);
|
|
PcodeOp add = mk(Op::IntAdd, uniq(0x100), {rax, rsi}); // op2: RAX_idx + RSI
|
|
add.in_def = {1, -1}; // RAX defined at op1; RSI is input
|
|
fn.ops.push_back(add);
|
|
PcodeOp st = mkNoOut(Op::Store, {con(1), uniq(0x100), con(0x41, 1)}); // op3
|
|
st.in_def = {-1, 2, -1};
|
|
fn.ops.push_back(st);
|
|
REQUIRE(summarizePointerWrites(fn, ParamAbi::SysVX64).writesUnbounded(1));
|
|
// With the AArch64 ABI, 0x30 is not an arg register → not flagged.
|
|
REQUIRE(summarizePointerWrites(fn, ParamAbi::AArch64).empty());
|
|
}
|
|
|
|
TEST_CASE("summary: forwarding a pure-param pointer to a callee is recorded", "[summary]") {
|
|
// W(p) { g(p); } — p (x0) passed straight through as g's arg0 → forward edge.
|
|
Function fn;
|
|
fn.name = "wrapper";
|
|
PcodeOp call;
|
|
call.op = Op::Call;
|
|
call.call_target = "g";
|
|
call.in = {con(0x2000, 8), x(0)}; // in[0] target, in[1] = our param0
|
|
fn.ops.push_back(call);
|
|
auto fwd = summarizePointerWrites(fn).forwards;
|
|
REQUIRE(fwd.size() == 1);
|
|
REQUIRE(fwd[0].my_param == 0);
|
|
REQUIRE(fwd[0].callee_arg == 0);
|
|
REQUIRE(fwd[0].callee == "g");
|
|
}
|
|
|
|
TEST_CASE("summary: passing a non-param (local) to a callee is not a forward", "[summary]") {
|
|
Function fn;
|
|
fn.name = "wrapper2";
|
|
PcodeOp call;
|
|
call.op = Op::Call;
|
|
call.call_target = "g";
|
|
call.in = {con(0x2000, 8), uniq(0x300)}; // a local, not a parameter
|
|
fn.ops.push_back(call);
|
|
REQUIRE(summarizePointerWrites(fn).forwards.empty());
|
|
}
|
|
|
|
TEST_CASE("summary: store through a non-param base is NOT flagged", "[summary]") {
|
|
// base is a local (uniq), not an argument register.
|
|
Function fn;
|
|
fn.name = "local_write";
|
|
fn.ops.push_back(loopPhi());
|
|
fn.ops.push_back(mk(Op::IntAdd, uniq(0x100), {uniq(0x300), uniq(0x10, 4)}));
|
|
fn.ops.push_back(mkNoOut(Op::Store, {con(1), uniq(0x100), con(0x41, 1)}));
|
|
REQUIRE(summarizePointerWrites(fn).empty());
|
|
}
|