Problem: null-deref, div-by-zero and int-overflow re-check a portfolio verdict on the reporting solver and reconcile the two, but then memoized the re-check's unsat core (and read its model) from the raw re-check result. A portfolio SAT that the re-check called UNSAT became unknown, yet its core was still reused as the assumptions for later queries on the same path. The disagreement itself was silent: no stat, and the trace showed the portfolio's verdict. Fix: all six sites use symbolic::recheckPortfolioVerdict and act only on the reconciled verdict, so an unknown leaves no core or model behind. The helper counts SolverStats::portfolio_disagreements (merged, printed, and exported in the detector profile), and the sites trace "UNKNOWN (portfolio re-check disagreed)". Test: test_model_validation "[portfolio]" checks the helper's agreement, unknown and disagreement cases and the merged counter. Full suite: 140/140.
160 lines
7.1 KiB
C++
160 lines
7.1 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include "fold/detect/model_validation.hpp"
|
|
#include "fold/symbolic/tactics.hpp"
|
|
|
|
TEST_CASE("symbolic models validate argument indexes against available arity",
|
|
"[symbolic][model-validation]") {
|
|
using fold::detect::validModeledArgument;
|
|
CHECK(validModeledArgument(0, 1));
|
|
CHECK(validModeledArgument(1, 2, 2));
|
|
CHECK_FALSE(validModeledArgument(-1, 2));
|
|
CHECK_FALSE(validModeledArgument(2, 2));
|
|
CHECK_FALSE(validModeledArgument(1, 2, 1));
|
|
}
|
|
|
|
TEST_CASE("assumption checks fail closed on unsupported distinct translation",
|
|
"[symbolic][model-validation][translator]") {
|
|
z3::context solver_ctx;
|
|
z3::context expression_ctx;
|
|
z3::solver solver(solver_ctx, "QF_BV");
|
|
const z3::expr unknown = expression_ctx.bv_const("unknown", 64);
|
|
const z3::expr narrow = z3::zext(unknown.extract(7, 0), 56).simplify();
|
|
z3::expr_vector assumptions(expression_ctx);
|
|
assumptions.push_back(narrow != expression_ctx.bv_val(0, 64));
|
|
|
|
z3::check_result result = z3::sat;
|
|
CHECK_NOTHROW(result = fold::symbolic::checkWithAssumptionsFailClosed(solver, assumptions));
|
|
CHECK(result == z3::unknown);
|
|
}
|
|
|
|
TEST_CASE("portfolio profiling records bounded expression-free query telemetry",
|
|
"[symbolic][profile][portfolio]") {
|
|
z3::context ctx;
|
|
z3::solver solver(ctx, "QF_BV");
|
|
const z3::expr value = ctx.bv_const("value", 64);
|
|
solver.add(value == ctx.bv_val(7, 64));
|
|
z3::expr_vector assumptions(ctx);
|
|
|
|
fold::SolverStats stats;
|
|
stats.profiling_enabled = true;
|
|
const z3::check_result result =
|
|
fold::symbolic::checkWithPortfolio(solver, assumptions, 100, stats);
|
|
REQUIRE(result == z3::sat);
|
|
REQUIRE(stats.query_samples.size() == 1);
|
|
const auto& sample = stats.query_samples.front();
|
|
CHECK(sample.canonical_hash != 0);
|
|
CHECK(sample.ast_nodes >= 3);
|
|
CHECK(sample.max_depth >= 2);
|
|
CHECK(sample.symbols == 1);
|
|
CHECK(sample.assertions == 1);
|
|
CHECK(sample.max_bv_width == 64);
|
|
CHECK(sample.bv_ops >= 1);
|
|
CHECK(sample.equality_ops == 1);
|
|
CHECK((sample.winner == "smt" || sample.winner == "bitblast"));
|
|
CHECK(sample.result == "sat");
|
|
|
|
fold::SolverStats incremental_stats;
|
|
incremental_stats.profiling_enabled = true;
|
|
REQUIRE(fold::symbolic::checkWithAssumptionsProfiled(solver, assumptions, incremental_stats) ==
|
|
z3::sat);
|
|
REQUIRE(incremental_stats.query_samples.size() == 1);
|
|
CHECK(incremental_stats.query_samples[0].canonical_hash == sample.canonical_hash);
|
|
CHECK(incremental_stats.query_samples[0].winner == "incremental");
|
|
|
|
z3::solver renamed_solver(ctx, "QF_BV");
|
|
const z3::expr renamed_value = ctx.bv_const("different_generated_name_917", 64);
|
|
renamed_solver.add(renamed_value == ctx.bv_val(7, 64));
|
|
fold::SolverStats renamed_stats;
|
|
renamed_stats.profiling_enabled = true;
|
|
REQUIRE(fold::symbolic::checkWithAssumptionsProfiled(renamed_solver, assumptions,
|
|
renamed_stats) == z3::sat);
|
|
REQUIRE(renamed_stats.query_samples.size() == 1);
|
|
CHECK(renamed_stats.query_samples[0].canonical_hash == sample.canonical_hash);
|
|
|
|
for (std::size_t i = 0; i < fold::SolverStats::kMaxQuerySamples + 2; ++i)
|
|
stats.recordQuerySample({.canonical_hash = i + 1});
|
|
CHECK(stats.query_samples.size() == fold::SolverStats::kMaxQuerySamples);
|
|
CHECK(stats.query_samples_dropped == 3);
|
|
}
|
|
|
|
TEST_CASE("nonlinear bit-vector routing is shape-specific", "[symbolic][validation]") {
|
|
z3::context ctx;
|
|
const z3::expr x = ctx.bv_const("x", 64);
|
|
const z3::expr y = ctx.bv_const("y", 64);
|
|
z3::expr_vector assumptions(ctx);
|
|
|
|
z3::solver linear(ctx, "QF_BV");
|
|
linear.add(x == ctx.bv_val(4, 64));
|
|
CHECK_FALSE(fold::symbolic::isNonlinearBitVectorQuery(linear, assumptions));
|
|
|
|
z3::solver nonlinear(ctx, "QF_BV");
|
|
nonlinear.add(x * y == ctx.bv_val(4, 64));
|
|
CHECK(fold::symbolic::isNonlinearBitVectorQuery(nonlinear, assumptions));
|
|
|
|
const z3::expr memory = ctx.constant("memory", ctx.array_sort(ctx.bv_sort(64), ctx.bv_sort(8)));
|
|
z3::solver with_array(ctx);
|
|
with_array.add(x * y == ctx.bv_val(4, 64));
|
|
with_array.add(z3::select(memory, x) == ctx.bv_val(0, 8));
|
|
CHECK_FALSE(fold::symbolic::isNonlinearBitVectorQuery(with_array, assumptions));
|
|
}
|
|
|
|
TEST_CASE("portfolio translation accepts Fold theory and rejects unsupported expressions",
|
|
"[symbolic][model-validation][portfolio]") {
|
|
z3::context ctx;
|
|
const z3::expr bv = ctx.bv_const("bv", 64);
|
|
const z3::expr predicate = bv == ctx.bv_val(7, 64);
|
|
CHECK(fold::symbolic::isPortfolioExpressionSupported(predicate));
|
|
|
|
const z3::sort bytes = ctx.array_sort(ctx.bv_sort(64), ctx.bv_sort(8));
|
|
const z3::expr memory = ctx.constant("memory", bytes);
|
|
CHECK(
|
|
fold::symbolic::isPortfolioExpressionSupported(z3::select(memory, bv) == ctx.bv_val(0, 8)));
|
|
|
|
const z3::expr integer = ctx.int_const("integer");
|
|
CHECK_FALSE(fold::symbolic::isPortfolioExpressionSupported(integer > 0));
|
|
CHECK_FALSE(fold::symbolic::isPortfolioExpressionSupported(z3::forall(bv, predicate)));
|
|
|
|
z3::solver unsupported_solver(ctx);
|
|
unsupported_solver.add(integer > 0);
|
|
z3::expr_vector assumptions(ctx);
|
|
fold::SolverStats stats;
|
|
CHECK(fold::symbolic::checkWithPortfolio(unsupported_solver, assumptions, 100, stats) ==
|
|
z3::unknown);
|
|
}
|
|
|
|
TEST_CASE("portfolio verdicts stand only when the re-check agrees", "[portfolio][soundness]") {
|
|
// The re-check used to be consulted only for its model/core; a SAT
|
|
// portfolio verdict whose re-check timed out was still reported as SAT.
|
|
using fold::symbolic::reconcilePortfolioRecheck;
|
|
CHECK(reconcilePortfolioRecheck(z3::sat, z3::sat) == z3::sat);
|
|
CHECK(reconcilePortfolioRecheck(z3::unsat, z3::unsat) == z3::unsat);
|
|
CHECK(reconcilePortfolioRecheck(z3::sat, z3::unknown) == z3::unknown);
|
|
CHECK(reconcilePortfolioRecheck(z3::unsat, z3::unknown) == z3::unknown);
|
|
CHECK(reconcilePortfolioRecheck(z3::sat, z3::unsat) == z3::unknown);
|
|
CHECK(reconcilePortfolioRecheck(z3::unsat, z3::sat) == z3::unknown);
|
|
}
|
|
|
|
TEST_CASE("portfolio re-check on the reporting solver records disagreements",
|
|
"[portfolio][soundness]") {
|
|
// A disagreement used to be silent, and the detectors memoized the
|
|
// re-check's unsat core even when the verdict became unknown.
|
|
z3::context ctx;
|
|
z3::solver solver(ctx, "QF_BV");
|
|
const z3::expr value = ctx.bv_const("value", 64);
|
|
solver.add(value == ctx.bv_val(7, 64));
|
|
z3::expr_vector assumptions(ctx);
|
|
using fold::symbolic::recheckPortfolioVerdict;
|
|
|
|
fold::SolverStats stats;
|
|
CHECK(recheckPortfolioVerdict(solver, assumptions, z3::sat, stats) == z3::sat);
|
|
CHECK(stats.portfolio_disagreements == 0);
|
|
CHECK(recheckPortfolioVerdict(solver, assumptions, z3::unknown, stats) == z3::unknown);
|
|
CHECK(stats.portfolio_disagreements == 0);
|
|
CHECK(recheckPortfolioVerdict(solver, assumptions, z3::unsat, stats) == z3::unknown);
|
|
CHECK(stats.portfolio_disagreements == 1);
|
|
|
|
fold::SolverStats merged;
|
|
merged.merge(stats);
|
|
CHECK(merged.portfolio_disagreements == 1);
|
|
}
|