//===-- UncheckedOptionalAccessModel.cpp ------------------------*- 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 defines a dataflow analysis that detects unsafe uses of optional // values. // //===----------------------------------------------------------------------===// #include "clang/Analysis/FlowSensitive/Models/UncheckedOptionalAccessModel.h" #include "clang/AST/ASTContext.h" #include "clang/AST/DeclCXX.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" #include "clang/AST/Stmt.h" #include "clang/ASTMatchers/ASTMatchers.h" #include "clang/ASTMatchers/ASTMatchersMacros.h" #include "clang/Analysis/CFG.h" #include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h" #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h" #include "clang/Analysis/FlowSensitive/Formula.h" #include "clang/Analysis/FlowSensitive/NoopLattice.h" #include "clang/Analysis/FlowSensitive/StorageLocation.h" #include "clang/Analysis/FlowSensitive/Value.h" #include "clang/Basic/SourceLocation.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Casting.h" #include "llvm/Support/ErrorHandling.h" #include #include #include #include namespace clang { namespace dataflow { static bool isTopLevelNamespaceWithName(const NamespaceDecl &NS, llvm::StringRef Name) { return NS.getDeclName().isIdentifier() && NS.getName() == Name && NS.getParent() != nullptr && NS.getParent()->isTranslationUnit(); } static bool hasOptionalClassName(const CXXRecordDecl &RD) { if (!RD.getDeclName().isIdentifier()) return false; if (RD.getName() == "optional") { if (const auto *N = dyn_cast_or_null(RD.getDeclContext())) return N->isStdNamespace() || isTopLevelNamespaceWithName(*N, "absl"); return false; } if (RD.getName() == "Optional") { // Check whether namespace is "::base" or "::folly". const auto *N = dyn_cast_or_null(RD.getDeclContext()); return N != nullptr && (isTopLevelNamespaceWithName(*N, "base") || isTopLevelNamespaceWithName(*N, "folly")); } return false; } static const CXXRecordDecl *getOptionalBaseClass(const CXXRecordDecl *RD) { if (RD == nullptr) return nullptr; if (hasOptionalClassName(*RD)) return RD; if (!RD->hasDefinition()) return nullptr; for (const CXXBaseSpecifier &Base : RD->bases()) if (const CXXRecordDecl *BaseClass = getOptionalBaseClass(Base.getType()->getAsCXXRecordDecl())) return BaseClass; return nullptr; } namespace { using namespace ::clang::ast_matchers; using LatticeTransferState = TransferState; AST_MATCHER(CXXRecordDecl, optionalClass) { return hasOptionalClassName(Node); } AST_MATCHER(CXXRecordDecl, optionalOrDerivedClass) { return getOptionalBaseClass(&Node) != nullptr; } auto desugarsToOptionalType() { return hasUnqualifiedDesugaredType( recordType(hasDeclaration(cxxRecordDecl(optionalClass())))); } auto desugarsToOptionalOrDerivedType() { return hasUnqualifiedDesugaredType( recordType(hasDeclaration(cxxRecordDecl(optionalOrDerivedClass())))); } auto hasOptionalType() { return hasType(desugarsToOptionalType()); } /// Matches any of the spellings of the optional types and sugar, aliases, /// derived classes, etc. auto hasOptionalOrDerivedType() { return hasType(desugarsToOptionalOrDerivedType()); } QualType getPublicType(const Expr *E) { auto *Cast = dyn_cast(E->IgnoreParens()); if (Cast == nullptr || Cast->getCastKind() != CK_UncheckedDerivedToBase) { QualType Ty = E->getType(); if (Ty->isPointerType()) return Ty->getPointeeType(); return Ty; } // Is the derived type that we're casting from the type of `*this`? In this // special case, we can upcast to the base class even if the base is // non-public. bool CastingFromThis = isa(Cast->getSubExpr()); // Find the least-derived type in the path (i.e. the last entry in the list) // that we can access. const CXXBaseSpecifier *PublicBase = nullptr; for (const CXXBaseSpecifier *Base : Cast->path()) { if (Base->getAccessSpecifier() != AS_public && !CastingFromThis) break; PublicBase = Base; CastingFromThis = false; } if (PublicBase != nullptr) return PublicBase->getType(); // We didn't find any public type that we could cast to. There may be more // casts in `getSubExpr()`, so recurse. (If there aren't any more casts, this // will return the type of `getSubExpr()`.) return getPublicType(Cast->getSubExpr()); } // Returns the least-derived type for the receiver of `MCE` that // `MCE.getImplicitObjectArgument()->IgnoreParentImpCasts()` can be downcast to. // Effectively, we upcast until we reach a non-public base class, unless that // base is a base of `*this`. // // This is needed to correctly match methods called on types derived from // `std::optional`. // // Say we have a `struct Derived : public std::optional {} d;` For a call // `d.has_value()`, the `getImplicitObjectArgument()` looks like this: // // ImplicitCastExpr 'const std::__optional_storage_base' lvalue // | __optional_storage_base)> // `-DeclRefExpr 'Derived' lvalue Var 'd' 'Derived' // // The type of the implicit object argument is `__optional_storage_base` // (since this is the internal type that `has_value()` is declared on). If we // call `IgnoreParenImpCasts()` on the implicit object argument, we get the // `DeclRefExpr`, which has type `Derived`. Neither of these types is // `optional`, and hence neither is sufficient for querying whether we are // calling a method on `optional`. // // Instead, starting with the most derived type, we need to follow the chain of // casts QualType getPublicReceiverType(const CXXMemberCallExpr &MCE) { return getPublicType(MCE.getImplicitObjectArgument()); } AST_MATCHER_P(CXXMemberCallExpr, publicReceiverType, ast_matchers::internal::Matcher, InnerMatcher) { return InnerMatcher.matches(getPublicReceiverType(Node), Finder, Builder); } auto isOptionalMemberCallWithNameMatcher( ast_matchers::internal::Matcher matcher, const std::optional &Ignorable = std::nullopt) { return cxxMemberCallExpr(Ignorable ? on(expr(unless(*Ignorable))) : anything(), publicReceiverType(desugarsToOptionalType()), callee(cxxMethodDecl(matcher))); } auto isOptionalOperatorCallWithName( llvm::StringRef operator_name, const std::optional &Ignorable = std::nullopt) { return cxxOperatorCallExpr( hasOverloadedOperatorName(operator_name), callee(cxxMethodDecl(ofClass(optionalClass()))), Ignorable ? callExpr(unless(hasArgument(0, *Ignorable))) : callExpr()); } auto isMakeOptionalCall() { return callExpr(callee(functionDecl(hasAnyName( "std::make_optional", "base::make_optional", "absl::make_optional", "folly::make_optional"))), hasOptionalType()); } auto nulloptTypeDecl() { return namedDecl(hasAnyName("std::nullopt_t", "absl::nullopt_t", "base::nullopt_t", "folly::None")); } auto hasNulloptType() { return hasType(nulloptTypeDecl()); } auto inPlaceClass() { return recordDecl(hasAnyName("std::in_place_t", "absl::in_place_t", "base::in_place_t", "folly::in_place_t")); } auto isOptionalNulloptConstructor() { return cxxConstructExpr( hasDeclaration(cxxConstructorDecl(parameterCountIs(1), hasParameter(0, hasNulloptType()))), hasOptionalOrDerivedType()); } auto isOptionalInPlaceConstructor() { return cxxConstructExpr(hasArgument(0, hasType(inPlaceClass())), hasOptionalOrDerivedType()); } auto isOptionalValueOrConversionConstructor() { return cxxConstructExpr( unless(hasDeclaration( cxxConstructorDecl(anyOf(isCopyConstructor(), isMoveConstructor())))), argumentCountIs(1), hasArgument(0, unless(hasNulloptType())), hasOptionalOrDerivedType()); } auto isOptionalValueOrConversionAssignment() { return cxxOperatorCallExpr( hasOverloadedOperatorName("="), callee(cxxMethodDecl(ofClass(optionalOrDerivedClass()))), unless(hasDeclaration(cxxMethodDecl( anyOf(isCopyAssignmentOperator(), isMoveAssignmentOperator())))), argumentCountIs(2), hasArgument(1, unless(hasNulloptType()))); } auto isOptionalNulloptAssignment() { return cxxOperatorCallExpr( hasOverloadedOperatorName("="), callee(cxxMethodDecl(ofClass(optionalOrDerivedClass()))), argumentCountIs(2), hasArgument(1, hasNulloptType())); } auto isStdSwapCall() { return callExpr(callee(functionDecl(hasName("std::swap"))), argumentCountIs(2), hasArgument(0, hasOptionalOrDerivedType()), hasArgument(1, hasOptionalOrDerivedType())); } auto isStdForwardCall() { return callExpr(callee(functionDecl(hasName("std::forward"))), argumentCountIs(1), hasArgument(0, hasOptionalOrDerivedType())); } constexpr llvm::StringLiteral ValueOrCallID = "ValueOrCall"; auto isValueOrStringEmptyCall() { // `opt.value_or("").empty()` return cxxMemberCallExpr( callee(cxxMethodDecl(hasName("empty"))), onImplicitObjectArgument(ignoringImplicit( cxxMemberCallExpr(on(expr(unless(cxxThisExpr()))), callee(cxxMethodDecl(hasName("value_or"), ofClass(optionalClass()))), hasArgument(0, stringLiteral(hasSize(0)))) .bind(ValueOrCallID)))); } auto isValueOrNotEqX() { auto ComparesToSame = [](ast_matchers::internal::Matcher Arg) { return hasOperands( ignoringImplicit( cxxMemberCallExpr(on(expr(unless(cxxThisExpr()))), callee(cxxMethodDecl(hasName("value_or"), ofClass(optionalClass()))), hasArgument(0, Arg)) .bind(ValueOrCallID)), ignoringImplicit(Arg)); }; // `opt.value_or(X) != X`, for X is `nullptr`, `""`, or `0`. Ideally, we'd // support this pattern for any expression, but the AST does not have a // generic expression comparison facility, so we specialize to common cases // seen in practice. FIXME: define a matcher that compares values across // nodes, which would let us generalize this to any `X`. return binaryOperation(hasOperatorName("!="), anyOf(ComparesToSame(cxxNullPtrLiteralExpr()), ComparesToSame(stringLiteral(hasSize(0))), ComparesToSame(integerLiteral(equals(0))))); } auto isCallReturningOptional() { return callExpr(hasType(qualType( anyOf(desugarsToOptionalOrDerivedType(), referenceType(pointee(desugarsToOptionalOrDerivedType())))))); } template auto isComparisonOperatorCall(L lhs_arg_matcher, R rhs_arg_matcher) { return cxxOperatorCallExpr( anyOf(hasOverloadedOperatorName("=="), hasOverloadedOperatorName("!=")), argumentCountIs(2), hasArgument(0, lhs_arg_matcher), hasArgument(1, rhs_arg_matcher)); } /// Ensures that `Expr` is mapped to a `BoolValue` and returns its formula. const Formula &forceBoolValue(Environment &Env, const Expr &Expr) { auto *Value = Env.get(Expr); if (Value != nullptr) return Value->formula(); Value = &Env.makeAtomicBoolValue(); Env.setValue(Expr, *Value); return Value->formula(); } StorageLocation &locForHasValue(const RecordStorageLocation &OptionalLoc) { return OptionalLoc.getSyntheticField("has_value"); } StorageLocation &locForValue(const RecordStorageLocation &OptionalLoc) { return OptionalLoc.getSyntheticField("value"); } /// Sets `HasValueVal` as the symbolic value that represents the "has_value" /// property of the optional at `OptionalLoc`. void setHasValue(RecordStorageLocation &OptionalLoc, BoolValue &HasValueVal, Environment &Env) { Env.setValue(locForHasValue(OptionalLoc), HasValueVal); } /// Returns the symbolic value that represents the "has_value" property of the /// optional at `OptionalLoc`. Returns null if `OptionalLoc` is null. BoolValue *getHasValue(Environment &Env, RecordStorageLocation *OptionalLoc) { if (OptionalLoc == nullptr) return nullptr; StorageLocation &HasValueLoc = locForHasValue(*OptionalLoc); auto *HasValueVal = Env.get(HasValueLoc); if (HasValueVal == nullptr) { HasValueVal = &Env.makeAtomicBoolValue(); Env.setValue(HasValueLoc, *HasValueVal); } return HasValueVal; } QualType valueTypeFromOptionalDecl(const CXXRecordDecl &RD) { auto &CTSD = cast(RD); return CTSD.getTemplateArgs()[0].getAsType(); } /// Returns the number of optional wrappers in `Type`. /// /// For example, if `Type` is `optional>`, the result of this /// function will be 2. int countOptionalWrappers(const ASTContext &ASTCtx, QualType Type) { const CXXRecordDecl *Optional = getOptionalBaseClass(Type->getAsCXXRecordDecl()); if (Optional == nullptr) return 0; return 1 + countOptionalWrappers( ASTCtx, valueTypeFromOptionalDecl(*Optional).getDesugaredType(ASTCtx)); } StorageLocation *getLocBehindPossiblePointer(const Expr &E, const Environment &Env) { if (E.isPRValue()) { if (auto *PointerVal = dyn_cast_or_null(Env.getValue(E))) return &PointerVal->getPointeeLoc(); return nullptr; } return Env.getStorageLocation(E); } void transferUnwrapCall(const Expr *UnwrapExpr, const Expr *ObjectExpr, LatticeTransferState &State) { if (auto *OptionalLoc = cast_or_null( getLocBehindPossiblePointer(*ObjectExpr, State.Env))) { if (State.Env.getStorageLocation(*UnwrapExpr) == nullptr) State.Env.setStorageLocation(*UnwrapExpr, locForValue(*OptionalLoc)); } } void transferArrowOpCall(const Expr *UnwrapExpr, const Expr *ObjectExpr, LatticeTransferState &State) { if (auto *OptionalLoc = cast_or_null( getLocBehindPossiblePointer(*ObjectExpr, State.Env))) State.Env.setValue( *UnwrapExpr, State.Env.create(locForValue(*OptionalLoc))); } void transferMakeOptionalCall(const CallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { setHasValue(State.Env.getResultObjectLocation(*E), State.Env.getBoolLiteralValue(true), State.Env); } void transferOptionalHasValueCall(const CXXMemberCallExpr *CallExpr, const MatchFinder::MatchResult &, LatticeTransferState &State) { if (auto *HasValueVal = getHasValue( State.Env, getImplicitObjectLocation(*CallExpr, State.Env))) { State.Env.setValue(*CallExpr, *HasValueVal); } } /// `ModelPred` builds a logical formula relating the predicate in /// `ValueOrPredExpr` to the optional's `has_value` property. void transferValueOrImpl( const clang::Expr *ValueOrPredExpr, const MatchFinder::MatchResult &Result, LatticeTransferState &State, const Formula &(*ModelPred)(Environment &Env, const Formula &ExprVal, const Formula &HasValueVal)) { auto &Env = State.Env; const auto *MCE = Result.Nodes.getNodeAs(ValueOrCallID); auto *HasValueVal = getHasValue(State.Env, getImplicitObjectLocation(*MCE, State.Env)); if (HasValueVal == nullptr) return; Env.assume(ModelPred(Env, forceBoolValue(Env, *ValueOrPredExpr), HasValueVal->formula())); } void transferValueOrStringEmptyCall(const clang::Expr *ComparisonExpr, const MatchFinder::MatchResult &Result, LatticeTransferState &State) { return transferValueOrImpl(ComparisonExpr, Result, State, [](Environment &Env, const Formula &ExprVal, const Formula &HasValueVal) -> const Formula & { auto &A = Env.arena(); // If the result is *not* empty, then we know the // optional must have been holding a value. If // `ExprVal` is true, though, we don't learn // anything definite about `has_value`, so we // don't add any corresponding implications to // the flow condition. return A.makeImplies(A.makeNot(ExprVal), HasValueVal); }); } void transferValueOrNotEqX(const Expr *ComparisonExpr, const MatchFinder::MatchResult &Result, LatticeTransferState &State) { transferValueOrImpl(ComparisonExpr, Result, State, [](Environment &Env, const Formula &ExprVal, const Formula &HasValueVal) -> const Formula & { auto &A = Env.arena(); // We know that if `(opt.value_or(X) != X)` then // `opt.hasValue()`, even without knowing further // details about the contents of `opt`. return A.makeImplies(ExprVal, HasValueVal); }); } void transferCallReturningOptional(const CallExpr *E, const MatchFinder::MatchResult &Result, LatticeTransferState &State) { RecordStorageLocation *Loc = nullptr; if (E->isPRValue()) { Loc = &State.Env.getResultObjectLocation(*E); } else { Loc = State.Env.get(*E); if (Loc == nullptr) { Loc = &cast(State.Env.createStorageLocation(*E)); State.Env.setStorageLocation(*E, *Loc); } } if (State.Env.getValue(locForHasValue(*Loc)) != nullptr) return; setHasValue(*Loc, State.Env.makeAtomicBoolValue(), State.Env); } void constructOptionalValue(const Expr &E, Environment &Env, BoolValue &HasValueVal) { RecordStorageLocation &Loc = Env.getResultObjectLocation(E); setHasValue(Loc, HasValueVal, Env); } /// Returns a symbolic value for the "has_value" property of an `optional` /// value that is constructed/assigned from a value of type `U` or `optional` /// where `T` is constructible from `U`. BoolValue &valueOrConversionHasValue(QualType DestType, const Expr &E, const MatchFinder::MatchResult &MatchRes, LatticeTransferState &State) { const int DestTypeOptionalWrappersCount = countOptionalWrappers(*MatchRes.Context, DestType); const int ArgTypeOptionalWrappersCount = countOptionalWrappers( *MatchRes.Context, E.getType().getNonReferenceType()); // Is this an constructor of the form `template optional(U &&)` / // assignment of the form `template optional& operator=(U &&)` // (where `T` is assignable / constructible from `U`)? // We recognize this because the number of optionals in the optional being // assigned to is different from the function argument type. if (DestTypeOptionalWrappersCount != ArgTypeOptionalWrappersCount) return State.Env.getBoolLiteralValue(true); // Otherwise, this must be a constructor of the form // `template optional &&)` / assignment of the form // `template optional& operator=(optional &&) // (where, again, `T` is assignable / constructible from `U`). auto *Loc = State.Env.get(E); if (auto *HasValueVal = getHasValue(State.Env, Loc)) return *HasValueVal; return State.Env.makeAtomicBoolValue(); } void transferValueOrConversionConstructor( const CXXConstructExpr *E, const MatchFinder::MatchResult &MatchRes, LatticeTransferState &State) { assert(E->getNumArgs() > 0); constructOptionalValue( *E, State.Env, valueOrConversionHasValue( E->getConstructor()->getThisType()->getPointeeType(), *E->getArg(0), MatchRes, State)); } void transferAssignment(const CXXOperatorCallExpr *E, BoolValue &HasValueVal, LatticeTransferState &State) { assert(E->getNumArgs() > 0); if (auto *Loc = State.Env.get(*E->getArg(0))) { setHasValue(*Loc, HasValueVal, State.Env); // Assign a storage location for the whole expression. State.Env.setStorageLocation(*E, *Loc); } } void transferValueOrConversionAssignment( const CXXOperatorCallExpr *E, const MatchFinder::MatchResult &MatchRes, LatticeTransferState &State) { assert(E->getNumArgs() > 1); transferAssignment( E, valueOrConversionHasValue(E->getArg(0)->getType().getNonReferenceType(), *E->getArg(1), MatchRes, State), State); } void transferNulloptAssignment(const CXXOperatorCallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferAssignment(E, State.Env.getBoolLiteralValue(false), State); } void transferSwap(RecordStorageLocation *Loc1, RecordStorageLocation *Loc2, Environment &Env) { // We account for cases where one or both of the optionals are not modeled, // either lacking associated storage locations, or lacking values associated // to such storage locations. if (Loc1 == nullptr) { if (Loc2 != nullptr) setHasValue(*Loc2, Env.makeAtomicBoolValue(), Env); return; } if (Loc2 == nullptr) { setHasValue(*Loc1, Env.makeAtomicBoolValue(), Env); return; } // Both expressions have locations, though they may not have corresponding // values. In that case, we create a fresh value at this point. Note that if // two branches both do this, they will not share the value, but it at least // allows for local reasoning about the value. To avoid the above, we would // need *lazy* value allocation. // FIXME: allocate values lazily, instead of just creating a fresh value. BoolValue *BoolVal1 = getHasValue(Env, Loc1); if (BoolVal1 == nullptr) BoolVal1 = &Env.makeAtomicBoolValue(); BoolValue *BoolVal2 = getHasValue(Env, Loc2); if (BoolVal2 == nullptr) BoolVal2 = &Env.makeAtomicBoolValue(); setHasValue(*Loc1, *BoolVal2, Env); setHasValue(*Loc2, *BoolVal1, Env); } void transferSwapCall(const CXXMemberCallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { assert(E->getNumArgs() == 1); auto *OtherLoc = State.Env.get(*E->getArg(0)); transferSwap(getImplicitObjectLocation(*E, State.Env), OtherLoc, State.Env); } void transferStdSwapCall(const CallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { assert(E->getNumArgs() == 2); auto *Arg0Loc = State.Env.get(*E->getArg(0)); auto *Arg1Loc = State.Env.get(*E->getArg(1)); transferSwap(Arg0Loc, Arg1Loc, State.Env); } void transferStdForwardCall(const CallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { assert(E->getNumArgs() == 1); if (auto *Loc = State.Env.getStorageLocation(*E->getArg(0))) State.Env.setStorageLocation(*E, *Loc); } const Formula &evaluateEquality(Arena &A, const Formula &EqVal, const Formula &LHS, const Formula &RHS) { // Logically, an optional object is composed of two values - a `has_value` // bit and a value of type T. Equality of optional objects compares both // values. Therefore, merely comparing the `has_value` bits isn't sufficient: // when two optional objects are engaged, the equality of their respective // values of type T matters. Since we only track the `has_value` bits, we // can't make any conclusions about equality when we know that two optional // objects are engaged. // // We express this as two facts about the equality: // a) EqVal => (LHS & RHS) v (!RHS & !LHS) // If they are equal, then either both are set or both are unset. // b) (!LHS & !RHS) => EqVal // If neither is set, then they are equal. // We rewrite b) as !EqVal => (LHS v RHS), for a more compact formula. return A.makeAnd( A.makeImplies(EqVal, A.makeOr(A.makeAnd(LHS, RHS), A.makeAnd(A.makeNot(LHS), A.makeNot(RHS)))), A.makeImplies(A.makeNot(EqVal), A.makeOr(LHS, RHS))); } void transferOptionalAndOptionalCmp(const clang::CXXOperatorCallExpr *CmpExpr, const MatchFinder::MatchResult &, LatticeTransferState &State) { Environment &Env = State.Env; auto &A = Env.arena(); auto *CmpValue = &forceBoolValue(Env, *CmpExpr); auto *Arg0Loc = Env.get(*CmpExpr->getArg(0)); if (auto *LHasVal = getHasValue(Env, Arg0Loc)) { auto *Arg1Loc = Env.get(*CmpExpr->getArg(1)); if (auto *RHasVal = getHasValue(Env, Arg1Loc)) { if (CmpExpr->getOperator() == clang::OO_ExclaimEqual) CmpValue = &A.makeNot(*CmpValue); Env.assume(evaluateEquality(A, *CmpValue, LHasVal->formula(), RHasVal->formula())); } } } void transferOptionalAndValueCmp(const clang::CXXOperatorCallExpr *CmpExpr, const clang::Expr *E, Environment &Env) { auto &A = Env.arena(); auto *CmpValue = &forceBoolValue(Env, *CmpExpr); auto *Loc = Env.get(*E); if (auto *HasVal = getHasValue(Env, Loc)) { if (CmpExpr->getOperator() == clang::OO_ExclaimEqual) CmpValue = &A.makeNot(*CmpValue); Env.assume( evaluateEquality(A, *CmpValue, HasVal->formula(), A.makeLiteral(true))); } } void transferOptionalAndNulloptCmp(const clang::CXXOperatorCallExpr *CmpExpr, const clang::Expr *E, Environment &Env) { auto &A = Env.arena(); auto *CmpValue = &forceBoolValue(Env, *CmpExpr); auto *Loc = Env.get(*E); if (auto *HasVal = getHasValue(Env, Loc)) { if (CmpExpr->getOperator() == clang::OO_ExclaimEqual) CmpValue = &A.makeNot(*CmpValue); Env.assume(evaluateEquality(A, *CmpValue, HasVal->formula(), A.makeLiteral(false))); } } std::optional ignorableOptional(const UncheckedOptionalAccessModelOptions &Options) { if (Options.IgnoreSmartPointerDereference) { auto SmartPtrUse = expr(ignoringParenImpCasts(cxxOperatorCallExpr( anyOf(hasOverloadedOperatorName("->"), hasOverloadedOperatorName("*")), unless(hasArgument(0, expr(hasOptionalType())))))); return expr( anyOf(SmartPtrUse, memberExpr(hasObjectExpression(SmartPtrUse)))); } return std::nullopt; } StatementMatcher valueCall(const std::optional &IgnorableOptional) { return isOptionalMemberCallWithNameMatcher(hasName("value"), IgnorableOptional); } StatementMatcher valueOperatorCall(const std::optional &IgnorableOptional) { return expr(anyOf(isOptionalOperatorCallWithName("*", IgnorableOptional), isOptionalOperatorCallWithName("->", IgnorableOptional))); } auto buildTransferMatchSwitch() { // FIXME: Evaluate the efficiency of matchers. If using matchers results in a // lot of duplicated work (e.g. string comparisons), consider providing APIs // that avoid it through memoization. return CFGMatchSwitchBuilder() // make_optional .CaseOfCFGStmt(isMakeOptionalCall(), transferMakeOptionalCall) // optional::optional (in place) .CaseOfCFGStmt( isOptionalInPlaceConstructor(), [](const CXXConstructExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { constructOptionalValue(*E, State.Env, State.Env.getBoolLiteralValue(true)); }) // optional::optional(nullopt_t) .CaseOfCFGStmt( isOptionalNulloptConstructor(), [](const CXXConstructExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { constructOptionalValue(*E, State.Env, State.Env.getBoolLiteralValue(false)); }) // optional::optional (value/conversion) .CaseOfCFGStmt(isOptionalValueOrConversionConstructor(), transferValueOrConversionConstructor) // optional::operator= .CaseOfCFGStmt( isOptionalValueOrConversionAssignment(), transferValueOrConversionAssignment) .CaseOfCFGStmt(isOptionalNulloptAssignment(), transferNulloptAssignment) // optional::value .CaseOfCFGStmt( valueCall(std::nullopt), [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferUnwrapCall(E, E->getImplicitObjectArgument(), State); }) // optional::operator* .CaseOfCFGStmt(isOptionalOperatorCallWithName("*"), [](const CallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferUnwrapCall(E, E->getArg(0), State); }) // optional::operator-> .CaseOfCFGStmt(isOptionalOperatorCallWithName("->"), [](const CallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferArrowOpCall(E, E->getArg(0), State); }) // optional::has_value, optional::hasValue // Of the supported optionals only folly::Optional uses hasValue, but this // will also pass for other types .CaseOfCFGStmt( isOptionalMemberCallWithNameMatcher( hasAnyName("has_value", "hasValue")), transferOptionalHasValueCall) // optional::operator bool .CaseOfCFGStmt( isOptionalMemberCallWithNameMatcher(hasName("operator bool")), transferOptionalHasValueCall) // optional::emplace .CaseOfCFGStmt( isOptionalMemberCallWithNameMatcher(hasName("emplace")), [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { if (RecordStorageLocation *Loc = getImplicitObjectLocation(*E, State.Env)) { setHasValue(*Loc, State.Env.getBoolLiteralValue(true), State.Env); } }) // optional::reset .CaseOfCFGStmt( isOptionalMemberCallWithNameMatcher(hasName("reset")), [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &, LatticeTransferState &State) { if (RecordStorageLocation *Loc = getImplicitObjectLocation(*E, State.Env)) { setHasValue(*Loc, State.Env.getBoolLiteralValue(false), State.Env); } }) // optional::swap .CaseOfCFGStmt( isOptionalMemberCallWithNameMatcher(hasName("swap")), transferSwapCall) // std::swap .CaseOfCFGStmt(isStdSwapCall(), transferStdSwapCall) // std::forward .CaseOfCFGStmt(isStdForwardCall(), transferStdForwardCall) // opt.value_or("").empty() .CaseOfCFGStmt(isValueOrStringEmptyCall(), transferValueOrStringEmptyCall) // opt.value_or(X) != X .CaseOfCFGStmt(isValueOrNotEqX(), transferValueOrNotEqX) // Comparisons (==, !=): .CaseOfCFGStmt( isComparisonOperatorCall(hasOptionalType(), hasOptionalType()), transferOptionalAndOptionalCmp) .CaseOfCFGStmt( isComparisonOperatorCall(hasOptionalType(), hasNulloptType()), [](const clang::CXXOperatorCallExpr *Cmp, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferOptionalAndNulloptCmp(Cmp, Cmp->getArg(0), State.Env); }) .CaseOfCFGStmt( isComparisonOperatorCall(hasNulloptType(), hasOptionalType()), [](const clang::CXXOperatorCallExpr *Cmp, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferOptionalAndNulloptCmp(Cmp, Cmp->getArg(1), State.Env); }) .CaseOfCFGStmt( isComparisonOperatorCall( hasOptionalType(), unless(anyOf(hasOptionalType(), hasNulloptType()))), [](const clang::CXXOperatorCallExpr *Cmp, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferOptionalAndValueCmp(Cmp, Cmp->getArg(0), State.Env); }) .CaseOfCFGStmt( isComparisonOperatorCall( unless(anyOf(hasOptionalType(), hasNulloptType())), hasOptionalType()), [](const clang::CXXOperatorCallExpr *Cmp, const MatchFinder::MatchResult &, LatticeTransferState &State) { transferOptionalAndValueCmp(Cmp, Cmp->getArg(1), State.Env); }) // returns optional .CaseOfCFGStmt(isCallReturningOptional(), transferCallReturningOptional) .Build(); } llvm::SmallVector diagnoseUnwrapCall(const Expr *ObjectExpr, const Environment &Env) { if (auto *OptionalLoc = cast_or_null( getLocBehindPossiblePointer(*ObjectExpr, Env))) { auto *Prop = Env.getValue(locForHasValue(*OptionalLoc)); if (auto *HasValueVal = cast_or_null(Prop)) { if (Env.proves(HasValueVal->formula())) return {}; } } // Record that this unwrap is *not* provably safe. // FIXME: include either the name of the optional (if applicable) or a source // range of the access for easier interpretation of the result. return {ObjectExpr->getBeginLoc()}; } auto buildDiagnoseMatchSwitch( const UncheckedOptionalAccessModelOptions &Options) { // FIXME: Evaluate the efficiency of matchers. If using matchers results in a // lot of duplicated work (e.g. string comparisons), consider providing APIs // that avoid it through memoization. auto IgnorableOptional = ignorableOptional(Options); return CFGMatchSwitchBuilder>() // optional::value .CaseOfCFGStmt( valueCall(IgnorableOptional), [](const CXXMemberCallExpr *E, const MatchFinder::MatchResult &, const Environment &Env) { return diagnoseUnwrapCall(E->getImplicitObjectArgument(), Env); }) // optional::operator*, optional::operator-> .CaseOfCFGStmt(valueOperatorCall(IgnorableOptional), [](const CallExpr *E, const MatchFinder::MatchResult &, const Environment &Env) { return diagnoseUnwrapCall(E->getArg(0), Env); }) .Build(); } } // namespace ast_matchers::DeclarationMatcher UncheckedOptionalAccessModel::optionalClassDecl() { return cxxRecordDecl(optionalClass()); } UncheckedOptionalAccessModel::UncheckedOptionalAccessModel(ASTContext &Ctx, Environment &Env) : DataflowAnalysis(Ctx), TransferMatchSwitch(buildTransferMatchSwitch()) { Env.getDataflowAnalysisContext().setSyntheticFieldCallback( [&Ctx](QualType Ty) -> llvm::StringMap { const CXXRecordDecl *Optional = getOptionalBaseClass(Ty->getAsCXXRecordDecl()); if (Optional == nullptr) return {}; return {{"value", valueTypeFromOptionalDecl(*Optional)}, {"has_value", Ctx.BoolTy}}; }); } void UncheckedOptionalAccessModel::transfer(const CFGElement &Elt, NoopLattice &L, Environment &Env) { LatticeTransferState State(L, Env); TransferMatchSwitch(Elt, getASTContext(), State); } UncheckedOptionalAccessDiagnoser::UncheckedOptionalAccessDiagnoser( UncheckedOptionalAccessModelOptions Options) : DiagnoseMatchSwitch(buildDiagnoseMatchSwitch(Options)) {} } // namespace dataflow } // namespace clang