// Unit tests for the pure UAF/double-free disassembly state machine // (scanInstructionsForUaf), exercised with synthetic ARM64 instruction // sequences — no radare2 required. Dangling state is keyed on the stack slot // a heap pointer lives in (r2 names it, e.g. `[ptr]`). #include #include #include #include #include #include "fold/binary/r2_backend.hpp" #include "fold/report/finding.hpp" namespace { using Op = std::pair; std::vector seq(std::initializer_list lines) { std::vector ops; std::uint64_t addr = 0x1000; for (const auto& l : lines) ops.emplace_back(addr += 4, l); return ops; } } // namespace TEST_CASE("uaf: double free on the same stack slot is detected", "[binary][uaf]") { // malloc -> store to ptr -> free(ptr) -> reload ptr -> free(ptr) again. auto hazards = fold::binary::scanInstructionsForUaf(seq({ "mov x0, 0x40", "bl sym.imp.malloc", "str x0, [ptr]", "ldr x0, [ptr]", "bl sym.imp.free", "ldr x0, [ptr]", "bl sym.imp.free", })); REQUIRE(hazards.size() == 1); REQUIRE(hazards[0].kind == fold::FindingKind::DoubleFree); REQUIRE(hazards[0].slot == "ptr"); } TEST_CASE("uaf: free then pass freed pointer to a call is use-after-free", "[binary][uaf]") { auto hazards = fold::binary::scanInstructionsForUaf(seq({ "str x0, [dest]", "ldr x0, [dest]", "bl sym.imp.free", "ldr x0, [dest]", "bl sym.imp.__strcpy_chk", })); REQUIRE(hazards.size() == 1); REQUIRE(hazards[0].kind == fold::FindingKind::UseAfterFree); REQUIRE(hazards[0].slot == "dest"); REQUIRE(hazards[0].callee == "strcpy"); // _chk suffix normalized away } TEST_CASE("uaf: a single well-formed free is not flagged", "[binary][uaf]") { auto hazards = fold::binary::scanInstructionsForUaf(seq({ "str x0, [ptr]", "ldr x0, [ptr]", "bl sym.imp.free", "ret", })); REQUIRE(hazards.empty()); } TEST_CASE("uaf: free + reallocate + free is legitimate (slot revived)", "[binary][uaf]") { // Storing a fresh allocation back into the slot clears the dangling state, // so the second free is on a live pointer. auto hazards = fold::binary::scanInstructionsForUaf(seq({ "ldr x0, [ptr]", "bl sym.imp.free", "mov x0, 0x40", "bl sym.imp.malloc", "str x0, [ptr]", "ldr x0, [ptr]", "bl sym.imp.free", })); REQUIRE(hazards.empty()); } TEST_CASE("uaf: freeing one slot does not taint a different slot", "[binary][uaf]") { auto hazards = fold::binary::scanInstructionsForUaf(seq({ "ldr x0, [a]", "bl sym.imp.free", "ldr x0, [b]", "bl sym.imp.strcpy", })); REQUIRE(hazards.empty()); }