296 lines · cpp
1//==- GTestChecker.cpp - Model gtest API --*- C++ -*-==//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8//9// This checker models the behavior of un-inlined APIs from the gtest10// unit-testing library to avoid false positives when using assertions from11// that library.12//13//===----------------------------------------------------------------------===//14 15#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"16#include "clang/AST/Expr.h"17#include "clang/Basic/LangOptions.h"18#include "clang/StaticAnalyzer/Core/Checker.h"19#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"20#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"21#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"22#include "llvm/Support/raw_ostream.h"23#include <optional>24 25using namespace clang;26using namespace ento;27 28// Modeling of un-inlined AssertionResult constructors29//30// The gtest unit testing API provides macros for assertions that expand31// into an if statement that calls a series of constructors and returns32// when the "assertion" is false.33//34// For example,35//36// ASSERT_TRUE(a == b)37//38// expands into:39//40// switch (0)41// case 0:42// default:43// if (const ::testing::AssertionResult gtest_ar_ =44// ::testing::AssertionResult((a == b)))45// ;46// else47// return ::testing::internal::AssertHelper(48// ::testing::TestPartResult::kFatalFailure,49// "<path to project>",50// <line number>,51// ::testing::internal::GetBoolAssertionFailureMessage(52// gtest_ar_, "a == b", "false", "true")53// .c_str()) = ::testing::Message();54//55// where AssertionResult is defined similarly to56//57// class AssertionResult {58// public:59// AssertionResult(const AssertionResult& other);60// explicit AssertionResult(bool success) : success_(success) {}61// operator bool() const { return success_; }62// ...63// private:64// bool success_;65// };66//67// In order for the analyzer to correctly handle this assertion, it needs to68// know that the boolean value of the expression "a == b" is stored the69// 'success_' field of the original AssertionResult temporary and propagated70// (via the copy constructor) into the 'success_' field of the object stored71// in 'gtest_ar_'. That boolean value will then be returned from the bool72// conversion method in the if statement. This guarantees that the assertion73// holds when the return path is not taken.74//75// If the success value is not properly propagated, then the eager case split76// on evaluating the expression can cause pernicious false positives77// on the non-return path:78//79// ASSERT(ptr != NULL)80// *ptr = 7; // False positive null pointer dereference here81//82// Unfortunately, the bool constructor cannot be inlined (because its83// implementation is not present in the headers) and the copy constructor is84// not inlined (because it is constructed into a temporary and the analyzer85// does not inline these since it does not yet reliably call temporary86// destructors).87//88// This checker compensates for the missing inlining by propagating the89// _success value across the bool and copy constructors so the assertion behaves90// as expected.91 92namespace {93class GTestChecker : public Checker<check::PostCall> {94 95 mutable IdentifierInfo *AssertionResultII = nullptr;96 mutable IdentifierInfo *SuccessII = nullptr;97 98public:99 GTestChecker() = default;100 101 void checkPostCall(const CallEvent &Call, CheckerContext &C) const;102 103private:104 void modelAssertionResultBoolConstructor(const CXXConstructorCall *Call,105 bool IsRef, CheckerContext &C) const;106 107 void modelAssertionResultCopyConstructor(const CXXConstructorCall *Call,108 CheckerContext &C) const;109 110 void initIdentifierInfo(ASTContext &Ctx) const;111 112 SVal113 getAssertionResultSuccessFieldValue(const CXXRecordDecl *AssertionResultDecl,114 SVal Instance,115 ProgramStateRef State) const;116 117 static ProgramStateRef assumeValuesEqual(SVal Val1, SVal Val2,118 ProgramStateRef State,119 CheckerContext &C);120};121} // End anonymous namespace.122 123/// Model a call to an un-inlined AssertionResult(bool) or124/// AssertionResult(bool &, ...).125/// To do so, constrain the value of the newly-constructed instance's 'success_'126/// field to be equal to the passed-in boolean value.127///128/// \param IsRef Whether the boolean parameter is a reference or not.129void GTestChecker::modelAssertionResultBoolConstructor(130 const CXXConstructorCall *Call, bool IsRef, CheckerContext &C) const {131 assert(Call->getNumArgs() >= 1 && Call->getNumArgs() <= 2);132 133 ProgramStateRef State = C.getState();134 SVal BooleanArgVal = Call->getArgSVal(0);135 if (IsRef) {136 // The argument is a reference, so load from it to get the boolean value.137 if (!isa<Loc>(BooleanArgVal))138 return;139 BooleanArgVal = C.getState()->getSVal(BooleanArgVal.castAs<Loc>());140 }141 142 SVal ThisVal = Call->getCXXThisVal();143 144 SVal ThisSuccess = getAssertionResultSuccessFieldValue(145 Call->getDecl()->getParent(), ThisVal, State);146 147 State = assumeValuesEqual(ThisSuccess, BooleanArgVal, State, C);148 C.addTransition(State);149}150 151/// Model a call to an un-inlined AssertionResult copy constructor:152///153/// AssertionResult(const &AssertionResult other)154///155/// To do so, constrain the value of the newly-constructed instance's156/// 'success_' field to be equal to the value of the pass-in instance's157/// 'success_' field.158void GTestChecker::modelAssertionResultCopyConstructor(159 const CXXConstructorCall *Call, CheckerContext &C) const {160 assert(Call->getNumArgs() == 1);161 162 // The first parameter of the copy constructor must be the other163 // instance to initialize this instances fields from.164 SVal OtherVal = Call->getArgSVal(0);165 SVal ThisVal = Call->getCXXThisVal();166 167 const CXXRecordDecl *AssertResultClassDecl = Call->getDecl()->getParent();168 ProgramStateRef State = C.getState();169 170 SVal ThisSuccess = getAssertionResultSuccessFieldValue(AssertResultClassDecl,171 ThisVal, State);172 SVal OtherSuccess = getAssertionResultSuccessFieldValue(AssertResultClassDecl,173 OtherVal, State);174 175 State = assumeValuesEqual(ThisSuccess, OtherSuccess, State, C);176 C.addTransition(State);177}178 179/// Model calls to AssertionResult constructors that are not inlined.180void GTestChecker::checkPostCall(const CallEvent &Call,181 CheckerContext &C) const {182 /// If the constructor was inlined, there is no need model it.183 if (C.wasInlined)184 return;185 186 initIdentifierInfo(C.getASTContext());187 188 auto *CtorCall = dyn_cast<CXXConstructorCall>(&Call);189 if (!CtorCall)190 return;191 192 const CXXConstructorDecl *CtorDecl = CtorCall->getDecl();193 const CXXRecordDecl *CtorParent = CtorDecl->getParent();194 if (CtorParent->getIdentifier() != AssertionResultII)195 return;196 197 unsigned ParamCount = CtorDecl->getNumParams();198 199 // Call the appropriate modeling method based the parameters and their200 // types.201 202 // We have AssertionResult(const &AssertionResult)203 if (CtorDecl->isCopyConstructor() && ParamCount == 1) {204 modelAssertionResultCopyConstructor(CtorCall, C);205 return;206 }207 208 // There are two possible boolean constructors, depending on which209 // version of gtest is being used:210 //211 // v1.7 and earlier:212 // AssertionResult(bool success)213 //214 // v1.8 and greater:215 // template <typename T>216 // AssertionResult(const T& success,217 // typename internal::EnableIf<218 // !internal::ImplicitlyConvertible<T,219 // AssertionResult>::value>::type*)220 //221 CanQualType BoolTy = C.getASTContext().BoolTy;222 if (ParamCount == 1 && CtorDecl->getParamDecl(0)->getType() == BoolTy) {223 // We have AssertionResult(bool)224 modelAssertionResultBoolConstructor(CtorCall, /*IsRef=*/false, C);225 return;226 }227 if (ParamCount == 2){228 auto *RefTy = CtorDecl->getParamDecl(0)->getType()->getAs<ReferenceType>();229 if (RefTy &&230 RefTy->getPointeeType()->getCanonicalTypeUnqualified() == BoolTy) {231 // We have AssertionResult(bool &, ...)232 modelAssertionResultBoolConstructor(CtorCall, /*IsRef=*/true, C);233 return;234 }235 }236}237 238void GTestChecker::initIdentifierInfo(ASTContext &Ctx) const {239 if (AssertionResultII)240 return;241 242 AssertionResultII = &Ctx.Idents.get("AssertionResult");243 SuccessII = &Ctx.Idents.get("success_");244}245 246/// Returns the value stored in the 'success_' field of the passed-in247/// AssertionResult instance.248SVal GTestChecker::getAssertionResultSuccessFieldValue(249 const CXXRecordDecl *AssertionResultDecl, SVal Instance,250 ProgramStateRef State) const {251 252 DeclContext::lookup_result Result = AssertionResultDecl->lookup(SuccessII);253 if (Result.empty())254 return UnknownVal();255 256 auto *SuccessField = dyn_cast<FieldDecl>(Result.front());257 if (!SuccessField)258 return UnknownVal();259 260 std::optional<Loc> FieldLoc =261 State->getLValue(SuccessField, Instance).getAs<Loc>();262 if (!FieldLoc)263 return UnknownVal();264 265 return State->getSVal(*FieldLoc);266}267 268/// Constrain the passed-in state to assume two values are equal.269ProgramStateRef GTestChecker::assumeValuesEqual(SVal Val1, SVal Val2,270 ProgramStateRef State,271 CheckerContext &C) {272 auto DVal1 = Val1.getAs<DefinedOrUnknownSVal>();273 auto DVal2 = Val2.getAs<DefinedOrUnknownSVal>();274 if (!DVal1 || !DVal2)275 return State;276 277 auto ValuesEqual =278 C.getSValBuilder().evalEQ(State, *DVal1, *DVal2).getAs<DefinedSVal>();279 if (!ValuesEqual)280 return State;281 282 State = C.getConstraintManager().assume(State, *ValuesEqual, true);283 return State;284}285 286void ento::registerGTestChecker(CheckerManager &Mgr) {287 Mgr.registerChecker<GTestChecker>();288}289 290bool ento::shouldRegisterGTestChecker(const CheckerManager &mgr) {291 // gtest is a C++ API so there is no sense running the checker292 // if not compiling for C++.293 const LangOptions &LO = mgr.getLangOpts();294 return LO.CPlusPlus;295}296