355 lines
14 KiB
C++
355 lines
14 KiB
C++
//===- NumberObjectConversionChecker.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 NumberObjectConversionChecker, which checks for a
|
|
// particular common mistake when dealing with numbers represented as objects
|
|
// passed around by pointers. Namely, the language allows to reinterpret the
|
|
// pointer as a number directly, often without throwing any warnings,
|
|
// but in most cases the result of such conversion is clearly unexpected,
|
|
// as pointer value, rather than number value represented by the pointee object,
|
|
// becomes the result of such operation.
|
|
//
|
|
// Currently the checker supports the Objective-C NSNumber class,
|
|
// and the OSBoolean class found in macOS low-level code; the latter
|
|
// can only hold boolean values.
|
|
//
|
|
// This checker has an option "Pedantic" (boolean), which enables detection of
|
|
// more conversion patterns (which are most likely more harmless, and therefore
|
|
// are more likely to produce false positives) - disabled by default,
|
|
// enabled with `-analyzer-config osx.NumberObjectConversion:Pedantic=true'.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "clang/StaticAnalyzer/Checkers/BuiltinCheckerRegistration.h"
|
|
#include "clang/ASTMatchers/ASTMatchFinder.h"
|
|
#include "clang/StaticAnalyzer/Core/BugReporter/BugReporter.h"
|
|
#include "clang/StaticAnalyzer/Core/BugReporter/BugType.h"
|
|
#include "clang/StaticAnalyzer/Core/Checker.h"
|
|
#include "clang/StaticAnalyzer/Core/PathSensitive/AnalysisManager.h"
|
|
#include "clang/Lex/Lexer.h"
|
|
#include "llvm/ADT/APSInt.h"
|
|
|
|
using namespace clang;
|
|
using namespace ento;
|
|
using namespace ast_matchers;
|
|
|
|
namespace {
|
|
|
|
class NumberObjectConversionChecker : public Checker<check::ASTCodeBody> {
|
|
public:
|
|
bool Pedantic;
|
|
|
|
void checkASTCodeBody(const Decl *D, AnalysisManager &AM,
|
|
BugReporter &BR) const;
|
|
};
|
|
|
|
class Callback : public MatchFinder::MatchCallback {
|
|
const NumberObjectConversionChecker *C;
|
|
BugReporter &BR;
|
|
AnalysisDeclContext *ADC;
|
|
|
|
public:
|
|
Callback(const NumberObjectConversionChecker *C,
|
|
BugReporter &BR, AnalysisDeclContext *ADC)
|
|
: C(C), BR(BR), ADC(ADC) {}
|
|
void run(const MatchFinder::MatchResult &Result) override;
|
|
};
|
|
} // end of anonymous namespace
|
|
|
|
void Callback::run(const MatchFinder::MatchResult &Result) {
|
|
bool IsPedanticMatch =
|
|
(Result.Nodes.getNodeAs<Stmt>("pedantic") != nullptr);
|
|
if (IsPedanticMatch && !C->Pedantic)
|
|
return;
|
|
|
|
ASTContext &ACtx = ADC->getASTContext();
|
|
|
|
if (const Expr *CheckIfNull =
|
|
Result.Nodes.getNodeAs<Expr>("check_if_null")) {
|
|
// Unless the macro indicates that the intended type is clearly not
|
|
// a pointer type, we should avoid warning on comparing pointers
|
|
// to zero literals in non-pedantic mode.
|
|
// FIXME: Introduce an AST matcher to implement the macro-related logic?
|
|
bool MacroIndicatesWeShouldSkipTheCheck = false;
|
|
SourceLocation Loc = CheckIfNull->getBeginLoc();
|
|
if (Loc.isMacroID()) {
|
|
StringRef MacroName = Lexer::getImmediateMacroName(
|
|
Loc, ACtx.getSourceManager(), ACtx.getLangOpts());
|
|
if (MacroName == "NULL" || MacroName == "nil")
|
|
return;
|
|
if (MacroName == "YES" || MacroName == "NO")
|
|
MacroIndicatesWeShouldSkipTheCheck = true;
|
|
}
|
|
if (!MacroIndicatesWeShouldSkipTheCheck) {
|
|
Expr::EvalResult EVResult;
|
|
if (CheckIfNull->IgnoreParenCasts()->EvaluateAsInt(
|
|
EVResult, ACtx, Expr::SE_AllowSideEffects)) {
|
|
llvm::APSInt Result = EVResult.Val.getInt();
|
|
if (Result == 0) {
|
|
if (!C->Pedantic)
|
|
return;
|
|
IsPedanticMatch = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const Stmt *Conv = Result.Nodes.getNodeAs<Stmt>("conv");
|
|
assert(Conv);
|
|
|
|
const Expr *ConvertedCObject = Result.Nodes.getNodeAs<Expr>("c_object");
|
|
const Expr *ConvertedCppObject = Result.Nodes.getNodeAs<Expr>("cpp_object");
|
|
const Expr *ConvertedObjCObject = Result.Nodes.getNodeAs<Expr>("objc_object");
|
|
bool IsCpp = (ConvertedCppObject != nullptr);
|
|
bool IsObjC = (ConvertedObjCObject != nullptr);
|
|
const Expr *Obj = IsObjC ? ConvertedObjCObject
|
|
: IsCpp ? ConvertedCppObject
|
|
: ConvertedCObject;
|
|
assert(Obj);
|
|
|
|
bool IsComparison =
|
|
(Result.Nodes.getNodeAs<Stmt>("comparison") != nullptr);
|
|
|
|
bool IsOSNumber =
|
|
(Result.Nodes.getNodeAs<Decl>("osnumber") != nullptr);
|
|
|
|
bool IsInteger =
|
|
(Result.Nodes.getNodeAs<QualType>("int_type") != nullptr);
|
|
bool IsObjCBool =
|
|
(Result.Nodes.getNodeAs<QualType>("objc_bool_type") != nullptr);
|
|
bool IsCppBool =
|
|
(Result.Nodes.getNodeAs<QualType>("cpp_bool_type") != nullptr);
|
|
|
|
llvm::SmallString<64> Msg;
|
|
llvm::raw_svector_ostream OS(Msg);
|
|
|
|
// Remove ObjC ARC qualifiers.
|
|
QualType ObjT = Obj->getType().getUnqualifiedType();
|
|
|
|
// Remove consts from pointers.
|
|
if (IsCpp) {
|
|
assert(ObjT.getCanonicalType()->isPointerType());
|
|
ObjT = ACtx.getPointerType(
|
|
ObjT->getPointeeType().getCanonicalType().getUnqualifiedType());
|
|
}
|
|
|
|
if (IsComparison)
|
|
OS << "Comparing ";
|
|
else
|
|
OS << "Converting ";
|
|
|
|
OS << "a pointer value of type '" << ObjT.getAsString() << "' to a ";
|
|
|
|
std::string EuphemismForPlain = "primitive";
|
|
std::string SuggestedApi = IsObjC ? (IsInteger ? "" : "-boolValue")
|
|
: IsCpp ? (IsOSNumber ? "" : "getValue()")
|
|
: "CFNumberGetValue()";
|
|
if (SuggestedApi.empty()) {
|
|
// A generic message if we're not sure what API should be called.
|
|
// FIXME: Pattern-match the integer type to make a better guess?
|
|
SuggestedApi =
|
|
"a method on '" + ObjT.getAsString() + "' to get the scalar value";
|
|
// "scalar" is not quite correct or common, but some documentation uses it
|
|
// when describing object methods we suggest. For consistency, we use
|
|
// "scalar" in the whole sentence when we need to use this word in at least
|
|
// one place, otherwise we use "primitive".
|
|
EuphemismForPlain = "scalar";
|
|
}
|
|
|
|
if (IsInteger)
|
|
OS << EuphemismForPlain << " integer value";
|
|
else if (IsObjCBool)
|
|
OS << EuphemismForPlain << " BOOL value";
|
|
else if (IsCppBool)
|
|
OS << EuphemismForPlain << " bool value";
|
|
else // Branch condition?
|
|
OS << EuphemismForPlain << " boolean value";
|
|
|
|
|
|
if (IsPedanticMatch)
|
|
OS << "; instead, either compare the pointer to "
|
|
<< (IsObjC ? "nil" : IsCpp ? "nullptr" : "NULL") << " or ";
|
|
else
|
|
OS << "; did you mean to ";
|
|
|
|
if (IsComparison)
|
|
OS << "compare the result of calling " << SuggestedApi;
|
|
else
|
|
OS << "call " << SuggestedApi;
|
|
|
|
if (!IsPedanticMatch)
|
|
OS << "?";
|
|
|
|
BR.EmitBasicReport(
|
|
ADC->getDecl(), C, "Suspicious number object conversion", "Logic error",
|
|
OS.str(),
|
|
PathDiagnosticLocation::createBegin(Obj, BR.getSourceManager(), ADC),
|
|
Conv->getSourceRange());
|
|
}
|
|
|
|
void NumberObjectConversionChecker::checkASTCodeBody(const Decl *D,
|
|
AnalysisManager &AM,
|
|
BugReporter &BR) const {
|
|
// Currently this matches CoreFoundation opaque pointer typedefs.
|
|
auto CSuspiciousNumberObjectExprM =
|
|
expr(ignoringParenImpCasts(
|
|
expr(hasType(
|
|
typedefType(hasDeclaration(anyOf(
|
|
typedefDecl(hasName("CFNumberRef")),
|
|
typedefDecl(hasName("CFBooleanRef")))))))
|
|
.bind("c_object")));
|
|
|
|
// Currently this matches XNU kernel number-object pointers.
|
|
auto CppSuspiciousNumberObjectExprM =
|
|
expr(ignoringParenImpCasts(
|
|
expr(hasType(hasCanonicalType(
|
|
pointerType(pointee(hasCanonicalType(
|
|
recordType(hasDeclaration(
|
|
anyOf(
|
|
cxxRecordDecl(hasName("OSBoolean")),
|
|
cxxRecordDecl(hasName("OSNumber"))
|
|
.bind("osnumber"))))))))))
|
|
.bind("cpp_object")));
|
|
|
|
// Currently this matches NeXTSTEP number objects.
|
|
auto ObjCSuspiciousNumberObjectExprM =
|
|
expr(ignoringParenImpCasts(
|
|
expr(hasType(hasCanonicalType(
|
|
objcObjectPointerType(pointee(
|
|
qualType(hasCanonicalType(
|
|
qualType(hasDeclaration(
|
|
objcInterfaceDecl(hasName("NSNumber")))))))))))
|
|
.bind("objc_object")));
|
|
|
|
auto SuspiciousNumberObjectExprM = anyOf(
|
|
CSuspiciousNumberObjectExprM,
|
|
CppSuspiciousNumberObjectExprM,
|
|
ObjCSuspiciousNumberObjectExprM);
|
|
|
|
// Useful for predicates like "Unless we've seen the same object elsewhere".
|
|
auto AnotherSuspiciousNumberObjectExprM =
|
|
expr(anyOf(
|
|
equalsBoundNode("c_object"),
|
|
equalsBoundNode("objc_object"),
|
|
equalsBoundNode("cpp_object")));
|
|
|
|
// The .bind here is in order to compose the error message more accurately.
|
|
auto ObjCSuspiciousScalarBooleanTypeM =
|
|
qualType(typedefType(hasDeclaration(
|
|
typedefDecl(hasName("BOOL"))))).bind("objc_bool_type");
|
|
|
|
// The .bind here is in order to compose the error message more accurately.
|
|
auto SuspiciousScalarBooleanTypeM =
|
|
qualType(anyOf(qualType(booleanType()).bind("cpp_bool_type"),
|
|
ObjCSuspiciousScalarBooleanTypeM));
|
|
|
|
// The .bind here is in order to compose the error message more accurately.
|
|
// Also avoid intptr_t and uintptr_t because they were specifically created
|
|
// for storing pointers.
|
|
auto SuspiciousScalarNumberTypeM =
|
|
qualType(hasCanonicalType(isInteger()),
|
|
unless(typedefType(hasDeclaration(
|
|
typedefDecl(matchesName("^::u?intptr_t$"))))))
|
|
.bind("int_type");
|
|
|
|
auto SuspiciousScalarTypeM =
|
|
qualType(anyOf(SuspiciousScalarBooleanTypeM,
|
|
SuspiciousScalarNumberTypeM));
|
|
|
|
auto SuspiciousScalarExprM =
|
|
expr(ignoringParenImpCasts(expr(hasType(SuspiciousScalarTypeM))));
|
|
|
|
auto ConversionThroughAssignmentM =
|
|
binaryOperator(allOf(hasOperatorName("="),
|
|
hasLHS(SuspiciousScalarExprM),
|
|
hasRHS(SuspiciousNumberObjectExprM)));
|
|
|
|
auto ConversionThroughBranchingM =
|
|
ifStmt(allOf(
|
|
hasCondition(SuspiciousNumberObjectExprM),
|
|
unless(hasConditionVariableStatement(declStmt())
|
|
))).bind("pedantic");
|
|
|
|
auto ConversionThroughCallM =
|
|
callExpr(hasAnyArgument(allOf(hasType(SuspiciousScalarTypeM),
|
|
ignoringParenImpCasts(
|
|
SuspiciousNumberObjectExprM))));
|
|
|
|
// We bind "check_if_null" to modify the warning message
|
|
// in case it was intended to compare a pointer to 0 with a relatively-ok
|
|
// construct "x == 0" or "x != 0".
|
|
auto ConversionThroughEquivalenceM =
|
|
binaryOperator(allOf(anyOf(hasOperatorName("=="), hasOperatorName("!=")),
|
|
hasEitherOperand(SuspiciousNumberObjectExprM),
|
|
hasEitherOperand(SuspiciousScalarExprM
|
|
.bind("check_if_null"))))
|
|
.bind("comparison");
|
|
|
|
auto ConversionThroughComparisonM =
|
|
binaryOperator(allOf(anyOf(hasOperatorName(">="), hasOperatorName(">"),
|
|
hasOperatorName("<="), hasOperatorName("<")),
|
|
hasEitherOperand(SuspiciousNumberObjectExprM),
|
|
hasEitherOperand(SuspiciousScalarExprM)))
|
|
.bind("comparison");
|
|
|
|
auto ConversionThroughConditionalOperatorM =
|
|
conditionalOperator(allOf(
|
|
hasCondition(SuspiciousNumberObjectExprM),
|
|
unless(hasTrueExpression(
|
|
hasDescendant(AnotherSuspiciousNumberObjectExprM))),
|
|
unless(hasFalseExpression(
|
|
hasDescendant(AnotherSuspiciousNumberObjectExprM)))))
|
|
.bind("pedantic");
|
|
|
|
auto ConversionThroughExclamationMarkM =
|
|
unaryOperator(allOf(hasOperatorName("!"),
|
|
has(expr(SuspiciousNumberObjectExprM))))
|
|
.bind("pedantic");
|
|
|
|
auto ConversionThroughExplicitBooleanCastM =
|
|
explicitCastExpr(allOf(hasType(SuspiciousScalarBooleanTypeM),
|
|
has(expr(SuspiciousNumberObjectExprM))));
|
|
|
|
auto ConversionThroughExplicitNumberCastM =
|
|
explicitCastExpr(allOf(hasType(SuspiciousScalarNumberTypeM),
|
|
has(expr(SuspiciousNumberObjectExprM))));
|
|
|
|
auto ConversionThroughInitializerM =
|
|
declStmt(hasSingleDecl(
|
|
varDecl(hasType(SuspiciousScalarTypeM),
|
|
hasInitializer(SuspiciousNumberObjectExprM))));
|
|
|
|
auto FinalM = stmt(anyOf(ConversionThroughAssignmentM,
|
|
ConversionThroughBranchingM,
|
|
ConversionThroughCallM,
|
|
ConversionThroughComparisonM,
|
|
ConversionThroughConditionalOperatorM,
|
|
ConversionThroughEquivalenceM,
|
|
ConversionThroughExclamationMarkM,
|
|
ConversionThroughExplicitBooleanCastM,
|
|
ConversionThroughExplicitNumberCastM,
|
|
ConversionThroughInitializerM)).bind("conv");
|
|
|
|
MatchFinder F;
|
|
Callback CB(this, BR, AM.getAnalysisDeclContext(D));
|
|
|
|
F.addMatcher(traverse(TK_AsIs, stmt(forEachDescendant(FinalM))), &CB);
|
|
F.match(*D->getBody(), AM.getASTContext());
|
|
}
|
|
|
|
void ento::registerNumberObjectConversionChecker(CheckerManager &Mgr) {
|
|
NumberObjectConversionChecker *Chk =
|
|
Mgr.registerChecker<NumberObjectConversionChecker>();
|
|
Chk->Pedantic =
|
|
Mgr.getAnalyzerOptions().getCheckerBooleanOption(Chk, "Pedantic");
|
|
}
|
|
|
|
bool ento::shouldRegisterNumberObjectConversionChecker(const CheckerManager &mgr) {
|
|
return true;
|
|
}
|