Skip to content

[analyzer] Retry UNDEF Z3 queries 2 times by default #120239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
//
//===----------------------------------------------------------------------===//

#ifndef LLVM_ADT_STRINGREF_H
#ifndef LLVM_CLANG_STATICANALYZER_CORE_ANALYZEROPTIONS_H
#error This .def file is expected to be included in translation units where \
"llvm/ADT/StringRef.h" is already included!
"clang/StaticAnalyzer/Core/AnalyzerOptions.h" is already included!
#endif

#ifdef ANALYZER_OPTION
Expand Down Expand Up @@ -193,6 +193,8 @@ ANALYZER_OPTION(
"with \"crosscheck-with-z3-timeout-threshold\" of 300 ms, would nicely "
"guarantee that no bug report equivalence class can take longer than "
"1 second, effectively mitigating Z3 hangs during refutation. "
"If there were Z3 retries, only the minimum query time is considered "
"when accumulating query times within a report equivalence class. "
"Set 0 for no timeout.", 0)

ANALYZER_OPTION(
Expand All @@ -213,6 +215,15 @@ ANALYZER_OPTION(
"400'000 should on average make Z3 queries run for up to 100ms on modern "
"hardware. Set 0 for unlimited.", 0)

ANALYZER_OPTION(
PositiveAnalyzerOption, Z3CrosscheckMaxAttemptsPerQuery,
"crosscheck-with-z3-max-attempts-per-query",
"Set how many times the oracle is allowed to run a Z3 query. "
"This must be a positive value. Set 1 to not allow any retry attempts. "
"Increasing the number of attempts is often more effective at reducing "
"the number of nondeterministic diagnostics than "
"\"crosscheck-with-z3-timeout-threshold\" in practice.", 3)

ANALYZER_OPTION(bool, ShouldReportIssuesInMainSourceFile,
"report-in-main-source-file",
"Whether or not the diagnostic report should be always "
Expand Down
25 changes: 25 additions & 0 deletions clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ enum UserModeKind {

enum class CTUPhase1InliningKind { None, Small, All };

class PositiveAnalyzerOption {
public:
PositiveAnalyzerOption() = default;
PositiveAnalyzerOption(const PositiveAnalyzerOption &) = default;
PositiveAnalyzerOption &operator=(const PositiveAnalyzerOption &) = default;

static std::optional<PositiveAnalyzerOption> create(unsigned Val) {
if (Val == 0)
return std::nullopt;
return PositiveAnalyzerOption{Val};
}
static std::optional<PositiveAnalyzerOption> create(StringRef Str) {
unsigned Parsed = 0;
if (Str.getAsInteger(0, Parsed))
return std::nullopt;
return PositiveAnalyzerOption::create(Parsed);
}
operator unsigned() const { return Value; }

private:
explicit constexpr PositiveAnalyzerOption(unsigned Value) : Value(Value) {}

unsigned Value = 1;
};

/// Stores options for the analyzer from the command line.
///
/// Some options are frontend flags (e.g.: -analyzer-output), but some are
Expand Down
19 changes: 19 additions & 0 deletions clang/lib/Frontend/CompilerInvocation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,25 @@ static void initOption(AnalyzerOptions::ConfigTable &Config,
<< Name << "an unsigned";
}

static void initOption(AnalyzerOptions::ConfigTable &Config,
DiagnosticsEngine *Diags,
PositiveAnalyzerOption &OptionField, StringRef Name,
unsigned DefaultVal) {
auto Parsed = PositiveAnalyzerOption::create(
getStringOption(Config, Name, std::to_string(DefaultVal)));
if (Parsed.has_value()) {
OptionField = Parsed.value();
return;
}
if (Diags && !Parsed.has_value())
Diags->Report(diag::err_analyzer_config_invalid_input)
<< Name << "a positive";

auto Default = PositiveAnalyzerOption::create(DefaultVal);
assert(Default.has_value());
OptionField = Default.value();
}

static void parseAnalyzerConfigs(AnalyzerOptions &AnOpts,
DiagnosticsEngine *Diags) {
// TODO: There's no need to store the entire configtable, it'd be plenty
Expand Down
38 changes: 29 additions & 9 deletions clang/lib/StaticAnalyzer/Core/Z3CrosscheckVisitor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@

#define DEBUG_TYPE "Z3CrosscheckOracle"

// Queries attempted at most `Z3CrosscheckMaxAttemptsPerQuery` number of times.
// Multiple `check()` calls might be called on the same query if previous
// attempts of the same query resulted in UNSAT for any reason. Each query is
// only counted once for these statistics, the retries are not accounted for.
STATISTIC(NumZ3QueriesDone, "Number of Z3 queries done");
STATISTIC(NumTimesZ3TimedOut, "Number of times Z3 query timed out");
STATISTIC(NumTimesZ3ExhaustedRLimit,
Expand Down Expand Up @@ -77,16 +81,32 @@ void Z3CrosscheckVisitor::finalizeVisitor(BugReporterContext &BRC,
RefutationSolver->addConstraint(SMTConstraints);
}

// And check for satisfiability
llvm::TimeRecord Start = llvm::TimeRecord::getCurrentTime(/*Start=*/true);
std::optional<bool> IsSAT = RefutationSolver->check();
llvm::TimeRecord Diff = llvm::TimeRecord::getCurrentTime(/*Start=*/false);
Diff -= Start;
Result = Z3Result{
IsSAT,
static_cast<unsigned>(Diff.getWallTime() * 1000),
RefutationSolver->getStatistics()->getUnsigned("rlimit count"),
auto GetUsedRLimit = [](const llvm::SMTSolverRef &Solver) {
return Solver->getStatistics()->getUnsigned("rlimit count");
};

auto AttemptOnce = [&](const llvm::SMTSolverRef &Solver) -> Z3Result {
constexpr auto getCurrentTime = llvm::TimeRecord::getCurrentTime;
unsigned InitialRLimit = GetUsedRLimit(Solver);
double Start = getCurrentTime(/*Start=*/true).getWallTime();
std::optional<bool> IsSAT = Solver->check();
double End = getCurrentTime(/*Start=*/false).getWallTime();
return {
IsSAT,
static_cast<unsigned>((End - Start) * 1000),
GetUsedRLimit(Solver) - InitialRLimit,
};
};

// And check for satisfiability
unsigned MinQueryTimeAcrossAttempts = std::numeric_limits<unsigned>::max();
for (unsigned I = 0; I < Opts.Z3CrosscheckMaxAttemptsPerQuery; ++I) {
Result = AttemptOnce(RefutationSolver);
Result.Z3QueryTimeMilliseconds =
std::min(MinQueryTimeAcrossAttempts, Result.Z3QueryTimeMilliseconds);
if (Result.IsSAT.has_value())
return;
}
}

void Z3CrosscheckVisitor::addConstraints(
Expand Down
1 change: 1 addition & 0 deletions clang/test/Analysis/analyzer-config.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
// CHECK-NEXT: cplusplus.SmartPtrModeling:ModelSmartPtrDereference = false
// CHECK-NEXT: crosscheck-with-z3 = false
// CHECK-NEXT: crosscheck-with-z3-eqclass-timeout-threshold = 0
// CHECK-NEXT: crosscheck-with-z3-max-attempts-per-query = 3
// CHECK-NEXT: crosscheck-with-z3-rlimit-threshold = 0
// CHECK-NEXT: crosscheck-with-z3-timeout-threshold = 15000
// CHECK-NEXT: ctu-dir = ""
Expand Down
42 changes: 42 additions & 0 deletions clang/test/Analysis/z3-crosscheck-max-attempts.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Check the default config.
// RUN: %clang_analyze_cc1 -analyzer-checker=debug.ConfigDumper 2>&1 \
// RUN: | FileCheck %s --match-full-lines
// CHECK: crosscheck-with-z3-max-attempts-per-query = 3

// RUN: rm -rf %t && mkdir %t
// RUN: %host_cxx -shared -fPIC \
// RUN: %S/z3/Inputs/MockZ3_solver_check.cpp \
// RUN: -o %t/MockZ3_solver_check.so

// DEFINE: %{mocked_clang} = \
// DEFINE: LD_PRELOAD="%t/MockZ3_solver_check.so" \
// DEFINE: %clang_cc1 %s -analyze -setup-static-analyzer \
// DEFINE: -analyzer-config crosscheck-with-z3=true \
// DEFINE: -analyzer-checker=core

// DEFINE: %{attempts} = -analyzer-config crosscheck-with-z3-max-attempts-per-query

// RUN: not %clang_analyze_cc1 %{attempts}=0 2>&1 | FileCheck %s --check-prefix=VERIFY-INVALID
// VERIFY-INVALID: invalid input for analyzer-config option 'crosscheck-with-z3-max-attempts-per-query', that expects a positive value

// RUN: Z3_SOLVER_RESULTS="UNDEF" %{mocked_clang} %{attempts}=1 -verify=refuted
// RUN: Z3_SOLVER_RESULTS="UNSAT" %{mocked_clang} %{attempts}=1 -verify=refuted
// RUN: Z3_SOLVER_RESULTS="SAT" %{mocked_clang} %{attempts}=1 -verify=accepted

// RUN: Z3_SOLVER_RESULTS="UNDEF,UNDEF" %{mocked_clang} %{attempts}=2 -verify=refuted
// RUN: Z3_SOLVER_RESULTS="UNDEF,UNSAT" %{mocked_clang} %{attempts}=2 -verify=refuted
// RUN: Z3_SOLVER_RESULTS="UNDEF,SAT" %{mocked_clang} %{attempts}=2 -verify=accepted

// RUN: Z3_SOLVER_RESULTS="UNDEF,UNDEF,UNDEF" %{mocked_clang} %{attempts}=3 -verify=refuted
// RUN: Z3_SOLVER_RESULTS="UNDEF,UNDEF,UNSAT" %{mocked_clang} %{attempts}=3 -verify=refuted
// RUN: Z3_SOLVER_RESULTS="UNDEF,UNDEF,SAT" %{mocked_clang} %{attempts}=3 -verify=accepted


// REQUIRES: z3, asserts, shell, system-linux

// refuted-no-diagnostics

int div_by_zero_test(int b) {
if (b) {}
return 100 / b; // accepted-warning {{Division by zero}}
}
17 changes: 7 additions & 10 deletions clang/test/Analysis/z3/D83660.c
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
// RUN: rm -rf %t && mkdir %t
// RUN: %host_cxx -shared -fPIC %S/Inputs/MockZ3_solver_check.c -o %t/MockZ3_solver_check.so
// RUN: %host_cxx -shared -fPIC \
// RUN: %S/Inputs/MockZ3_solver_check.cpp \
// RUN: -o %t/MockZ3_solver_check.so
//
// RUN: LD_PRELOAD="%t/MockZ3_solver_check.so" \
// RUN: %clang_cc1 -analyze -analyzer-constraints=z3 -setup-static-analyzer \
// RUN: -analyzer-checker=core,debug.ExprInspection %s -verify 2>&1 | FileCheck %s
// RUN: Z3_SOLVER_RESULTS="SAT,SAT,SAT,SAT,UNDEF" \
// RUN: LD_PRELOAD="%t/MockZ3_solver_check.so" \
// RUN: %clang_cc1 -analyze -analyzer-constraints=z3 -setup-static-analyzer \
// RUN: -analyzer-checker=core %s -verify
//
// REQUIRES: z3, asserts, shell, system-linux
//
// Works only with the z3 constraint manager.
// expected-no-diagnostics

// CHECK: Z3_solver_check returns the real value: TRUE
// CHECK-NEXT: Z3_solver_check returns the real value: TRUE
// CHECK-NEXT: Z3_solver_check returns the real value: TRUE
// CHECK-NEXT: Z3_solver_check returns the real value: TRUE
// CHECK-NEXT: Z3_solver_check returns a mocked value: UNDEF

void D83660(int b) {
if (b) {
}
Expand Down
28 changes: 0 additions & 28 deletions clang/test/Analysis/z3/Inputs/MockZ3_solver_check.c

This file was deleted.

62 changes: 62 additions & 0 deletions clang/test/Analysis/z3/Inputs/MockZ3_solver_check.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#include <cassert>
#include <dlfcn.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>

#include <z3.h>

static char *Z3ResultsBegin;
static char *Z3ResultsCursor;

static __attribute__((constructor)) void init() {
const char *Env = getenv("Z3_SOLVER_RESULTS");
if (!Env) {
fprintf(stderr, "Z3_SOLVER_RESULTS envvar must be defined; abort\n");
abort();
}
Z3ResultsBegin = strdup(Env);
Z3ResultsCursor = Z3ResultsBegin;
if (!Z3ResultsBegin) {
fprintf(stderr, "strdup failed; abort\n");
abort();
}
}

static __attribute__((destructor)) void finit() {
if (strlen(Z3ResultsCursor) > 0) {
fprintf(stderr, "Z3_SOLVER_RESULTS should have been completely consumed "
"by the end of the test; abort\n");
abort();
}
free(Z3ResultsBegin);
}

static bool consume_token(char **pointer_to_cursor, const char *token) {
assert(pointer_to_cursor);
int len = strlen(token);
if (*pointer_to_cursor && strncmp(*pointer_to_cursor, token, len) == 0) {
*pointer_to_cursor += len;
return true;
}
return false;
}

Z3_lbool Z3_API Z3_solver_check(Z3_context c, Z3_solver s) {
consume_token(&Z3ResultsCursor, ",");

if (consume_token(&Z3ResultsCursor, "UNDEF")) {
printf("Z3_solver_check returns UNDEF\n");
return Z3_L_UNDEF;
}
if (consume_token(&Z3ResultsCursor, "SAT")) {
printf("Z3_solver_check returns SAT\n");
return Z3_L_TRUE;
}
if (consume_token(&Z3ResultsCursor, "UNSAT")) {
printf("Z3_solver_check returns UNSAT\n");
return Z3_L_FALSE;
}
fprintf(stderr, "Z3_SOLVER_RESULTS was exhausted; abort\n");
abort();
}
11 changes: 10 additions & 1 deletion clang/unittests/StaticAnalyzer/Z3CrosscheckOracleTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,22 @@ static constexpr std::optional<bool> UNDEF = std::nullopt;
static unsigned operator""_ms(unsigned long long ms) { return ms; }
static unsigned operator""_step(unsigned long long rlimit) { return rlimit; }

template <class Ret, class Arg> static Ret makeDefaultOption(Arg Value) {
return Value;
}
template <> PositiveAnalyzerOption makeDefaultOption(int Value) {
auto DefaultVal = PositiveAnalyzerOption::create(Value);
assert(DefaultVal.has_value());
return DefaultVal.value();
}

static const AnalyzerOptions DefaultOpts = [] {
AnalyzerOptions Config;
#define ANALYZER_OPTION_DEPENDS_ON_USER_MODE(TYPE, NAME, CMDFLAG, DESC, \
SHALLOW_VAL, DEEP_VAL) \
ANALYZER_OPTION(TYPE, NAME, CMDFLAG, DESC, DEEP_VAL)
#define ANALYZER_OPTION(TYPE, NAME, CMDFLAG, DESC, DEFAULT_VAL) \
Config.NAME = DEFAULT_VAL;
Config.NAME = makeDefaultOption<TYPE>(DEFAULT_VAL);
#include "clang/StaticAnalyzer/Core/AnalyzerOptions.def"

// Remember to update the tests in this file when these values change.
Expand Down
Loading