Skip to content

Commit ae570d8

Browse files
authored
Reland "[analyzer] Harden safeguards for Z3 query times" (#97298)
This is exactly as originally landed in #95129, but now the minimal Z3 version was increased to meet this change in #96682. https://discourse.llvm.org/t/bump-minimal-z3-requirements-from-4-7-1-to-4-8-9/79664/4 --- This patch is a functional change. https://discourse.llvm.org/t/analyzer-rfc-taming-z3-query-times/79520 As a result of this patch, individual Z3 queries in refutation will be bound by 300ms. Every report equivalence class will be processed in at most 1 second. The heuristic should have only really marginal observable impact - except for the cases when we had big report eqclasses with long-running (15s) Z3 queries, where previously CSA effectively halted. After this patch, CSA will tackle such extreme cases as well. (cherry picked from commit eacc3b3)
1 parent 65c807e commit ae570d8

File tree

6 files changed

+221
-38
lines changed

6 files changed

+221
-38
lines changed

clang/include/clang/StaticAnalyzer/Core/AnalyzerOptions.def

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,26 @@ ANALYZER_OPTION(bool, ShouldCrosscheckWithZ3, "crosscheck-with-z3",
184184
"constraint manager backend.",
185185
false)
186186

187+
ANALYZER_OPTION(
188+
unsigned, Z3CrosscheckEQClassTimeoutThreshold,
189+
"crosscheck-with-z3-eqclass-timeout-threshold",
190+
"Set a timeout for bug report equivalence classes in milliseconds. "
191+
"If we exhaust this threshold, we will drop the bug report eqclass "
192+
"instead of doing more Z3 queries. Set 0 for no timeout.", 700)
193+
194+
ANALYZER_OPTION(
195+
unsigned, Z3CrosscheckTimeoutThreshold,
196+
"crosscheck-with-z3-timeout-threshold",
197+
"Set a timeout for individual Z3 queries in milliseconds. "
198+
"Set 0 for no timeout.", 300)
199+
200+
ANALYZER_OPTION(
201+
unsigned, Z3CrosscheckRLimitThreshold,
202+
"crosscheck-with-z3-rlimit-threshold",
203+
"Set the Z3 resource limit threshold. This sets a deterministic cutoff "
204+
"point for Z3 queries, as longer queries usually consume more resources. "
205+
"Set 0 for unlimited.", 400'000)
206+
187207
ANALYZER_OPTION(bool, ShouldReportIssuesInMainSourceFile,
188208
"report-in-main-source-file",
189209
"Whether or not the diagnostic report should be always "

clang/include/clang/StaticAnalyzer/Core/BugReporter/Z3CrosscheckVisitor.h

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ class Z3CrosscheckVisitor final : public BugReporterVisitor {
2525
public:
2626
struct Z3Result {
2727
std::optional<bool> IsSAT = std::nullopt;
28+
unsigned Z3QueryTimeMilliseconds = 0;
29+
unsigned UsedRLimit = 0;
2830
};
29-
explicit Z3CrosscheckVisitor(Z3CrosscheckVisitor::Z3Result &Result);
31+
Z3CrosscheckVisitor(Z3CrosscheckVisitor::Z3Result &Result,
32+
const AnalyzerOptions &Opts);
3033

3134
void Profile(llvm::FoldingSetNodeID &ID) const override;
3235

@@ -44,21 +47,44 @@ class Z3CrosscheckVisitor final : public BugReporterVisitor {
4447
/// Holds the constraints in a given path.
4548
ConstraintMap Constraints;
4649
Z3Result &Result;
50+
const AnalyzerOptions &Opts;
4751
};
4852

4953
/// The oracle will decide if a report should be accepted or rejected based on
50-
/// the results of the Z3 solver.
54+
/// the results of the Z3 solver and the statistics of the queries of a report
55+
/// equivalenece class.
5156
class Z3CrosscheckOracle {
5257
public:
58+
explicit Z3CrosscheckOracle(const AnalyzerOptions &Opts) : Opts(Opts) {}
59+
5360
enum Z3Decision {
54-
AcceptReport, // The report was SAT.
55-
RejectReport, // The report was UNSAT or UNDEF.
61+
AcceptReport, // The report was SAT.
62+
RejectReport, // The report was UNSAT or UNDEF.
63+
RejectEQClass, // The heuristic suggests to skip the current eqclass.
5664
};
5765

58-
/// Makes a decision for accepting or rejecting the report based on the
59-
/// result of the corresponding Z3 query.
60-
static Z3Decision
61-
interpretQueryResult(const Z3CrosscheckVisitor::Z3Result &Query);
66+
/// Updates the internal state with the new Z3Result and makes a decision how
67+
/// to proceed:
68+
/// - Accept the report if the Z3Result was SAT.
69+
/// - Suggest dropping the report equvalence class based on the accumulated
70+
/// statistics.
71+
/// - Otherwise, reject the report if the Z3Result was UNSAT or UNDEF.
72+
///
73+
/// Conditions for dropping the equivalence class:
74+
/// - Accumulative time spent in Z3 checks is more than 700ms in the eqclass.
75+
/// - Hit the 300ms query timeout in the report eqclass.
76+
/// - Hit the 400'000 rlimit in the report eqclass.
77+
///
78+
/// All these thresholds are configurable via the analyzer options.
79+
///
80+
/// Refer to
81+
/// https://discourse.llvm.org/t/analyzer-rfc-taming-z3-query-times/79520 to
82+
/// see why this heuristic was chosen.
83+
Z3Decision interpretQueryResult(const Z3CrosscheckVisitor::Z3Result &Meta);
84+
85+
private:
86+
const AnalyzerOptions &Opts;
87+
unsigned AccumulatedZ3QueryTimeInEqClass = 0; // ms
6288
};
6389

6490
} // namespace clang::ento

clang/lib/StaticAnalyzer/Core/BugReporter.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ STATISTIC(MaxValidBugClassSize,
8989

9090
STATISTIC(NumTimesReportPassesZ3, "Number of reports passed Z3");
9191
STATISTIC(NumTimesReportRefuted, "Number of reports refuted by Z3");
92+
STATISTIC(NumTimesReportEQClassAborted,
93+
"Number of times a report equivalence class was aborted by the Z3 "
94+
"oracle heuristic");
9295
STATISTIC(NumTimesReportEQClassWasExhausted,
9396
"Number of times all reports of an equivalence class was refuted");
9497

@@ -2840,6 +2843,7 @@ generateVisitorsDiagnostics(PathSensitiveBugReport *R,
28402843
std::optional<PathDiagnosticBuilder> PathDiagnosticBuilder::findValidReport(
28412844
ArrayRef<PathSensitiveBugReport *> &bugReports,
28422845
PathSensitiveBugReporter &Reporter) {
2846+
Z3CrosscheckOracle Z3Oracle(Reporter.getAnalyzerOptions());
28432847

28442848
BugPathGetter BugGraph(&Reporter.getGraph(), bugReports);
28452849

@@ -2871,16 +2875,20 @@ std::optional<PathDiagnosticBuilder> PathDiagnosticBuilder::findValidReport(
28712875
// visitor and check again
28722876
R->clearVisitors();
28732877
Z3CrosscheckVisitor::Z3Result CrosscheckResult;
2874-
R->addVisitor<Z3CrosscheckVisitor>(CrosscheckResult);
2878+
R->addVisitor<Z3CrosscheckVisitor>(CrosscheckResult,
2879+
Reporter.getAnalyzerOptions());
28752880

28762881
// We don't overwrite the notes inserted by other visitors because the
28772882
// refutation manager does not add any new note to the path
28782883
generateVisitorsDiagnostics(R, BugPath->ErrorNode, BRC);
2879-
switch (Z3CrosscheckOracle::interpretQueryResult(CrosscheckResult)) {
2884+
switch (Z3Oracle.interpretQueryResult(CrosscheckResult)) {
28802885
case Z3CrosscheckOracle::RejectReport:
28812886
++NumTimesReportRefuted;
28822887
R->markInvalid("Infeasible constraints", /*Data=*/nullptr);
28832888
continue;
2889+
case Z3CrosscheckOracle::RejectEQClass:
2890+
++NumTimesReportEQClassAborted;
2891+
return {};
28842892
case Z3CrosscheckOracle::AcceptReport:
28852893
++NumTimesReportPassesZ3;
28862894
break;

clang/lib/StaticAnalyzer/Core/Z3CrosscheckVisitor.cpp

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,37 @@
1212
//===----------------------------------------------------------------------===//
1313

1414
#include "clang/StaticAnalyzer/Core/BugReporter/Z3CrosscheckVisitor.h"
15+
#include "clang/StaticAnalyzer/Core/AnalyzerOptions.h"
1516
#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
1617
#include "clang/StaticAnalyzer/Core/PathSensitive/SMTConv.h"
1718
#include "llvm/ADT/Statistic.h"
1819
#include "llvm/Support/SMTAPI.h"
20+
#include "llvm/Support/Timer.h"
1921

2022
#define DEBUG_TYPE "Z3CrosscheckOracle"
2123

2224
STATISTIC(NumZ3QueriesDone, "Number of Z3 queries done");
2325
STATISTIC(NumTimesZ3TimedOut, "Number of times Z3 query timed out");
26+
STATISTIC(NumTimesZ3ExhaustedRLimit,
27+
"Number of times Z3 query exhausted the rlimit");
28+
STATISTIC(NumTimesZ3SpendsTooMuchTimeOnASingleEQClass,
29+
"Number of times report equivalenece class was cut because it spent "
30+
"too much time in Z3");
2431

2532
STATISTIC(NumTimesZ3QueryAcceptsReport,
2633
"Number of Z3 queries accepting a report");
2734
STATISTIC(NumTimesZ3QueryRejectReport,
2835
"Number of Z3 queries rejecting a report");
36+
STATISTIC(NumTimesZ3QueryRejectEQClass,
37+
"Number of times rejecting an report equivalenece class");
2938

3039
using namespace clang;
3140
using namespace ento;
3241

33-
Z3CrosscheckVisitor::Z3CrosscheckVisitor(Z3CrosscheckVisitor::Z3Result &Result)
34-
: Constraints(ConstraintMap::Factory().getEmptyMap()), Result(Result) {}
42+
Z3CrosscheckVisitor::Z3CrosscheckVisitor(Z3CrosscheckVisitor::Z3Result &Result,
43+
const AnalyzerOptions &Opts)
44+
: Constraints(ConstraintMap::Factory().getEmptyMap()), Result(Result),
45+
Opts(Opts) {}
3546

3647
void Z3CrosscheckVisitor::finalizeVisitor(BugReporterContext &BRC,
3748
const ExplodedNode *EndPathNode,
@@ -41,8 +52,12 @@ void Z3CrosscheckVisitor::finalizeVisitor(BugReporterContext &BRC,
4152

4253
// Create a refutation manager
4354
llvm::SMTSolverRef RefutationSolver = llvm::CreateZ3Solver();
44-
RefutationSolver->setBoolParam("model", true); // Enable model finding
45-
RefutationSolver->setUnsignedParam("timeout", 15000); // ms
55+
if (Opts.Z3CrosscheckRLimitThreshold)
56+
RefutationSolver->setUnsignedParam("rlimit",
57+
Opts.Z3CrosscheckRLimitThreshold);
58+
if (Opts.Z3CrosscheckTimeoutThreshold)
59+
RefutationSolver->setUnsignedParam("timeout",
60+
Opts.Z3CrosscheckTimeoutThreshold); // ms
4661

4762
ASTContext &Ctx = BRC.getASTContext();
4863

@@ -63,8 +78,15 @@ void Z3CrosscheckVisitor::finalizeVisitor(BugReporterContext &BRC,
6378
}
6479

6580
// And check for satisfiability
81+
llvm::TimeRecord Start = llvm::TimeRecord::getCurrentTime(/*Start=*/true);
6682
std::optional<bool> IsSAT = RefutationSolver->check();
67-
Result = Z3Result{IsSAT};
83+
llvm::TimeRecord Diff = llvm::TimeRecord::getCurrentTime(/*Start=*/false);
84+
Diff -= Start;
85+
Result = Z3Result{
86+
IsSAT,
87+
static_cast<unsigned>(Diff.getWallTime() * 1000),
88+
RefutationSolver->getStatistics()->getUnsigned("rlimit count"),
89+
};
6890
}
6991

7092
void Z3CrosscheckVisitor::addConstraints(
@@ -101,18 +123,38 @@ void Z3CrosscheckVisitor::Profile(llvm::FoldingSetNodeID &ID) const {
101123
Z3CrosscheckOracle::Z3Decision Z3CrosscheckOracle::interpretQueryResult(
102124
const Z3CrosscheckVisitor::Z3Result &Query) {
103125
++NumZ3QueriesDone;
126+
AccumulatedZ3QueryTimeInEqClass += Query.Z3QueryTimeMilliseconds;
104127

105-
if (!Query.IsSAT.has_value()) {
106-
// For backward compatibility, let's accept the first timeout.
107-
++NumTimesZ3TimedOut;
128+
if (Query.IsSAT && Query.IsSAT.value()) {
129+
++NumTimesZ3QueryAcceptsReport;
108130
return AcceptReport;
109131
}
110132

111-
if (Query.IsSAT.value()) {
112-
++NumTimesZ3QueryAcceptsReport;
113-
return AcceptReport; // sat
133+
// Suggest cutting the EQClass if certain heuristics trigger.
134+
if (Opts.Z3CrosscheckTimeoutThreshold &&
135+
Query.Z3QueryTimeMilliseconds >= Opts.Z3CrosscheckTimeoutThreshold) {
136+
++NumTimesZ3TimedOut;
137+
++NumTimesZ3QueryRejectEQClass;
138+
return RejectEQClass;
139+
}
140+
141+
if (Opts.Z3CrosscheckRLimitThreshold &&
142+
Query.UsedRLimit >= Opts.Z3CrosscheckRLimitThreshold) {
143+
++NumTimesZ3ExhaustedRLimit;
144+
++NumTimesZ3QueryRejectEQClass;
145+
return RejectEQClass;
146+
}
147+
148+
if (Opts.Z3CrosscheckEQClassTimeoutThreshold &&
149+
AccumulatedZ3QueryTimeInEqClass >
150+
Opts.Z3CrosscheckEQClassTimeoutThreshold) {
151+
++NumTimesZ3SpendsTooMuchTimeOnASingleEQClass;
152+
++NumTimesZ3QueryRejectEQClass;
153+
return RejectEQClass;
114154
}
115155

156+
// If no cutoff heuristics trigger, and the report is "unsat" or "undef",
157+
// then reject the report.
116158
++NumTimesZ3QueryRejectReport;
117-
return RejectReport; // unsat
159+
return RejectReport;
118160
}

clang/test/Analysis/analyzer-config.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
// CHECK-NEXT: cplusplus.Move:WarnOn = KnownsAndLocals
4444
// CHECK-NEXT: cplusplus.SmartPtrModeling:ModelSmartPtrDereference = false
4545
// CHECK-NEXT: crosscheck-with-z3 = false
46+
// CHECK-NEXT: crosscheck-with-z3-eqclass-timeout-threshold = 700
47+
// CHECK-NEXT: crosscheck-with-z3-rlimit-threshold = 400000
48+
// CHECK-NEXT: crosscheck-with-z3-timeout-threshold = 300
4649
// CHECK-NEXT: ctu-dir = ""
4750
// CHECK-NEXT: ctu-import-cpp-threshold = 8
4851
// CHECK-NEXT: ctu-import-threshold = 24

0 commit comments

Comments
 (0)