https://github.com/spaits created https://github.com/llvm/llvm-project/pull/76580
Add a checker to detect bad `std::any` type accesses. It warns, when the active type is different from the requested type when calling `std::any_cast`: ```cpp void anyCast() { std::any a = 5; char c = std::any_cast<char>(a); // // warn: "int" is the active alternative } ``` It also warns, when the `std::any` instance is empty: ```cpp void noTypeHeld() { std::any a; int i = std::any_cast<int>(a); // warn: any 'a' is empty } ``` This checker works in a similar way to `std::variant` checker: Recognizes when the active type of an `std::any` instance changes and stores this information. Later on, when the data hold in this instance is accessed with `std::any_cast` it is checked if the active type is retrieved or not. If not then a warning is emitted. This checker does not produce false positives. It is conservative: if there is no information of the `std::any` instance or the already recorded information becomes invalid no report will be emitted. From 98b52eb390438402562de96a74771b0132fc71cb Mon Sep 17 00:00:00 2001 From: Gabor Spaits <gaborspai...@gmail.com> Date: Fri, 29 Dec 2023 17:54:34 +0100 Subject: [PATCH] [analyzer] Add std::any checker --- clang/docs/analyzer/checkers.rst | 21 ++ .../clang/StaticAnalyzer/Checkers/Checkers.td | 4 + .../StaticAnalyzer/Checkers/CMakeLists.txt | 1 + .../StaticAnalyzer/Checkers/StdAnyChecker.cpp | 201 ++++++++++++++++++ .../Checkers/StdVariantChecker.cpp | 46 ++-- .../Checkers/TaggedUnionModeling.h | 9 +- .../Inputs/system-header-simulator-cxx.h | 59 +++++ clang/test/Analysis/std-any-checker.cpp | 170 +++++++++++++++ clang/test/Analysis/std-variant-checker.cpp | 8 + 9 files changed, 490 insertions(+), 29 deletions(-) create mode 100644 clang/lib/StaticAnalyzer/Checkers/StdAnyChecker.cpp create mode 100644 clang/test/Analysis/std-any-checker.cpp diff --git a/clang/docs/analyzer/checkers.rst b/clang/docs/analyzer/checkers.rst index bb637cf1b8007b..867bdfc9012253 100644 --- a/clang/docs/analyzer/checkers.rst +++ b/clang/docs/analyzer/checkers.rst @@ -2095,6 +2095,27 @@ This checker is a part of ``core.StackAddressEscape``, but is temporarily disabl // returned block } +.. _alpha-core-StdAny: + +alpha.core.StdAny (C++) +""""""""""""""""""""""" +Check if a value of active type is retrieved from an ``std::any`` instance with ``std::any_cats`` +or if the ``std::any`` instance holds type. In case of bad any type access +(the accessed type differs from the active type, or the instance has no stored value) +a warning is emitted. Currently, this checker does not take exception handling into account. + +.. code-block:: cpp + + void anyCast() { + std::any a = 5; + char c = std::any_cast<char>(a); // // warn: "int" is the active alternative + } + + void noTypeHeld() { + std::any a; + int i = std::any_cast<int>(a); // warn: any 'a' is empty + } + .. _alpha-core-StdVariant: alpha.core.StdVariant (C++) diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td index e7774e5a9392d2..954584fadb225d 100644 --- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td +++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td @@ -321,6 +321,10 @@ def C11LockChecker : Checker<"C11Lock">, Dependencies<[PthreadLockBase]>, Documentation<HasDocumentation>; +def StdAnyChecker : Checker<"StdAny">, + HelpText<"Check for bad type access for std::any.">, + Documentation<HasDocumentation>; + def StdVariantChecker : Checker<"StdVariant">, HelpText<"Check for bad type access for std::variant.">, Documentation<HasDocumentation>; diff --git a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt index 4443ffd0929388..4e3475ff7f37da 100644 --- a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt +++ b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt @@ -107,6 +107,7 @@ add_clang_library(clangStaticAnalyzerCheckers SmartPtrChecker.cpp SmartPtrModeling.cpp StackAddrEscapeChecker.cpp + StdAnyChecker.cpp StdLibraryFunctionsChecker.cpp StdVariantChecker.cpp STLAlgorithmModeling.cpp diff --git a/clang/lib/StaticAnalyzer/Checkers/StdAnyChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StdAnyChecker.cpp new file mode 100644 index 00000000000000..647ee72e555140 --- /dev/null +++ b/clang/lib/StaticAnalyzer/Checkers/StdAnyChecker.cpp @@ -0,0 +1,201 @@ +//===- StdAnyChecker.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 +// +//===----------------------------------------------------------------------===// + +#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" +#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" +#include "clang/StaticAnalyzer/Core/Checker.h" +#include "clang/StaticAnalyzer/Core/CheckerManager.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" +#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" +#include "llvm/Support/ErrorHandling.h" + +#include "TaggedUnionModeling.h" + +using namespace clang; +using namespace ento; +using namespace tagged_union_modeling; + +REGISTER_MAP_WITH_PROGRAMSTATE(AnyHeldTypeMap, const MemRegion *, QualType) + +class StdAnyChecker : public Checker<eval::Call, check::RegionChanges> { + CallDescription AnyConstructor{{"std", "any", "any"}}; + CallDescription AnyAsOp{{"std", "any", "operator="}}; + CallDescription AnyReset{{"std", "any", "reset"}, 0, 0}; + CallDescription AnyCast{{"std", "any_cast"}, 1, 1}; + + BugType BadAnyType{this, "BadAnyType", "BadAnyType"}; + BugType NullAnyType{this, "NullAnyType", "NullAnyType"}; + +public: + ProgramStateRef + checkRegionChanges(ProgramStateRef State, + const InvalidatedSymbols *Invalidated, + ArrayRef<const MemRegion *> ExplicitRegions, + ArrayRef<const MemRegion *> Regions, + const LocationContext *LCtx, const CallEvent *Call) const { + if (!Call) + return State; + + return removeInformationStoredForDeadInstances<AnyHeldTypeMap>(*Call, State, + Regions); + } + + bool evalCall(const CallEvent &Call, CheckerContext &C) const { + // Do not take implementation details into consideration + if (Call.isCalledFromSystemHeader()) + return false; + + if (AnyCast.matches(Call)) + return handleAnyCastCall(Call, C); + + if (AnyReset.matches(Call)) { + const auto *AsMemberCall = dyn_cast<CXXMemberCall>(&Call); + if (!AsMemberCall) + return false; + + const auto *ThisMemRegion = AsMemberCall->getCXXThisVal().getAsRegion(); + if (!ThisMemRegion) + return false; + + setNullTypeAny(ThisMemRegion, C); + return true; + } + + bool IsAnyConstructor = + isa<CXXConstructorCall>(Call) && AnyConstructor.matches(Call); + bool IsAnyAssignmentOperatorCall = + isa<CXXMemberOperatorCall>(Call) && AnyAsOp.matches(Call); + + if (IsAnyConstructor || IsAnyAssignmentOperatorCall) { + auto State = C.getState(); + SVal ThisSVal = [&]() { + if (IsAnyConstructor) { + const auto *AsConstructorCall = dyn_cast<CXXConstructorCall>(&Call); + return AsConstructorCall->getCXXThisVal(); + } + if (IsAnyAssignmentOperatorCall) { + const auto *AsMemberOpCall = dyn_cast<CXXMemberOperatorCall>(&Call); + return AsMemberOpCall->getCXXThisVal(); + } + llvm_unreachable("We must have an assignment operator or constructor"); + }(); + + // default constructor call + // in this case the any holds a null type + if (Call.getNumArgs() == 0) { + const auto *ThisMemRegion = ThisSVal.getAsRegion(); + setNullTypeAny(ThisMemRegion, C); + return true; + } + + if (Call.getNumArgs() != 1) + return false; + + handleConstructorAndAssignment<AnyHeldTypeMap>(Call, C, ThisSVal); + return true; + } + return false; + } + +private: + // When an std::any is rested or default constructed it has a null type. + // We represent it by storing a null QualType. + void setNullTypeAny(const MemRegion *Mem, CheckerContext &C) const { + auto State = C.getState(); + State = State->set<AnyHeldTypeMap>(Mem, QualType{}); + C.addTransition(State); + } + + // this function name is terrible + bool handleAnyCastCall(const CallEvent &Call, CheckerContext &C) const { + auto State = C.getState(); + + if (Call.getNumArgs() != 1) + return false; + + auto ArgSVal = Call.getArgSVal(0); + + // The argument is aether a const reference or a right value reference + // We need the type referred + const auto *ArgType = ArgSVal.getType(C.getASTContext()) + .getTypePtr() + ->getPointeeType() + .getTypePtr(); + if (!isStdAny(ArgType)) + return false; + + const auto *AnyMemRegion = ArgSVal.getAsRegion(); + + if (!State->contains<AnyHeldTypeMap>(AnyMemRegion)) + return false; + + // get the type we are trying to get from any + const CallExpr *CE = cast<CallExpr>(Call.getOriginExpr()); + const FunctionDecl *FD = CE->getDirectCallee(); + if (FD->getTemplateSpecializationArgs()->size() < 1) + return false; + + const auto &FirstTemplateArgument = + FD->getTemplateSpecializationArgs()->asArray()[0]; + if (FirstTemplateArgument.getKind() != TemplateArgument::ArgKind::Type) + return false; + + auto TypeOut = FirstTemplateArgument.getAsType(); + const auto *TypeStored = State->get<AnyHeldTypeMap>(AnyMemRegion); + + // Report when we try to use std::any_cast on an std::any that held a null + // type + if (TypeStored->isNull()) { + ExplodedNode *ErrNode = C.generateNonFatalErrorNode(); + if (!ErrNode) + return false; + llvm::SmallString<128> Str; + llvm::raw_svector_ostream OS(Str); + OS << "std::any " << AnyMemRegion->getDescriptiveName() << " is empty."; + auto R = std::make_unique<PathSensitiveBugReport>(NullAnyType, OS.str(), + ErrNode); + C.emitReport(std::move(R)); + return true; + } + + // Check if the right type is being accessed + // There is spacial case for object types. + QualType RetrievedCanonicalType = TypeOut.getCanonicalType(); + QualType StoredCanonicalType = TypeStored->getCanonicalType(); + if (RetrievedCanonicalType == StoredCanonicalType) + return true; + + // Report when the type we want to get out of std::any is wrong + ExplodedNode *ErrNode = C.generateNonFatalErrorNode(); + if (!ErrNode) + return false; + llvm::SmallString<128> Str; + llvm::raw_svector_ostream OS(Str); + std::string StoredTypeName = StoredCanonicalType.getAsString(); + std::string RetrievedTypeName = RetrievedCanonicalType.getAsString(); + OS << "std::any " << AnyMemRegion->getDescriptiveName() << " held " + << indefiniteArticleBasedOnVowel(StoredTypeName[0]) << " \'" + << StoredTypeName << "\', not " + << indefiniteArticleBasedOnVowel(RetrievedTypeName[0]) << " \'" + << RetrievedTypeName << "\'"; + auto R = + std::make_unique<PathSensitiveBugReport>(BadAnyType, OS.str(), ErrNode); + C.emitReport(std::move(R)); + return true; + } +}; + +bool clang::ento::shouldRegisterStdAnyChecker( + clang::ento::CheckerManager const &mgr) { + return true; +} + +void clang::ento::registerStdAnyChecker(clang::ento::CheckerManager &mgr) { + mgr.registerChecker<StdAnyChecker>(); +} \ No newline at end of file diff --git a/clang/lib/StaticAnalyzer/Checkers/StdVariantChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StdVariantChecker.cpp index f7b7befe28ee7d..62ca9f0ae836e0 100644 --- a/clang/lib/StaticAnalyzer/Checkers/StdVariantChecker.cpp +++ b/clang/lib/StaticAnalyzer/Checkers/StdVariantChecker.cpp @@ -17,9 +17,7 @@ #include "clang/StaticAnalyzer/Core/PathSensitive/SVals.h" #include "llvm/ADT/FoldingSet.h" #include "llvm/ADT/StringRef.h" -#include "llvm/Support/Casting.h" #include <optional> -#include <string_view> #include "TaggedUnionModeling.h" @@ -87,6 +85,28 @@ bool isStdVariant(const Type *Type) { return isStdType(Type, llvm::StringLiteral("variant")); } +bool isStdAny(const Type *Type) { + return isStdType(Type, llvm::StringLiteral("any")); +} + +bool isVowel(char a) { + switch (a) { + case 'a': + case 'e': + case 'i': + case 'o': + case 'u': + return true; + default: + return false; + } +} + +llvm::StringRef indefiniteArticleBasedOnVowel(char a) { + if (isVowel(a)) + return "an"; + return "a"; +} } // end of namespace clang::ento::tagged_union_modeling static std::optional<ArrayRef<TemplateArgument>> @@ -108,25 +128,6 @@ getNthTemplateTypeArgFromVariant(const Type *varType, unsigned i) { return (*VariantTemplates)[i].getAsType(); } -static bool isVowel(char a) { - switch (a) { - case 'a': - case 'e': - case 'i': - case 'o': - case 'u': - return true; - default: - return false; - } -} - -static llvm::StringRef indefiniteArticleBasedOnVowel(char a) { - if (isVowel(a)) - return "an"; - return "a"; -} - class StdVariantChecker : public Checker<eval::Call, check::RegionChanges> { // Call descriptors to find relevant calls CallDescription VariantConstructor{{"std", "variant", "variant"}}; @@ -184,9 +185,8 @@ class StdVariantChecker : public Checker<eval::Call, check::RegionChanges> { } else if (IsVariantAssignmentOperatorCall) { const auto &AsMemberOpCall = cast<CXXMemberOperatorCall>(Call); ThisSVal = AsMemberOpCall.getCXXThisVal(); - } else { + } else return false; - } handleConstructorAndAssignment<VariantHeldTypeMap>(Call, C, ThisSVal); return true; diff --git a/clang/lib/StaticAnalyzer/Checkers/TaggedUnionModeling.h b/clang/lib/StaticAnalyzer/Checkers/TaggedUnionModeling.h index 557e8a76506e61..3b376777ae16c2 100644 --- a/clang/lib/StaticAnalyzer/Checkers/TaggedUnionModeling.h +++ b/clang/lib/StaticAnalyzer/Checkers/TaggedUnionModeling.h @@ -9,15 +9,9 @@ #ifndef LLVM_CLANG_LIB_STATICANALYZER_CHECKERS_TAGGEDUNIONMODELING_H #define LLVM_CLANG_LIB_STATICANALYZER_CHECKERS_TAGGEDUNIONMODELING_H -#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h" -#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h" -#include "clang/StaticAnalyzer/Core/Checker.h" #include "clang/StaticAnalyzer/Core/CheckerManager.h" -#include "clang/StaticAnalyzer/Core/PathSensitive/CallDescription.h" #include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h" #include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h" -#include "llvm/ADT/FoldingSet.h" -#include <numeric> namespace clang::ento::tagged_union_modeling { @@ -30,6 +24,9 @@ bool isMoveAssignmentCall(const CallEvent &Call); bool isMoveConstructorCall(const CallEvent &Call); bool isStdType(const Type *Type, const std::string &TypeName); bool isStdVariant(const Type *Type); +bool isStdAny(const Type *Type); +bool isVowel(char a); +llvm::StringRef indefiniteArticleBasedOnVowel(char a); // When invalidating regions, we also have to follow that by invalidating the // corresponding custom data in the program state. diff --git a/clang/test/Analysis/Inputs/system-header-simulator-cxx.h b/clang/test/Analysis/Inputs/system-header-simulator-cxx.h index 3ef7af2ea6c6ab..e1ad590526eb38 100644 --- a/clang/test/Analysis/Inputs/system-header-simulator-cxx.h +++ b/clang/test/Analysis/Inputs/system-header-simulator-cxx.h @@ -1372,6 +1372,65 @@ template <typename Ret, typename... Args> class packaged_task<Ret(Args...)> { typename = std::enable_if_t<!is_same_v<std::variant<Types...>, decay_t<T>>>> variant& operator=(T&&); }; + + class bad_any_cast; + + // class any + class any; + + // non-member functions + void swap(any& x, any& y) noexcept; + + template<class T, class... Args> + any make_any(Args&&... args); + template<class T, class U, class... Args> + any make_any(initializer_list<U> il, Args&&... args); + + template<class T> + T any_cast(const any& operand); + template<class T> + T any_cast(any& operand); + template<class T> + T any_cast(any&& operand); + + template<class T> + const T* any_cast(const any* operand) noexcept; + template<class T> + T* any_cast(any* operand) noexcept; + + class any { + public: + // construction and destruction + constexpr any() noexcept; + + any(const any& other); + any(any&& other) noexcept; + + template<class T> + any(T&& value); + + ~any(); + + // assignments + any& operator=(const any& rhs); + any& operator=(any&& rhs) noexcept; + + template<typename T, + typename = std::enable_if_t<!is_same_v<std::any, decay_t<T>>>> + any& operator=(T&& rhs); + + // modifiers + template<class T, class... Args> + decay_t<T>& emplace(Args&&...); + template<class T, class U, class... Args> + decay_t<T>& emplace(initializer_list<U>, Args&&...); + void reset() noexcept; + void swap(any& rhs) noexcept; + + // observers + bool has_value() const noexcept; + }; + #endif } // namespace std diff --git a/clang/test/Analysis/std-any-checker.cpp b/clang/test/Analysis/std-any-checker.cpp new file mode 100644 index 00000000000000..d28d311b32d843 --- /dev/null +++ b/clang/test/Analysis/std-any-checker.cpp @@ -0,0 +1,170 @@ +// RUN: %clang %s -Xclang -verify --analyze \ +// RUN: -Xclang -analyzer-checker=core \ +// RUN: -Xclang -analyzer-checker=debug.ExprInspection \ +// RUN: -Xclang -analyzer-checker=core,alpha.core.StdAny + +#include "Inputs/system-header-simulator-cxx.h" + +void clang_analyzer_warnIfReached(); +void clang_analyzer_eval(int); + + +class DummyClass{ + public: + void foo(){}; +}; + +void nonInlined(std::any &a); +void nonInlinedConst(const std::any & a); + +void inlined(std::any &a) { + a = 5; +} + +using any_t = std::any; +using any_tt = any_t; + + +//----------------------------------------------------------------------------// +// std::any_cast +//----------------------------------------------------------------------------// +void objectHeld() { + std::any a = DummyClass{}; + DummyClass d = std::any_cast<DummyClass>(a); + d.foo(); +} + +void formVariable() { + std::any a = 5; + int b = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)b; + (void)c; +} + +void pointerHeld() { + int i = 5; + std::any a = &i; + int* x = std::any_cast<int*>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int *', not a 'char'}} + (void)x; + (void)c; +} + +//----------------------------------------------------------------------------// +// Empty std::any +//----------------------------------------------------------------------------// + +void noTypeHeld() { + std::any a; + int i = std::any_cast<int>(a); // expected-warning {{any 'a' is empty}} + (void)i; +} + +void reset() { + std::any a = 15; + a.reset(); + int i = std::any_cast<int>(a); // expected-warning {{any 'a' is empty}} + (void)i; +} + + +//----------------------------------------------------------------------------// +// Typedefs +//----------------------------------------------------------------------------// + +void typedefedAny () { + any_t a = 5; + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)i; + (void)c; +} + +void typedefedTypedefedAny () { + any_tt a = 5; + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)i; + (void)c; +} + +//----------------------------------------------------------------------------// +// Constructors and assignments +//----------------------------------------------------------------------------// + +void assignmentOp () { + std::any a; + a = 5; + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)i; + (void)c; +} + +void constructor() { + std::any a(5); + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)i; + (void)c; +} + +void copyCtor() { + std::any a(5); + std::any b(a); + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)i; + (void)c; +} + +void copyCtorNullType() { + std::any a; + std::any b(a); + char c = std::any_cast<char>(a); // expected-warning {{any 'a' is empty}} + (void)c; +} + +void copyAssignment() { + std::any a = 5; + std::any b = 'c'; + char c = std::any_cast<char>(b); + (void)c; + b = a; + int i = std::any_cast<int>(b); + c = std::any_cast<char>(b); // expected-warning {{std::any 'b' held an 'int', not a 'char'}} + (void)i; + (void)c; +} + +//----------------------------------------------------------------------------// +// Function calls +//----------------------------------------------------------------------------// + +void nonInlinedRefCall() { + std::any a = 5; + nonInlined(a); + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); + (void)i; + (void)c; +} + +void nonInlinedConstRefCall() { + std::any a = 5; + nonInlinedConst(a); + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)i; + (void)c; +} + +void inlinedCall() { + std::any a = 'c'; + inlined(a); + int i = std::any_cast<int>(a); + char c = std::any_cast<char>(a); // expected-warning {{std::any 'a' held an 'int', not a 'char'}} + (void)i; + (void)c; +} \ No newline at end of file diff --git a/clang/test/Analysis/std-variant-checker.cpp b/clang/test/Analysis/std-variant-checker.cpp index 7f136c06b19cc6..bdae26742826af 100644 --- a/clang/test/Analysis/std-variant-checker.cpp +++ b/clang/test/Analysis/std-variant-checker.cpp @@ -58,6 +58,14 @@ void wontConfuseStdGets() { //----------------------------------------------------------------------------// // std::get //----------------------------------------------------------------------------// +void stdGetType2() { + std::variant<int, char> v = {25}; + int a = std::get<int>(v); + char c = std::get<char>(v); // expected-warning {{std::variant 'v' held an 'int', not a 'char'}} + (void)a; + (void)c; +} + void stdGetType() { std::variant<int, char> v = 25; int a = std::get<int>(v); _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits