Abstract-block mode fired one over-approximate onBinaryArith per block with fresh unconstrained operands, so it proved nothing for div-by-zero / null-deref (0 binary findings on libdiv_by_zero / libnull_deref). A full per-instruction symbolic encoder with path modeling is the eventual fix, but on symbolic args an unconstrained overflow/aliasing check is trivially SAT and would fire on every add/sub/mul (the FP blowup) — so this slice lands the sound, zero-FP subset: constant-zero propagation. scanInstructionsForConstHazards (pure, unit-tested): a per-function state machine tracking which registers / stack slots provably hold 0. mov r,0 | mov r,wzr | str wzr,[slot] | ldr r,[zeroed-slot] -> r/slot is 0 sdiv/udiv d,a,b with b==0 -> div by zero ldr/str through a zero base register [x0] -> null deref Tracking resets at every branch / call / ret, so it never reasons across an unmodeled path — no false positives, at the cost of missing path-dependent zeros (div_by_zero_variable, null_deref_conditional remain FN: those need branch modeling, deferred). Both structural disasm passes (this + UAF) now share one per-function pdfj sweep via disasmFindingsInSession; const-hazard scanning runs default-on alongside the import/string detectors and under --detectors security/div-by-zero/null-deref. Findings carry Heuristic confidence. Validated on the CTF corpus: +3 TP — div_by_zero_direct, mod_by_zero (CWE-369), null_deref_direct (CWE-476), all previously FN in binary mode — with zero new findings across the other 8 dylibs. 6 r2-free unit tests (test_binary_const_hazards) cover literal-zero divisor, slot-propagated null deref, non-zero/unknown divisors, branch-reset, and non-zero pointer negatives. 70 Ok / 0 Fail / 1 pre-existing binary_backend timeout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
86 lines
2.7 KiB
C++
86 lines
2.7 KiB
C++
// Unit tests for the constant-zero hazard state machine
|
|
// (scanInstructionsForConstHazards), exercised with synthetic ARM64 sequences
|
|
// — no radare2. The pass proves division-by-zero (divisor is a constant 0) and
|
|
// null-dereference (deref through a constant-0 register) without path modeling,
|
|
// and resets tracking at every branch so it never reasons across a path.
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "fold/binary/r2_backend.hpp"
|
|
#include "fold/report/finding.hpp"
|
|
|
|
namespace {
|
|
|
|
using Op = std::pair<std::uint64_t, std::string>;
|
|
|
|
std::vector<Op> seq(std::initializer_list<std::string> lines) {
|
|
std::vector<Op> ops;
|
|
std::uint64_t addr = 0x1000;
|
|
for (const auto& l : lines)
|
|
ops.emplace_back(addr += 4, l);
|
|
return ops;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("const-hazard: sdiv by a literal-zero register is division by zero", "[binary][divz]") {
|
|
auto h = fold::binary::scanInstructionsForConstHazards(seq({
|
|
"ldr w8, [var_ch]",
|
|
"mov w9, 0",
|
|
"sdiv w0, w8, w9",
|
|
}));
|
|
REQUIRE(h.size() == 1);
|
|
REQUIRE(h[0].kind == fold::FindingKind::DivByZero);
|
|
REQUIRE(h[0].reg == "w9");
|
|
}
|
|
|
|
TEST_CASE("const-hazard: deref of a zero pointer loaded from a zeroed slot is null-deref",
|
|
"[binary][nullderef]") {
|
|
auto h = fold::binary::scanInstructionsForConstHazards(seq({
|
|
"str xzr, [var_8h]",
|
|
"ldr x8, [var_8h]",
|
|
"ldr w0, [x8]",
|
|
}));
|
|
REQUIRE(h.size() == 1);
|
|
REQUIRE(h[0].kind == fold::FindingKind::NullDeref);
|
|
REQUIRE(h[0].reg == "x8");
|
|
}
|
|
|
|
TEST_CASE("const-hazard: a non-zero divisor is not flagged", "[binary][divz]") {
|
|
auto h = fold::binary::scanInstructionsForConstHazards(seq({
|
|
"mov w9, 5",
|
|
"sdiv w0, w8, w9",
|
|
}));
|
|
REQUIRE(h.empty());
|
|
}
|
|
|
|
TEST_CASE("const-hazard: an unknown (loaded) divisor is not flagged", "[binary][divz]") {
|
|
auto h = fold::binary::scanInstructionsForConstHazards(seq({
|
|
"ldr w9, [var_4h]",
|
|
"sdiv w0, w8, w9",
|
|
}));
|
|
REQUIRE(h.empty());
|
|
}
|
|
|
|
TEST_CASE("const-hazard: tracking is reset across a branch (no cross-path zero)",
|
|
"[binary][divz]") {
|
|
// The zero in w9 must not carry past the branch to the sdiv: that would be
|
|
// an unmodeled-path inference and a potential false positive.
|
|
auto h = fold::binary::scanInstructionsForConstHazards(seq({
|
|
"mov w9, 0",
|
|
"cbnz w8, 0x2000",
|
|
"sdiv w0, w8, w9",
|
|
}));
|
|
REQUIRE(h.empty());
|
|
}
|
|
|
|
TEST_CASE("const-hazard: deref through a non-zero pointer is not flagged", "[binary][nullderef]") {
|
|
auto h = fold::binary::scanInstructionsForConstHazards(seq({
|
|
"ldr x8, [var_8h]", // var_8h not known-zero
|
|
"ldr w0, [x8]",
|
|
}));
|
|
REQUIRE(h.empty());
|
|
}
|