fold/tests/unit/test_binary_const_hazards.cpp
trvon f4f59ce23c
feat(binary): constant-zero div-by-zero + null-deref over disassembly
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>
2026-05-31 15:02:43 -04:00

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());
}