- 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
422 lines
13 KiB
C++
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
|