blob: 0bb06b3e015aabad03f7fbe4b90072908fb52127 [file] [edit]
//===- BoundsChecking.cpp - Bounds checking related APIs --------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file implements 'checkBounds', a function that compares memory offsets
// (that may be symbolic) and uses heuristical workarounds to provide more
// accurate results than the 'naive' evalBinOp calls.
//
//===----------------------------------------------------------------------===//
#include "clang/StaticAnalyzer/Checkers/BoundsChecking.h"
using namespace clang;
using namespace ento;
// NOTE: This function is the "heart" of this algorithm. It simplifies
// inequalities with transformations that are valid (and very elementary) in
// pure mathematics, but become invalid if we use them in C++ number model
// where the calculations may overflow.
// Due to the overflow issues I think it's impossible (or at least not
// practical) to integrate this kind of simplification into the resolution of
// arbitrary inequalities (i.e. the code of `evalBinOp`); but this function
// produces valid results when the calculations are handling memory offsets
// and every value is well below SIZE_MAX.
// NOTE: the simplification preserves the order of the two operands in a
// mathematical sense, but it may change the result produced by a C++
// comparison operator (and the automatic type conversions).
// For example, consider a comparison "X+1 < 0", where the LHS is stored as a
// size_t and the RHS is stored in an int. (As size_t is unsigned, this
// comparison is false for all values of "X".) However, the simplification may
// turn it into "X < -1", which is still always false in a mathematical sense,
// but can produce a true result when evaluated by `evalBinOp` (which follows
// the rules of C++ and casts -1 to SIZE_MAX).
static std::pair<NonLoc, nonloc::ConcreteInt>
getSimplifiedOffsets(NonLoc Offset, nonloc::ConcreteInt Extent,
SValBuilder &SVB) {
const llvm::APSInt &ExtentVal = Extent.getValue();
std::optional<nonloc::SymbolVal> SymVal = Offset.getAs<nonloc::SymbolVal>();
if (SymVal && SymVal->isExpression()) {
if (const SymIntExpr *SIE = dyn_cast<SymIntExpr>(SymVal->getSymbol())) {
llvm::APSInt Num = APSIntType(ExtentVal).convert(SIE->getRHS());
switch (SIE->getOpcode()) {
case BO_Mul:
// The Num should never be 0 here, because multiplication by zero
// is simplified by the engine.
if ((ExtentVal % Num) != 0)
return std::pair<NonLoc, nonloc::ConcreteInt>(Offset, Extent);
else
return getSimplifiedOffsets(nonloc::SymbolVal(SIE->getLHS()),
SVB.makeIntVal(ExtentVal / Num), SVB);
case BO_Add:
return getSimplifiedOffsets(nonloc::SymbolVal(SIE->getLHS()),
SVB.makeIntVal(ExtentVal - Num), SVB);
default:
break;
}
}
}
return std::pair<NonLoc, nonloc::ConcreteInt>(Offset, Extent);
}
static bool isNegative(SValBuilder &SVB, ProgramStateRef State, NonLoc Value) {
const llvm::APSInt *MaxV = SVB.getMaxValue(State, Value);
return MaxV && MaxV->isNegative();
}
static bool isUnsigned(SValBuilder &SVB, NonLoc Value) {
QualType T = Value.getType(SVB.getContext());
return T->isUnsignedIntegerType();
}
std::pair<ProgramStateRef, ProgramStateRef>
bounds::compareValueToThreshold(ProgramStateRef State, SValBuilder &SVB,
NonLoc Value, NonLoc Threshold,
Comparison CmpKind) {
if (auto ConcreteThreshold = Threshold.getAs<nonloc::ConcreteInt>()) {
std::tie(Value, Threshold) =
getSimplifiedOffsets(Value, *ConcreteThreshold, SVB);
}
// We want to perform a _mathematical_ comparison between the numbers `Value`
// and `Threshold`; but `evalBinOpNN` evaluates a C/C++ operator that may
// perform automatic conversions. For example the number -1 is less than the
// number 1000, but -1 < `1000ull` will evaluate to `false` because the `int`
// -1 is converted to ULONGLONG_MAX.
// To avoid automatic conversions, we evaluate the "obvious" cases without
// calling `evalBinOpNN`:
if (isNegative(SVB, State, Value) && isUnsigned(SVB, Threshold)) {
if (CmpKind == Comparison::EQ) {
// negative == unsigned is always false
return {nullptr, State};
}
// negative < unsigned and negative <= unsigned are always true
return {State, nullptr};
}
if (isUnsigned(SVB, Value) && isNegative(SVB, State, Threshold)) {
// unsigned == negative, unsigned < negative and unsigned <= negative are
// all always false
return {nullptr, State};
}
// FIXME: These special cases are sufficient for handling real-world
// comparisons, but in theory there could be contrived situations where
// automatic conversion of a symbolic value (which can be negative and can be
// positive) leads to incorrect results.
// NOTE: We NEED to use the `evalBinOpNN` call in the "common" case, because
// we want to ensure that assumptions coming from this precondition and
// assumptions coming from regular C/C++ operator calls are represented by
// constraints on the same symbolic expression. A solution that would
// evaluate these "mathematical" comparisons through a separate pathway would
// be a step backwards in this sense.
const BinaryOperatorKind OpKind = asOpcode(CmpKind);
auto BelowThreshold =
SVB.evalBinOpNN(State, OpKind, Value, Threshold, SVB.getConditionType())
.getAs<NonLoc>();
if (BelowThreshold)
return State->assume(*BelowThreshold);
return {nullptr, nullptr};
}
bounds::CheckResult bounds::checkBounds(ProgramStateRef State, SValBuilder &SVB,
NonLoc Offset,
std::optional<NonLoc> Extent,
bounds::CheckFlags Flags) {
bounds::CheckResult Res(Offset);
// CHECK LOWER BOUND
if (Flags.CheckUnderflow) {
auto [PrecedesLowerBound, WithinLowerBound] = compareValueToThreshold(
State, SVB, Offset, SVB.makeZeroArrayIndex(), Comparison::LT);
if (PrecedesLowerBound) {
// The analyzer thinks that the offset may be invalid (negative)...
if (Flags.OffsetObviouslyNonnegative) {
// ...but the offset is obviously non-negative (clear array subscript
// with an unsigned index), so we're in a buggy situation.
// TODO: Currently the analyzer ignores many casts (e.g. signed ->
// unsigned casts), so it can easily reach states where it will load a
// signed (and negative) value from an unsigned variable. This sanity
// check is a duct tape "solution" that silences most of the ugly false
// positives that are caused by this buggy behavior. Note that this is
// not a complete solution: this cannot silence reports where pointer
// arithmetic complicates the picture and cannot ensure modeling of the
// "unsigned index is positive with highest bit set" cases which are
// "usurped" by the nonsense "unsigned index is negative" case.
// For more information about this topic, see the umbrella ticket
// https://github.com/llvm/llvm-project/issues/39492
// TODO: Remove this hack once 'SymbolCast's are modeled properly.
if (!WithinLowerBound) {
// The state is completely nonsense -- let's just sink it!
Res.IsCorruptedState = true;
return Res;
}
// Otherwise continue on the 'WithinLowerBound' branch where the
// unsigned index _is_ non-negative. Don't mention this assumption as a
// note tag, because it would just confuse the users!
} else {
Res.MayUnderflow = true;
if (!WithinLowerBound) {
// ...and it cannot be valid (>= 0), so report an error.
return Res;
}
}
}
// Actually update the state. The "if" only fails in the extremely unlikely
// case when compareValueToThreshold returns {nullptr, nullptr} because
// evalBinOpNN fails to evaluate the less-than operator.
if (WithinLowerBound)
State = WithinLowerBound;
}
// CHECK UPPER BOUND
if (Extent) {
Comparison CK = Flags.AlsoAcceptEquality ? Comparison::LE : Comparison::LT;
auto [WithinUpperBound, ExceedsUpperBound] =
compareValueToThreshold(State, SVB, Offset, *Extent, /*CmpKind=*/CK);
if (ExceedsUpperBound) {
// The offset may be invalid (>= Size)...
Res.ExtentIfMayOverflow = Extent;
if (!WithinUpperBound) {
// ...and it cannot be within bounds.
return Res;
}
}
if (WithinUpperBound)
State = WithinUpperBound;
}
Res.InBoundsState = State;
return Res;
}