llvm-project/clang/lib/StaticAnalyzer/Checkers/StoreToImmutableChecker.cpp
Endre Fülöp a3d0c541eb
[clang][analyzer] Add StoreToImmutable checker (#150417)
This adds alpha.core.StoreToImmutable, a new alpha checker that detects
writes
to immutable memory regions, implementing part of SEI CERT Rule ENV30-C.
The
original proposal only handled global const variables, but this
implementation
extends it to also detect writes to:
- Local const variables
- String literals
- Const parameters and struct members
- Const arrays and pointers to const data

This checker is the continuation of the work started by zukatsinadze.
Discussion: https://reviews.llvm.org/D124244
2025-08-04 14:56:33 +02:00

189 lines
6.7 KiB
C++

//=== StoreToImmutableChecker.cpp - Store to immutable memory ---*- 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 StoreToImmutableChecker, a checker that detects writes
// to immutable memory regions. This implements part of SEI CERT Rule ENV30-C.
//
//===----------------------------------------------------------------------===//
#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/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/MemRegion.h"
using namespace clang;
using namespace ento;
namespace {
class StoreToImmutableChecker : public Checker<check::Bind> {
const BugType BT{this, "Write to immutable memory", "CERT Environment (ENV)"};
public:
void checkBind(SVal Loc, SVal Val, const Stmt *S, CheckerContext &C) const;
};
} // end anonymous namespace
static bool isInitializationContext(const Stmt *S, CheckerContext &C) {
// Check if this is a DeclStmt (variable declaration)
if (isa<DeclStmt>(S))
return true;
// This part is specific for initialization of const lambdas pre-C++17.
// Lets look at the AST of the statement:
// ```
// const auto lambda = [](){};
// ```
//
// The relevant part of the AST for this case prior to C++17 is:
// ...
// `-DeclStmt
// `-VarDecl
// `-ExprWithCleanups
// `-CXXConstructExpr
// ...
// In C++17 and later, the AST is different:
// ...
// `-DeclStmt
// `-VarDecl
// `-ImplicitCastExpr
// `-LambdaExpr
// |-CXXRecordDecl
// `-CXXConstructExpr
// ...
// And even beside this, the statement `S` that is given to the checkBind
// callback is the VarDecl in C++17 and later, and the CXXConstructExpr in
// C++14 and before. So in order to support the C++14 we need the following
// ugly hack to detect whether this construction is used to initialize a
// variable.
//
// FIXME: This should be eliminated by improving the API of checkBind to
// ensure that it consistently passes the `VarDecl` (instead of the
// `CXXConstructExpr`) when the constructor call denotes the initialization
// of a variable with a lambda, or maybe less preferably, try the more
// invasive approach of passing the information forward to the checkers
// whether the current bind is an initialization or an assignment.
const auto *ConstructExp = dyn_cast<CXXConstructExpr>(S);
return ConstructExp && ConstructExp->isElidable();
}
static bool isEffectivelyConstRegion(const MemRegion *MR, CheckerContext &C) {
if (isa<GlobalImmutableSpaceRegion>(MR))
return true;
// Check if this is a TypedRegion with a const-qualified type
if (const auto *TR = dyn_cast<TypedRegion>(MR)) {
QualType LocationType = TR->getDesugaredLocationType(C.getASTContext());
if (LocationType->isPointerOrReferenceType())
LocationType = LocationType->getPointeeType();
if (LocationType.isConstQualified())
return true;
}
// Check if this is a SymbolicRegion with a const-qualified pointee type
if (const auto *SR = dyn_cast<SymbolicRegion>(MR)) {
QualType PointeeType = SR->getPointeeStaticType();
if (PointeeType.isConstQualified())
return true;
}
// NOTE: The above branches do not cover AllocaRegion. We do not need to check
// AllocaRegion, as it models untyped memory, that is allocated on the stack.
return false;
}
static const MemRegion *getInnermostConstRegion(const MemRegion *MR,
CheckerContext &C) {
while (true) {
if (isEffectivelyConstRegion(MR, C))
return MR;
if (auto *SR = dyn_cast<SubRegion>(MR))
MR = SR->getSuperRegion();
else
return nullptr;
}
}
static const DeclRegion *
getInnermostEnclosingConstDeclRegion(const MemRegion *MR, CheckerContext &C) {
while (true) {
if (const auto *DR = dyn_cast<DeclRegion>(MR)) {
const ValueDecl *D = DR->getDecl();
QualType DeclaredType = D->getType();
if (DeclaredType.isConstQualified())
return DR;
}
if (auto *SR = dyn_cast<SubRegion>(MR))
MR = SR->getSuperRegion();
else
return nullptr;
}
}
void StoreToImmutableChecker::checkBind(SVal Loc, SVal Val, const Stmt *S,
CheckerContext &C) const {
// We are only interested in stores to memory regions
const MemRegion *MR = Loc.getAsRegion();
if (!MR)
return;
// Skip variable declarations and initializations - we only want to catch
// actual writes
// FIXME: If the API of checkBind would allow to distinguish between
// initialization and assignment, we could use that instead.
if (isInitializationContext(S, C))
return;
// Check if the region is in the global immutable space
const MemSpaceRegion *MS = MR->getMemorySpace(C.getState());
const bool IsGlobalImmutableSpace = isa<GlobalImmutableSpaceRegion>(MS);
// Check if the region corresponds to a const variable
const MemRegion *InnermostConstRegion = getInnermostConstRegion(MR, C);
if (!IsGlobalImmutableSpace && !InnermostConstRegion)
return;
SmallString<64> WarningMessage{"Trying to write to immutable memory"};
if (IsGlobalImmutableSpace)
WarningMessage += " in global read-only storage";
// Generate the bug report
ExplodedNode *N = C.generateNonFatalErrorNode();
if (!N)
return;
auto R = std::make_unique<PathSensitiveBugReport>(BT, WarningMessage, N);
R->addRange(S->getSourceRange());
// Generate a note if the location that is being written to has a
// declaration or if it is a subregion of a const region with a declaration.
const DeclRegion *DR =
getInnermostEnclosingConstDeclRegion(InnermostConstRegion, C);
if (DR) {
const char *NoteMessage =
(DR != MR) ? "Enclosing memory region is declared as immutable here"
: "Memory region is declared as immutable here";
R->addNote(NoteMessage, PathDiagnosticLocation::create(
DR->getDecl(), C.getSourceManager()));
}
// For this checker, we are only interested in the value being written, no
// need to mark the value being assigned interesting.
C.emitReport(std::move(R));
}
void ento::registerStoreToImmutableChecker(CheckerManager &mgr) {
mgr.registerChecker<StoreToImmutableChecker>();
}
bool ento::shouldRegisterStoreToImmutableChecker(const CheckerManager &mgr) {
return true;
}