Author: DonĂ¡t Nagy Date: 2026-08-04T12:48:23+02:00 New Revision: 25d51a8156da0845928e2fc09bb2736507fc5adf
URL: https://github.com/llvm/llvm-project/commit/25d51a8156da0845928e2fc09bb2736507fc5adf DIFF: https://github.com/llvm/llvm-project/commit/25d51a8156da0845928e2fc09bb2736507fc5adf.diff LOG: [NFC][analyzer] Extract general logic in security.ArrayBound (#210774) The checker `security.ArrayBound` contains general-purpose logic that will be useful to bring other bounds checking checkers out of `alpha` stage. This change refactors the implementation of `security.ArrayBound` to separate the general-purpose logic and the concrete details that are only relevant in that particular checkers. Shortly after merging this, a follow-up commit will move the general-purpose code to separate files. (This is left out of this change to ensure continuity in the git history: this commit renames and reorganizes functions, the next one will move them with minimal changes.) Note that after this commit the `ProgramState` associated with the error nodes created by `security.ArrayBound` will be slightly different in some cases (they may have different constraints) but the state of a sink node is practically unused, so this does not cause any functional changes. Added: Modified: clang/lib/StaticAnalyzer/Checkers/ArrayBoundChecker.cpp clang/test/Analysis/ArrayBound/assumption-reporting.c clang/test/Analysis/ArrayBound/verbose-tests.c Removed: ################################################################################ diff --git a/clang/lib/StaticAnalyzer/Checkers/ArrayBoundChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/ArrayBoundChecker.cpp index 67110f021bc56..460b1020b0e1b 100644 --- a/clang/lib/StaticAnalyzer/Checkers/ArrayBoundChecker.cpp +++ b/clang/lib/StaticAnalyzer/Checkers/ArrayBoundChecker.cpp @@ -57,92 +57,151 @@ getAsCleanArraySubscriptExpr(const Expr *E, const CheckerContext &C) { return ASE; } -/// If `E` is a "clean" array subscript expression, return the type of the -/// accessed element; otherwise return std::nullopt because that's the best (or -/// least bad) option for the diagnostic generation that relies on this. -static std::optional<QualType> determineElementType(const Expr *E, - const CheckerContext &C) { - const auto *ASE = getAsCleanArraySubscriptExpr(E, C); - if (!ASE) - return std::nullopt; +class SizeUnit { + QualType AsType; + int64_t AsCharUnits; - return ASE->getType(); -} + SizeUnit() : AsType(), AsCharUnits(1) {} -static std::optional<int64_t> -determineElementSize(const std::optional<QualType> T, const CheckerContext &C) { - if (!T) - return std::nullopt; - return C.getASTContext().getTypeSizeInChars(*T).getQuantity(); -} +public: + SizeUnit(QualType T, const ASTContext &ACtx) + : AsType(T), AsCharUnits(ACtx.getTypeSizeInChars(T).getQuantity()) { + assert(!T.isNull()); + } -class StateUpdateReporter { - const MemSpaceRegion *Space; - const SubRegion *Reg; - const NonLoc ByteOffsetVal; - const std::optional<QualType> ElementType; - const std::optional<int64_t> ElementSize; - bool AssumedNonNegative = false; - std::optional<NonLoc> AssumedUpperBound = std::nullopt; + static SizeUnit bytes() { return SizeUnit(); } -public: - StateUpdateReporter(const SubRegion *R, NonLoc ByteOffsVal, const Expr *E, - CheckerContext &C) - : Space(R->getMemorySpace(C.getState())), Reg(R), - ByteOffsetVal(ByteOffsVal), ElementType(determineElementType(E, C)), - ElementSize(determineElementSize(ElementType, C)) {} + bool isBytes() const { return AsType.isNull(); } + + /// Return the element type that is "natural" for reporting out-of-bounds + /// memory access to 'Location'. + static SizeUnit forSVal(SVal Location, const ASTContext &ACtx) { + if (const auto *R = Location.getAsRegion()->getAs<TypedValueRegion>()) + return SizeUnit(R->getValueType(), ACtx); + return bytes(); + } - void recordNonNegativeAssumption() { AssumedNonNegative = true; } - void recordUpperBoundAssumption(NonLoc UpperBoundVal) { - AssumedUpperBound = UpperBoundVal; + /// If `E` is a "clean" array subscript expression, return the type of the + /// accessed element; otherwise return 'Bytes' because that's the best (or + /// least bad) option for the assumption messages that use this. + /// FIXME: It is unfortunate that this heuristic diff ers from the heuristic + /// used for reporting assumption; but this diff erence is currently needed + /// due to the unfortunate phrasing of the assumption messages. + /// Get rid of this when the assumption note is rephrased and improved. + static SizeUnit forExpr(const Expr *E, const CheckerContext &C) { + const auto *ASE = getAsCleanArraySubscriptExpr(E, C); + if (!ASE) + return bytes(); + + return SizeUnit(ASE->getType(), C.getASTContext()); } - bool assumedNonNegative() { return AssumedNonNegative; } + int64_t asCharUnits() const { return AsCharUnits; } - const NoteTag *createNoteTag(CheckerContext &C) const; + bool canExpress(std::optional<int64_t> Val) const { + return asCharUnits() && (!Val || !(*Val % asCharUnits())); + } -private: - std::string getMessage(PathSensitiveBugReport &BR) const; - - /// Return true if information about the value of `Sym` can put constraints - /// on some symbol which is interesting within the bug report `BR`. - /// In particular, this returns true when `Sym` is interesting within `BR`; - /// but it also returns true if `Sym` is an expression that contains integer - /// constants and a single symbolic operand which is interesting (in `BR`). - /// We need to use this instead of plain `BR.isInteresting()` because if we - /// are analyzing code like - /// int array[10]; - /// int f(int arg) { - /// return array[arg] && array[arg + 10]; - /// } - /// then the byte offsets are `arg * 4` and `(arg + 10) * 4`, which are not - /// sub-expressions of each other (but `getSimplifiedOffsets` is smart enough - /// to detect this out of bounds access). - static bool providesInformationAboutInteresting(SymbolRef Sym, - PathSensitiveBugReport &BR); - static bool providesInformationAboutInteresting(SVal SV, - PathSensitiveBugReport &BR) { - return providesInformationAboutInteresting(SV.getAsSymbol(), BR); + std::string asExtentDesc() const { + if (isBytes()) + return "the extent of"; + return formatv("the number of '{0}' elements in", AsType.getAsString()); + } + + std::string asElementName() const { + if (isBytes()) + return "byte"; + return formatv("'{0}' element", AsType.getAsString()); } }; -struct Messages { - std::string Short, Full; +} // anonymous namespace + +namespace clang::ento::bounds { + +struct CheckFlags { + unsigned CheckUnderflow : 1; + unsigned OffsetObviouslyNonnegative : 1; + unsigned AcceptPastTheEnd : 1; }; -enum class BadOffsetKind { Negative, Overflowing, Indeterminate }; +class CheckResult; -constexpr llvm::StringLiteral Adjectives[] = {"a negative", "an overflowing", - "a negative or overflowing"}; -static StringRef asAdjective(BadOffsetKind Problem) { - return Adjectives[static_cast<int>(Problem)]; -} +/// Checks the validity of accessing a memory region with extent \p Extent at +/// offset \p Offset. The \p Flags influence the semantics of the check, in +/// particular if `AcceptPastTheEnd` is true, then Offset == Extent is also +/// accepted as valid. +CheckResult checkBounds(ProgramStateRef State, SValBuilder &SVB, NonLoc Offset, + std::optional<NonLoc> Extent, CheckFlags Flags); -constexpr llvm::StringLiteral Prepositions[] = {"preceding", "after the end of", - "around"}; -static StringRef asPreposition(BadOffsetKind Problem) { - return Prepositions[static_cast<int>(Problem)]; -} +class CheckResult { +public: + /// When true, the bounds check noticed that the value of an unsigned + /// expression is constrained to negative values (because the analyzer + /// skipped the modeling of a cast expression). This execution path must be + /// discarded because it does not represent a real possibility. + /// FIXME: This hack is currently needed to filter out many ugly false + /// positives; but it should be removed when we fix cast modeling. + bool isCorruptedState() const { return IsCorruptedState; } + + /// When true, the checked offset may be in bounds. + /// As an exceptional case, this is also true for idiomatic expressions that + /// define a past-the-end pointer (and do not dereference it). + bool mayBeInBounds() const { return static_cast<bool>(InBoundsState); } + + /// When true, the checked offset may be negative. + bool mayUnderflow() const { return MayUnderflow; } + /// When true, the checked offset may be >= the extent of the region. + /// As an exceptional case, this is also false for idiomatic expressions that + /// define a past-the-end pointer (and do not dereference it). + bool mayOverflow() const { return ExtentIfMayOverflow.has_value(); } + /// When true, the checked offset may be out of bounds. + bool mayBeInvalid() const { return MayUnderflow || ExtentIfMayOverflow; } + + /// Returns the offset of the accessed location from the beginning of the + /// accessd region. + NonLoc getOffset() const { return Offset; } + + /// Returns the extent of the accessed region if it is relevant (because the + /// offset may overflow it), otherwise returns std::nullopt. + std::optional<NonLoc> getExtentIfMayOverflow() const { + return ExtentIfMayOverflow; + } + + /// Returns the program state that should be used for continuing the analysis + /// after this bounds check. This returns null if mayBeInBounds() is false, in + /// that case the state before the check should be used in the error node. + /// Note that we also have a valid state in the exception case when the + /// 'access' calculates the past-the-end pointer without dereferencing it. + ProgramStateRef getInBoundsState() const { return InBoundsState; } + + friend CheckResult checkBounds(ProgramStateRef State, SValBuilder &SVB, + NonLoc Offset, std::optional<NonLoc> Extent, + CheckFlags Flags); + +private: + // Offset of the accessed location, measured from the start of the region. + // TODO: As of now, the offset and the extent are always measured in bytes, + // but we will probably need to allow other size units in the future. + const NonLoc Offset; + + explicit CheckResult(NonLoc Offs) : Offset(Offs) {} + + bool IsCorruptedState = false; + bool MayUnderflow = false; + std::optional<NonLoc> ExtentIfMayOverflow = std::nullopt; + ProgramStateRef InBoundsState = nullptr; +}; + +} // namespace clang::ento::bounds + +namespace { +/// Strings that will be passed to the parameters 'desc' and 'fullDesc' of the +/// constructor of 'PathSensitiveBugReport'. +struct BugDescription { + std::string Short; + std::string Full; +}; // NOTE: The `ArraySubscriptExpr` and `UnaryOperator` callbacks are `PostStmt` // instead of `PreStmt` because the current implementation passes the whole @@ -157,11 +216,11 @@ class ArrayBoundChecker : public Checker<check::PostStmt<ArraySubscriptExpr>, BugType BT{this, "Out-of-bound access"}; BugType TaintBT{this, "Out-of-bound access", categories::TaintedData}; - void performCheck(const Expr *E, CheckerContext &C) const; + void handleAccessExpr(const Expr *E, CheckerContext &C) const; - void reportOOB(CheckerContext &C, ProgramStateRef ErrorState, Messages Msgs, - NonLoc Offset, std::optional<NonLoc> Extent, - bool IsTaintBug = false) const; + void reportOOB(CheckerContext &C, ProgramStateRef ErrorState, + BugDescription Desc, NonLoc Offset, + std::optional<NonLoc> Extent, bool IsTaintBug = false) const; static void markPartsInteresting(PathSensitiveBugReport &BR, ProgramStateRef ErrorState, NonLoc Val, @@ -171,27 +230,57 @@ class ArrayBoundChecker : public Checker<check::PostStmt<ArraySubscriptExpr>, static bool isOffsetObviouslyNonnegative(const Expr *E, CheckerContext &C); - static bool isIdiomaticPastTheEndPtr(const Expr *E, ProgramStateRef State, - NonLoc Offset, NonLoc Limit, - CheckerContext &C); static bool isInAddressOf(const Stmt *S, ASTContext &AC); public: void checkPostStmt(const ArraySubscriptExpr *E, CheckerContext &C) const { - performCheck(E, C); + handleAccessExpr(E, C); } void checkPostStmt(const UnaryOperator *E, CheckerContext &C) const { if (E->getOpcode() == UO_Deref) - performCheck(E, C); + handleAccessExpr(E, C); } void checkPostStmt(const MemberExpr *E, CheckerContext &C) const { if (E->isArrow()) - performCheck(E->getBase(), C); + handleAccessExpr(E->getBase(), C); } }; } // anonymous namespace +/// Return true if information about the value of \p SV can put constraints +/// on some symbol which is interesting within the bug report \p BR. +/// In particular, this returns true when \p SV is interesting within \p BR; +/// but it also returns true if \p SV is an expression that contains integer +/// constants and a single symbolic operand which is interesting (in \p BR). +/// We need to use this instead of plain `BR.isInteresting()` because if we +/// are analyzing code like +/// int array[10]; +/// int f(int arg) { +/// return array[arg] && array[arg + 10]; +/// } +/// then the byte offsets are `arg * 4` and `(arg + 10) * 4`, which are not +/// sub-expressions of each other (but `getSimplifiedOffsets` is smart enough +/// to detect this out of bounds access). +static bool isDeterminedByInterestingSymbol(SVal SV, + PathSensitiveBugReport &BR) { + SymbolRef Sym = SV.getAsSymbol(); + if (!Sym) + return false; + for (SymbolRef PartSym : Sym->symbols()) { + // The interestingess mark may appear on any layer as we're stripping off + // the SymIntExpr, UnarySymExpr etc. layers... + if (BR.isInteresting(PartSym)) + return true; + // ...but if both sides of the expression are symbolic, then there is no + // practical algorithm to produce separate constraints for the two + // operands (from the single combined result). + if (isa<SymSymExpr>(PartSym)) + return false; + } + return false; +} + /// For a given Location that can be represented as a symbolic expression /// Arr[Idx] (or perhaps Arr[Idx1][Idx2] etc.), return the parent memory block /// Arr and the distance of Location from the beginning of Arr (expressed in a @@ -402,61 +491,53 @@ static std::optional<int64_t> getConcreteValue(std::optional<NonLoc> SV) { return SV ? getConcreteValue(*SV) : std::nullopt; } -/// Try to divide `Val1` and `Val2` (in place) by `Divisor` and return true if -/// it can be performed (`Divisor` is nonzero and there is no remainder). The -/// values `Val1` and `Val2` may be nullopt and in that case the corresponding -/// division is considered to be successful. -static bool tryDividePair(std::optional<int64_t> &Val1, - std::optional<int64_t> &Val2, int64_t Divisor) { - if (!Divisor) - return false; - const bool Val1HasRemainder = Val1 && *Val1 % Divisor; - const bool Val2HasRemainder = Val2 && *Val2 % Divisor; - if (Val1HasRemainder || Val2HasRemainder) - return false; - if (Val1) - *Val1 /= Divisor; - if (Val2) - *Val2 /= Divisor; - return true; +static StringRef getAdjective(const bounds::CheckResult &R) { + return (R.mayUnderflow() + ? (R.mayOverflow() ? "a negative or overflowing" : "a negative") + : (R.mayOverflow() ? "an overflowing" : "a valid")); } -static Messages getNonTaintMsgs(const ASTContext &ACtx, - const MemSpaceRegion *Space, - const SubRegion *Region, NonLoc Offset, - std::optional<NonLoc> Extent, SVal Location, - BadOffsetKind Problem) { - std::string RegName = getRegionName(Space, Region); - const auto *EReg = Location.getAsRegion()->getAs<ElementRegion>(); - assert(EReg && "this checker only handles element access"); - QualType ElemType = EReg->getElementType(); +static StringRef getPreposition(const bounds::CheckResult &R) { + return (R.mayUnderflow() ? (R.mayOverflow() ? "around" : "preceding") + : (R.mayOverflow() ? "after the end of" : "within")); +} - std::optional<int64_t> OffsetN = getConcreteValue(Offset); - std::optional<int64_t> ExtentN = getConcreteValue(Extent); +static BugDescription describeInvalidAccess(bounds::CheckResult Res, + StringRef RegName, SizeUnit SU) { + std::optional<int64_t> OffsetN = getConcreteValue(Res.getOffset()); + std::optional<int64_t> ExtentN = + getConcreteValue(Res.getExtentIfMayOverflow()); - int64_t ElemSize = ACtx.getTypeSizeInChars(ElemType).getQuantity(); + if (SU.canExpress(OffsetN) && SU.canExpress(ExtentN)) { + if (OffsetN) + *OffsetN /= SU.asCharUnits(); + if (ExtentN) + *ExtentN /= SU.asCharUnits(); + } else { + // Fall back to reporting the offsets in bytes. + SU = SizeUnit::bytes(); + } - bool UseByteOffsets = !tryDividePair(OffsetN, ExtentN, ElemSize); - const char *OffsetOrIndex = UseByteOffsets ? "byte offset" : "index"; + StringRef OffsetOrIndex = SU.isBytes() ? "byte offset" : "index"; SmallString<256> Buf; llvm::raw_svector_ostream Out(Buf); Out << "Access of "; - if (OffsetN && !ExtentN && !UseByteOffsets) { + if (OffsetN && !ExtentN && !SU.isBytes()) { // If the offset is reported as an index, then the report must mention the // element type (because it is not always clear from the code). It's more // natural to mention the element type later where the extent is described, // but if the extent is unknown/irrelevant, then the element type can be // inserted into the message at this point. - Out << "'" << ElemType.getAsString() << "' element in "; + Out << SU.asElementName() << " in "; } Out << RegName << " at "; if (OffsetN) { - if (Problem == BadOffsetKind::Negative) + if (Res.mayUnderflow() && !Res.mayOverflow()) Out << "negative "; Out << OffsetOrIndex << " " << *OffsetN; } else { - Out << asAdjective(Problem) << " " << OffsetOrIndex; + Out << getAdjective(Res) << " " << OffsetOrIndex; } if (ExtentN) { Out << ", while it holds only "; @@ -464,24 +545,20 @@ static Messages getNonTaintMsgs(const ASTContext &ACtx, Out << *ExtentN; else Out << "a single"; - if (UseByteOffsets) - Out << " byte"; - else - Out << " '" << ElemType.getAsString() << "' element"; + + Out << ' ' << SU.asElementName(); if (*ExtentN > 1) Out << "s"; } - return {formatv("Out of bound access to memory {0} {1}", - asPreposition(Problem), RegName), + return {formatv("Out of bound access to memory {0} {1}", getPreposition(Res), + RegName), std::string(Buf)}; } -static Messages getTaintMsgs(const MemSpaceRegion *Space, - const SubRegion *Region, const char *OffsetName, - bool AlsoMentionUnderflow) { - std::string RegName = getRegionName(Space, Region); +static BugDescription describeTaintBug(StringRef RegName, StringRef OffsetName, + bool AlsoMentionUnderflow) { return {formatv("Potential out of bound access to {0} with tainted {1}", RegName, OffsetName), formatv("Access of {0} with a tainted {1} that may be {2}too large", @@ -489,21 +566,18 @@ static Messages getTaintMsgs(const MemSpaceRegion *Space, AlsoMentionUnderflow ? "negative or " : "")}; } -const NoteTag *StateUpdateReporter::createNoteTag(CheckerContext &C) const { - // Don't create a note tag if we didn't assume anything: - if (!AssumedNonNegative && !AssumedUpperBound) - return nullptr; - - return C.getNoteTag([*this](PathSensitiveBugReport &BR) -> std::string { - return getMessage(BR); - }); -} - -std::string StateUpdateReporter::getMessage(PathSensitiveBugReport &BR) const { - bool ShouldReportNonNegative = AssumedNonNegative; - if (!providesInformationAboutInteresting(ByteOffsetVal, BR)) { - if (AssumedUpperBound && - providesInformationAboutInteresting(*AssumedUpperBound, BR)) { +/// When the access was ambiguous (that is, mayBeInBounds() && mayBeInvalid()), +/// returns the note "assuming in bounds" note that is relevant for the bug +/// report \p BR. When the access wasn't ambiguous or the the assumption is +/// irrelevant for \p BR, this returns the empty string (which signifies "do +/// not emit a note tag" when returned by a note tag callback). +static std::string getAssumptionNote(bounds::CheckResult Res, + PathSensitiveBugReport &BR, + StringRef RegName, SizeUnit SU) { + bool ShouldReportNonNegative = Res.mayUnderflow(); + if (!isDeterminedByInterestingSymbol(Res.getOffset(), BR)) { + std::optional<NonLoc> E = Res.getExtentIfMayOverflow(); + if (E && isDeterminedByInterestingSymbol(*E, BR)) { // Even if the byte offset isn't interesting (e.g. it's a constant value), // the assumption can still be interesting if it provides information // about an interesting symbolic upper bound. @@ -514,20 +588,28 @@ std::string StateUpdateReporter::getMessage(PathSensitiveBugReport &BR) const { } } - std::optional<int64_t> OffsetN = getConcreteValue(ByteOffsetVal); - std::optional<int64_t> ExtentN = getConcreteValue(AssumedUpperBound); + std::optional<int64_t> OffsetN = getConcreteValue(Res.getOffset()); + std::optional<int64_t> ExtentN = + getConcreteValue(Res.getExtentIfMayOverflow()); - const bool UseIndex = - ElementSize && tryDividePair(OffsetN, ExtentN, *ElementSize); + if (SU.canExpress(OffsetN) && SU.canExpress(ExtentN)) { + if (OffsetN) + *OffsetN /= SU.asCharUnits(); + if (ExtentN) + *ExtentN /= SU.asCharUnits(); + } else { + // Fall back to reporting the offsets in bytes. + SU = SizeUnit::bytes(); + } SmallString<256> Buf; llvm::raw_svector_ostream Out(Buf); Out << "Assuming "; - if (UseIndex) { + if (!SU.isBytes()) { Out << "index "; if (OffsetN) Out << "'" << OffsetN << "' "; - } else if (AssumedUpperBound) { + } else if (Res.mayOverflow()) { Out << "byte offset "; if (OffsetN) Out << "'" << OffsetN << "' "; @@ -539,41 +621,19 @@ std::string StateUpdateReporter::getMessage(PathSensitiveBugReport &BR) const { if (ShouldReportNonNegative) { Out << " non-negative"; } - if (AssumedUpperBound) { + if (Res.mayOverflow()) { if (ShouldReportNonNegative) Out << " and"; Out << " less than "; if (ExtentN) Out << *ExtentN << ", "; - if (UseIndex && ElementType) - Out << "the number of '" << ElementType->getAsString() - << "' elements in "; - else - Out << "the extent of "; - Out << getRegionName(Space, Reg); + Out << SU.asExtentDesc() << ' ' << RegName; } return std::string(Out.str()); } -bool StateUpdateReporter::providesInformationAboutInteresting( - SymbolRef Sym, PathSensitiveBugReport &BR) { - if (!Sym) - return false; - for (SymbolRef PartSym : Sym->symbols()) { - // The interestingess mark may appear on any layer as we're stripping off - // the SymIntExpr, UnarySymExpr etc. layers... - if (BR.isInteresting(PartSym)) - return true; - // ...but if both sides of the expression are symbolic, then there is no - // practical algorithm to produce separate constraints for the two - // operands (from the single combined result). - if (isa<SymSymExpr>(PartSym)) - return false; - } - return false; -} - -void ArrayBoundChecker::performCheck(const Expr *E, CheckerContext &C) const { +void ArrayBoundChecker::handleAccessExpr(const Expr *E, + CheckerContext &C) const { const SVal Location = C.getSVal(E); // The header ctype.h (from e.g. glibc) implements the isXXXXX() macros as @@ -595,27 +655,86 @@ void ArrayBoundChecker::performCheck(const Expr *E, CheckerContext &C) const { auto [Reg, ByteOffset] = *RawOffset; - // The state updates will be reported as a single note tag, which will be - // composed by this helper class. - StateUpdateReporter SUR(Reg, ByteOffset, E, C); + const MemSpaceRegion *Space = Reg->getMemorySpace(State); + auto Extent = getDynamicExtent(State, Reg, SVB).getAs<NonLoc>(); + + // A symbolic region in unknown space represents an unknown pointer that + // may point into the middle of an array, so we don't look for underflows. + // Both conditions are significant because we want to check underflows in + // symbolic regions on the heap (which may be introduced by checkers like + // MallocChecker that call SValBuilder::getConjuredHeapSymbolVal()) and + // non-symbolic regions (e.g. a field subregion of a symbolic region) in + // unknown space. + + bounds::CheckFlags Flags = { + /*CheckUnderflow=*/!(isa<SymbolicRegion>(Reg) && + isa<UnknownSpaceRegion>(Space)), + /*OffsetObviouslyNonnegative=*/isOffsetObviouslyNonnegative(E, C), + /*AcceptPastTheEnd=*/isa<ArraySubscriptExpr>(E) && + isInAddressOf(E, C.getASTContext()), + }; + + bounds::CheckResult Res = checkBounds(State, SVB, ByteOffset, Extent, Flags); + + if (Res.isCorruptedState()) { + C.addSink(); + return; + } + + std::string RegName = getRegionName(Space, Reg); + + const NoteTag *T = nullptr; + if (Res.mayBeInvalid()) { + if (!Res.mayBeInBounds()) { + SizeUnit SU = SizeUnit::forSVal(Location, C.getASTContext()); + BugDescription Desc = describeInvalidAccess(Res, RegName, SU); + reportOOB(C, State, Desc, ByteOffset, Res.getExtentIfMayOverflow()); + return; + } + + // FIXME: Remove `Res.mayOverflow()` and provide diagnostics for the case + // when the tainted access operation cannot overflow but can underflow. + // (This is an NFC commit, so I cannot include this improvement.) + if (Res.mayOverflow() && isTainted(State, ByteOffset)) { + // Diagnostic detail: saying "tainted offset" is always correct, but + // the common case is that 'idx' is tainted in 'arr[idx]' and then it's + // nicer to say "tainted index". + StringRef OffsetName = "offset"; + if (const auto *ASE = dyn_cast<ArraySubscriptExpr>(E)) + if (isTainted(State, ASE->getIdx(), C.getStackFrame())) + OffsetName = "index"; + + BugDescription Desc = + describeTaintBug(RegName, OffsetName, Res.mayUnderflow()); + reportOOB(C, State, Desc, ByteOffset, Extent, /*IsTaintBug=*/true); + return; + } + + SizeUnit SU = SizeUnit::forExpr(E, C); + T = C.getNoteTag( + [Res, RegName, SU](PathSensitiveBugReport &BR) -> std::string { + return getAssumptionNote(Res, BR, RegName, SU); + }); + } + + C.addTransition(Res.getInBoundsState(), T); +} + +bounds::CheckResult bounds::checkBounds(ProgramStateRef State, SValBuilder &SVB, + NonLoc Offset, + std::optional<NonLoc> Extent, + bounds::CheckFlags Flags) { + + bounds::CheckResult Res(Offset); // CHECK LOWER BOUND - const MemSpaceRegion *Space = Reg->getMemorySpace(State); - if (!(isa<SymbolicRegion>(Reg) && isa<UnknownSpaceRegion>(Space))) { - // A symbolic region in unknown space represents an unknown pointer that - // may point into the middle of an array, so we don't look for underflows. - // Both conditions are significant because we want to check underflows in - // symbolic regions on the heap (which may be introduced by checkers like - // MallocChecker that call SValBuilder::getConjuredHeapSymbolVal()) and - // non-symbolic regions (e.g. a field subregion of a symbolic region) in - // unknown space. - auto [PrecedesLowerBound, WithinLowerBound] = compareValueToThreshold( - State, ByteOffset, SVB.makeZeroArrayIndex(), SVB); + if (Flags.CheckUnderflow) { + auto [PrecedesLowerBound, WithinLowerBound] = + compareValueToThreshold(State, Offset, SVB.makeZeroArrayIndex(), SVB); if (PrecedesLowerBound) { // The analyzer thinks that the offset may be invalid (negative)... - - if (isOffsetObviouslyNonnegative(E, C)) { + if (Flags.OffsetObviouslyNonnegative) { // ...but the offset is obviously non-negative (clear array subscript // with an unsigned index), so we're in a buggy situation. @@ -634,24 +753,19 @@ void ArrayBoundChecker::performCheck(const Expr *E, CheckerContext &C) const { if (!WithinLowerBound) { // The state is completely nonsense -- let's just sink it! - C.addSink(); - return; + 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. - Messages Msgs = getNonTaintMsgs(C.getASTContext(), Space, Reg, - ByteOffset, /*Extent=*/std::nullopt, - Location, BadOffsetKind::Negative); - reportOOB(C, PrecedesLowerBound, Msgs, ByteOffset, std::nullopt); - return; + return Res; } - // ...but it can be valid as well, so the checker will (optimistically) - // assume that it's valid and mention this in the note tag. - SUR.recordNonNegativeAssumption(); } } @@ -663,71 +777,42 @@ void ArrayBoundChecker::performCheck(const Expr *E, CheckerContext &C) const { } // CHECK UPPER BOUND - DefinedOrUnknownSVal Size = getDynamicExtent(State, Reg, SVB); - if (auto KnownSize = Size.getAs<NonLoc>()) { + if (Extent) { // In a situation where both underflow and overflow are possible (but the // index is either tainted or known to be invalid), the logic of this // checker will first assume that the offset is non-negative, and then // (with this additional assumption) it will detect an overflow error. // In this situation the warning message should mention both possibilities. - bool AlsoMentionUnderflow = SUR.assumedNonNegative(); auto [WithinUpperBound, ExceedsUpperBound] = - compareValueToThreshold(State, ByteOffset, *KnownSize, SVB); + compareValueToThreshold(State, Offset, *Extent, SVB); if (ExceedsUpperBound) { // The offset may be invalid (>= Size)... + Res.ExtentIfMayOverflow = Extent; + if (!WithinUpperBound) { // ...and it cannot be within bounds, so report an error, unless we can // definitely determine that this is an idiomatic `&array[size]` // expression that calculates the past-the-end pointer. - if (isIdiomaticPastTheEndPtr(E, ExceedsUpperBound, ByteOffset, - *KnownSize, C)) { - C.addTransition(ExceedsUpperBound, SUR.createNoteTag(C)); - return; + if (Flags.AcceptPastTheEnd) { + auto [EqualsToThreshold, NotEqualToThreshold] = + compareValueToThreshold(State, Offset, *Extent, SVB, + /*CheckEquality=*/true); + if (EqualsToThreshold && !NotEqualToThreshold) { + Res.ExtentIfMayOverflow = std::nullopt; + Res.InBoundsState = EqualsToThreshold; + } } - - BadOffsetKind Problem = AlsoMentionUnderflow - ? BadOffsetKind::Indeterminate - : BadOffsetKind::Overflowing; - Messages Msgs = - getNonTaintMsgs(C.getASTContext(), Space, Reg, ByteOffset, - *KnownSize, Location, Problem); - reportOOB(C, ExceedsUpperBound, Msgs, ByteOffset, KnownSize); - return; - } - // ...and it can be valid as well... - if (isTainted(State, ByteOffset)) { - // ...but it's tainted, so report an error. - - // Diagnostic detail: saying "tainted offset" is always correct, but - // the common case is that 'idx' is tainted in 'arr[idx]' and then it's - // nicer to say "tainted index". - const char *OffsetName = "offset"; - if (const auto *ASE = dyn_cast<ArraySubscriptExpr>(E)) - if (isTainted(State, ASE->getIdx(), C.getStackFrame())) - OffsetName = "index"; - - Messages Msgs = - getTaintMsgs(Space, Reg, OffsetName, AlsoMentionUnderflow); - reportOOB(C, ExceedsUpperBound, Msgs, ByteOffset, KnownSize, - /*IsTaintBug=*/true); - return; + return Res; } - // ...and it isn't tainted, so the checker will (optimistically) assume - // that the offset is in bounds and mention this in the note tag. - SUR.recordUpperBoundAssumption(*KnownSize); } - - // 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 (WithinUpperBound) State = WithinUpperBound; } - // Add a transition, reporting the state updates that we accumulated. - C.addTransition(State, SUR.createNoteTag(C)); + Res.InBoundsState = State; + return Res; } void ArrayBoundChecker::markPartsInteresting(PathSensitiveBugReport &BR, @@ -754,7 +839,7 @@ void ArrayBoundChecker::markPartsInteresting(PathSensitiveBugReport &BR, } void ArrayBoundChecker::reportOOB(CheckerContext &C, ProgramStateRef ErrorState, - Messages Msgs, NonLoc Offset, + BugDescription Desc, NonLoc Offset, std::optional<NonLoc> Extent, bool IsTaintBug /*=false*/) const { @@ -763,7 +848,7 @@ void ArrayBoundChecker::reportOOB(CheckerContext &C, ProgramStateRef ErrorState, return; auto BR = std::make_unique<PathSensitiveBugReport>( - IsTaintBug ? TaintBT : BT, Msgs.Short, Msgs.Full, ErrorNode); + IsTaintBug ? TaintBT : BT, Desc.Short, Desc.Full, ErrorNode); // FIXME: ideally we would just call trackExpressionValue() and that would // "do the right thing": mark the relevant symbols as interesting, track the @@ -824,18 +909,6 @@ bool ArrayBoundChecker::isInAddressOf(const Stmt *S, ASTContext &ACtx) { return UnaryOp && UnaryOp->getOpcode() == UO_AddrOf; } -bool ArrayBoundChecker::isIdiomaticPastTheEndPtr(const Expr *E, - ProgramStateRef State, - NonLoc Offset, NonLoc Limit, - CheckerContext &C) { - if (isa<ArraySubscriptExpr>(E) && isInAddressOf(E, C.getASTContext())) { - auto [EqualsToThreshold, NotEqualToThreshold] = compareValueToThreshold( - State, Offset, Limit, C.getSValBuilder(), /*CheckEquality=*/true); - return EqualsToThreshold && !NotEqualToThreshold; - } - return false; -} - void ento::registerArrayBoundChecker(CheckerManager &mgr) { mgr.registerChecker<ArrayBoundChecker>(); } diff --git a/clang/test/Analysis/ArrayBound/assumption-reporting.c b/clang/test/Analysis/ArrayBound/assumption-reporting.c index 6ae2a31f22873..0b37ed8456b70 100644 --- a/clang/test/Analysis/ArrayBound/assumption-reporting.c +++ b/clang/test/Analysis/ArrayBound/assumption-reporting.c @@ -54,7 +54,28 @@ int assumingLower(int arg) { if (arg >= 10) return 0; int a = TenElements[arg]; - // expected-note@-1 {{Assuming index is non-negative}} + // expected-note-re@-1 {{Assuming index is non-negative{{$}}}} + int b = TenElements[arg + 10]; + // expected-warning@-1 {{Out of bound access to memory after the end of 'TenElements'}} + // expected-note@-2 {{Access of 'TenElements' at an overflowing index, while it holds only 10 'int' elements}} + return a + b; +} + +int assumingLowerOnlyUseIndex(int arg) { + // This testcase validates that the note tag says that the _index_ is + // non-negative when there is no upper bound assumption -- even in the case + // when the extent (which is totally irrelevant) is not an integer multiple + // of the element size. + + char TwoAndHalfInts[10] = {0}; + // expected-note@+2 {{Assuming 'arg' is < 2}} + // expected-note@+1 {{Taking false branch}} + if (arg >= 2) + return 0; + + int a = ((int*)TwoAndHalfInts)[arg]; + // expected-note-re@-1 {{Assuming index is non-negative{{$}}}} + int b = TenElements[arg + 10]; // expected-warning@-1 {{Out of bound access to memory after the end of 'TenElements'}} // expected-note@-2 {{Access of 'TenElements' at an overflowing index, while it holds only 10 'int' elements}} @@ -94,6 +115,22 @@ int assumingUpperIrrelevant(int arg) { return a + b; } +int assumingLowerIrrelevant(int arg) { + // FIXME: Analogously to `assumingUpperIrrelevant` here the assumption + // "assuming index is non-negative" is irrelevant, but printed. + // + // expected-note@+2 {{Assuming 'arg' is < 10}} + // expected-note@+1 {{Taking false branch}} + if (arg >= 10) + return 0; + int a = TenElements[arg]; + // expected-note-re@-1 {{Assuming index is non-negative{{$}}}} + int b = TenElements[arg - 10]; + // expected-warning@-1 {{Out of bound access to memory preceding 'TenElements'}} + // expected-note@-2 {{Access of 'TenElements' at a negative index}} + return a + b; +} + int assumingUpperUnsigned(unsigned arg) { int a = TenElements[arg]; // expected-note@-1 {{Assuming index is less than 10, the number of 'int' elements in 'TenElements'}} @@ -193,8 +230,8 @@ int assumingExtent(int arg) { } int *extentInterestingness(int arg) { - // Verify that in an out-of-bounds access issue the extent is marked as - // interesting (so assumptions about its value are printed). + // Verify that in a buffer overflow issue the extent is marked as interesting + // (so assumptions about its value are printed). int *mem = (int*)malloc(arg); TenElements[arg] = 123; @@ -205,6 +242,18 @@ int *extentInterestingness(int arg) { // expected-note@-2 {{Access of 'int' element in the heap area at index 12}} } +int *extentNonInterestingInUnderflow(int arg) { + // Verify that in a buffer underflow issue the extent is _not_ marked as + // interesting (because it does not influence anything). + int *mem = (int*)malloc(arg); + + TenElements[arg] = 123; // no-note: arg is not interesting + + return &mem[-2]; + // expected-warning@-1 {{Out of bound access to memory preceding the heap area}} + // expected-note@-2 {{Access of 'int' element in the heap area at negative index -2}} +} + int triggeredByAnyReport(int arg) { // Verify that note tags explaining the assumptions made by ArrayBound are // not limited to ArrayBound reports but will appear on any bug report (that diff --git a/clang/test/Analysis/ArrayBound/verbose-tests.c b/clang/test/Analysis/ArrayBound/verbose-tests.c index c0da93ea48591..c0b1f2a8ae6be 100644 --- a/clang/test/Analysis/ArrayBound/verbose-tests.c +++ b/clang/test/Analysis/ArrayBound/verbose-tests.c @@ -44,8 +44,7 @@ struct TwoInts underflowReportedAsStruct(void) { struct TwoInts underflowOnlyByteOffset(void) { // In this case the negative byte offset is not a multiple of the size of the - // accessed element, so the part "= -... * sizeof(type)" is omitted at the - // end of the message. + // accessed element, so we use a byte offset instead of an index. return *(struct TwoInts*)(TenElements - 3); // expected-warning@-1 {{Out of bound access to memory preceding 'TenElements'}} // expected-note@-2 {{Access of 'TenElements' at negative byte offset -12}} @@ -410,3 +409,19 @@ int *nothingIsCertain(int x, int y) { return mem; } + +#ifndef _WIN32 +// We disable this test under Windows because 'struct Empty {}' has a nozero +// size on that platform. Note that '_WIN32' is also defined on 64-bit systems +// and is apparently the customary way to detect Windows OS. + +struct Empty {}; +struct Empty ZeroSizeElements[10]; + +struct Empty zeroSizeElements(void) { + // FIXME: We probably shouldn't report this access. + return ZeroSizeElements[5]; + // expected-warning@-1 {{Out of bound access to memory after the end of 'ZeroSizeElements'}} + // expected-note@-2 {{Access of 'ZeroSizeElements' at byte offset 0, while it holds only 0 byte}} +} +#endif _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
