// 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 #include #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(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 in) { PcodeOp p; p.op = op; p.out = out; p.in = std::move(in); return p; } PcodeOp mkNoOut(Op op, std::vector 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()); }