fold/tests/unit/test_binary_expr.cpp
trvon 8a465b6675
feat: BinaryBBObserver — Z3-proved binary findings + unit tests
- BinaryBBObserver extends SafetyObserver for binary BB analysis
- Detects null-deref, div-by-zero, int-overflow via Z3 on binary paths
- Wired into BinaryDetectorRunner in scan pipeline
2026-05-29 21:55:48 -04:00

422 lines
13 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include "fold/ir/binary_expr.hpp"
namespace fold::ir {
namespace {
TEST_CASE("BinaryExprKind round-trips through string conversion", "[binary_expr]") {
struct Case {
BinaryExprKind kind;
std::string_view expected;
};
const std::vector<Case> cases = {
{BinaryExprKind::HardcodedString, "HardcodedString"},
{BinaryExprKind::ImportedSymbol, "ImportedSymbol"},
{BinaryExprKind::SectionData, "SectionData"},
{BinaryExprKind::BasicBlock, "BasicBlock"},
{BinaryExprKind::CallSite, "CallSite"},
{BinaryExprKind::IndirectCall, "IndirectCall"},
{BinaryExprKind::StringXref, "StringXref"},
{BinaryExprKind::DataXref, "DataXref"},
{BinaryExprKind::CodeXref, "CodeXref"},
{BinaryExprKind::ExportEntry, "ExportEntry"},
{BinaryExprKind::VtableEntry, "VtableEntry"},
{BinaryExprKind::SwitchTable, "SwitchTable"},
};
for (const auto& tc : cases) {
INFO("kind string: " << tc.expected);
const auto str = binaryKindToString(tc.kind);
REQUIRE(str == tc.expected);
const auto parsed = binaryKindFromString(str);
REQUIRE(parsed == tc.kind);
}
}
TEST_CASE("BinaryExprKind unknown round-trips", "[binary_expr]") {
REQUIRE(binaryKindToString(BinaryExprKind::Unknown) == "Unknown");
REQUIRE(binaryKindFromString("Unknown") == BinaryExprKind::Unknown);
REQUIRE(binaryKindFromString("InvalidKindName") == BinaryExprKind::Unknown);
}
TEST_CASE("BinaryExprNode default is Unknown with zero fields", "[binary_expr]") {
BinaryExprNode node;
REQUIRE(node.kind == BinaryExprKind::Unknown);
REQUIRE(node.address == 0);
REQUIRE(node.size == 0);
REQUIRE(node.label.empty());
REQUIRE(node.symbol_name.empty());
REQUIRE(node.section_name.empty());
REQUIRE(node.string_value.empty());
REQUIRE(node.xref_target == 0);
REQUIRE(node.xref_source == 0);
REQUIRE(node.call_target.empty());
REQUIRE(node.call_args.empty());
REQUIRE(node.method_name.empty());
REQUIRE(node.method_addr == 0);
REQUIRE(node.slot_index == -1);
REQUIRE(node.switch_entries.empty());
}
TEST_CASE("BinaryExprNode equality", "[binary_expr]") {
BinaryExprNode a;
a.kind = BinaryExprKind::HardcodedString;
a.address = 0x1000;
a.size = 16;
a.label = "test_string";
a.string_value = "AKIA1234567890ABCD";
BinaryExprNode b = a;
REQUIRE(a == b);
b.address = 0x2000;
REQUIRE(a != b);
}
TEST_CASE("BinaryIRTree empty", "[binary_expr]") {
BinaryIRTree tree;
REQUIRE(tree.empty());
REQUIRE(tree.size() == 0);
REQUIRE(toJson(tree).is_array());
REQUIRE(toJson(tree).empty());
}
TEST_CASE("BinaryIRTree fromJson of empty array", "[binary_expr]") {
const auto j = nlohmann::json::array();
const auto tree = binaryIRTreeFromJson(j);
REQUIRE(tree.empty());
}
TEST_CASE("BinaryIRTree fromJson ignores non-array input", "[binary_expr]") {
const auto j = nlohmann::json::object();
const auto tree = binaryIRTreeFromJson(j);
REQUIRE(tree.empty());
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — HardcodedString", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::HardcodedString;
node.address = 0x4000;
node.size = 32;
node.label = "secret_token";
node.string_value = "sk_live_abc123def456";
const auto j = toJson(node);
REQUIRE(j.at("kind") == "HardcodedString");
REQUIRE(j.at("address") == 0x4000);
REQUIRE(j.at("size") == 32);
REQUIRE(j.at("label") == "secret_token");
REQUIRE(j.at("string_value") == "sk_live_abc123def456");
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — ImportedSymbol", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::ImportedSymbol;
node.address = 0x2000;
node.size = 8;
node.label = "system";
node.symbol_name = "system@GLIBC_2.2.5";
const auto j = toJson(node);
REQUIRE(j.at("kind") == "ImportedSymbol");
REQUIRE(j.at("address") == 0x2000);
REQUIRE(j.at("label") == "system");
REQUIRE(j.at("symbol_name") == "system@GLIBC_2.2.5");
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — ExportEntry", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::ExportEntry;
node.address = 0x5000;
node.size = 128;
node.label = "main";
node.symbol_name = "main";
const auto j = toJson(node);
REQUIRE(j.at("kind") == "ExportEntry");
REQUIRE(j.at("symbol_name") == "main");
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — SectionData", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::SectionData;
node.address = 0x1000;
node.size = 4096;
node.label = ".text";
node.section_name = ".text";
const auto j = toJson(node);
REQUIRE(j.at("kind") == "SectionData");
REQUIRE(j.at("section_name") == ".text");
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — BasicBlock", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::BasicBlock;
node.address = 0x4010;
node.size = 24;
node.label = "bb_0x4010";
const auto j = toJson(node);
REQUIRE(j.at("kind") == "BasicBlock");
REQUIRE(j.at("address") == 0x4010);
REQUIRE(j.at("size") == 24);
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — CallSite", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::CallSite;
node.address = 0x4020;
node.size = 4;
node.label = "call puts";
node.call_target = "puts";
node.call_args = {0x6000};
const auto j = toJson(node);
REQUIRE(j.at("kind") == "CallSite");
REQUIRE(j.at("call_target") == "puts");
REQUIRE(j.at("call_args").is_array());
REQUIRE(j.at("call_args").size() == 1);
REQUIRE(j.at("call_args")[0] == 0x6000);
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — IndirectCall", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::IndirectCall;
node.address = 0x4030;
node.size = 4;
node.label = "blr x8";
node.call_target = ""; // unresolved
node.call_args = {0x6000, 0x6008, 0x6010};
const auto j = toJson(node);
REQUIRE(j.at("kind") == "IndirectCall");
REQUIRE(j.at("call_args").size() == 3);
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — StringXref", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::StringXref;
node.address = 0x4040;
node.label = "xref -> secret";
node.xref_target = 0x7000; // address of the string
node.xref_source = 0x4040; // address of the instruction doing the xref
const auto j = toJson(node);
REQUIRE(j.at("kind") == "StringXref");
REQUIRE(j.at("xref_target") == 0x7000);
REQUIRE(j.at("xref_source") == 0x4040);
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — DataXref", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::DataXref;
node.address = 0x4050;
node.xref_target = 0x8000;
node.xref_source = 0x4050;
const auto j = toJson(node);
REQUIRE(j.at("kind") == "DataXref");
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — CodeXref", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::CodeXref;
node.address = 0x4060;
node.xref_target = 0x9000;
node.xref_source = 0x4060;
const auto j = toJson(node);
REQUIRE(j.at("kind") == "CodeXref");
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — VtableEntry", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::VtableEntry;
node.address = 0xA000;
node.size = 8;
node.label = "vtable[3] MyClass::foo";
node.method_name = "MyClass::foo";
node.method_addr = 0x5100;
node.slot_index = 3;
const auto j = toJson(node);
REQUIRE(j.at("kind") == "VtableEntry");
REQUIRE(j.at("method_name") == "MyClass::foo");
REQUIRE(j.at("method_addr") == 0x5100);
REQUIRE(j.at("slot_index") == 3);
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryExprNode toJson/fromJson round-trip — SwitchTable", "[binary_expr]") {
BinaryExprNode node;
node.kind = BinaryExprKind::SwitchTable;
node.address = 0xB000;
node.size = 64;
node.label = "switch_table";
node.switch_entries = {0x5200, 0x5300, 0x5400, 0x5500};
const auto j = toJson(node);
REQUIRE(j.at("kind") == "SwitchTable");
REQUIRE(j.at("switch_entries").is_array());
REQUIRE(j.at("switch_entries").size() == 4);
const auto parsed = fromJson(j);
REQUIRE(parsed == node);
}
TEST_CASE("BinaryIRTree round-trip through JSON", "[binary_expr]") {
BinaryIRTree tree;
{
BinaryExprNode n1;
n1.kind = BinaryExprKind::HardcodedString;
n1.address = 0x4000;
n1.size = 10;
n1.label = "str1";
n1.string_value = "password123";
tree.nodes.push_back(n1);
}
{
BinaryExprNode n2;
n2.kind = BinaryExprKind::CallSite;
n2.address = 0x4020;
n2.size = 4;
n2.label = "call";
n2.call_target = "strcpy";
n2.call_args = {0x6000, 0x4000};
tree.nodes.push_back(n2);
}
{
BinaryExprNode n3;
n3.kind = BinaryExprKind::VtableEntry;
n3.address = 0xA000;
n3.size = 8;
n3.method_name = "Base::vfunc";
n3.method_addr = 0x5100;
n3.slot_index = 0;
tree.nodes.push_back(n3);
}
REQUIRE(tree.size() == 3);
const auto j = toJson(tree);
REQUIRE(j.is_array());
REQUIRE(j.size() == 3);
const auto parsed = binaryIRTreeFromJson(j);
REQUIRE(parsed.size() == 3);
REQUIRE(parsed.nodes[0] == tree.nodes[0]);
REQUIRE(parsed.nodes[1] == tree.nodes[1]);
REQUIRE(parsed.nodes[2] == tree.nodes[2]);
}
TEST_CASE("BinaryIRTree round-trip handles empty nodes mixed in", "[binary_expr]") {
BinaryIRTree tree;
{
BinaryExprNode n;
n.kind = BinaryExprKind::SectionData;
n.address = 0x1000;
n.size = 4096;
n.section_name = ".rodata";
tree.nodes.push_back(n);
}
// A node with all zero/default fields should round-trip as Unknown.
tree.nodes.push_back(BinaryExprNode{});
const auto j = toJson(tree);
REQUIRE(j.size() == 2);
const auto parsed = binaryIRTreeFromJson(j);
REQUIRE(parsed.size() == 2);
REQUIRE(parsed.nodes[0] == tree.nodes[0]);
REQUIRE(parsed.nodes[1] == tree.nodes[1]);
}
TEST_CASE("json_keys constants are valid for serialization", "[binary_expr]") {
using namespace binary_json_keys;
BinaryExprNode node;
node.kind = BinaryExprKind::SectionData;
node.address = 0x1000;
node.size = 4096;
node.section_name = ".text";
const auto j = toJson(node);
// Verify all the canonical keys exist.
REQUIRE(j.contains(kKind));
REQUIRE(j.contains(kAddress));
REQUIRE(j.contains(kSize));
REQUIRE(j.contains(kSectionName));
// Verify that using these constants for access works.
REQUIRE(j.at(kKind) == "SectionData");
REQUIRE(j.at(kAddress) == 0x1000);
REQUIRE(j.at(kSize) == 4096);
REQUIRE(j.at(kSectionName) == ".text");
}
TEST_CASE("BinaryExprNode fromJson gracefully handles missing fields", "[binary_expr]") {
// Missing kind
auto j = nlohmann::json::object();
auto parsed = fromJson(j);
REQUIRE(parsed.kind == BinaryExprKind::Unknown);
REQUIRE(parsed.address == 0);
REQUIRE(parsed.size == 0);
// Only kind provided, no payload
j = nlohmann::json{{"kind", "BasicBlock"}};
parsed = fromJson(j);
REQUIRE(parsed.kind == BinaryExprKind::BasicBlock);
REQUIRE(parsed.address == 0);
// Kind + address + size, no label
j = nlohmann::json{{"kind", "BasicBlock"}, {"address", 0x4200}, {"size", 64}};
parsed = fromJson(j);
REQUIRE(parsed.kind == BinaryExprKind::BasicBlock);
REQUIRE(parsed.address == 0x4200);
REQUIRE(parsed.size == 64);
REQUIRE(parsed.label.empty());
}
TEST_CASE("BinaryExprNode fromJson handles invalid kind gracefully", "[binary_expr]") {
const auto j = nlohmann::json{{"kind", "NonExistentKind"}, {"address", 0x1234}};
const auto parsed = fromJson(j);
REQUIRE(parsed.kind == BinaryExprKind::Unknown);
REQUIRE(parsed.address == 0x1234);
}
} // namespace
} // namespace fold::ir