[clang] a3d0c54 - [clang][analyzer] Add StoreToImmutable checker (#150417)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Aug 4 05:56:36 PDT 2025
Author: Endre Fülöp
Date: 2025-08-04T14:56:33+02:00
New Revision: a3d0c541eb6a443ef4cf6c84facb4d5c35009bea
URL: https://github.com/llvm/llvm-project/commit/a3d0c541eb6a443ef4cf6c84facb4d5c35009bea
DIFF: https://github.com/llvm/llvm-project/commit/a3d0c541eb6a443ef4cf6c84facb4d5c35009bea.diff
LOG: [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
Added:
clang/docs/analyzer/checkers/storetoimmutable_example.cpp
clang/lib/StaticAnalyzer/Checkers/StoreToImmutableChecker.cpp
clang/test/Analysis/store-to-immutable-basic.c
clang/test/Analysis/store-to-immutable-basic.cpp
clang/test/Analysis/store-to-immutable-lambda-init.cpp
Modified:
clang/docs/analyzer/checkers.rst
clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
Removed:
################################################################################
diff --git a/clang/docs/analyzer/checkers.rst b/clang/docs/analyzer/checkers.rst
index 4e8b31869bb56..b2effadacf9f1 100644
--- a/clang/docs/analyzer/checkers.rst
+++ b/clang/docs/analyzer/checkers.rst
@@ -3086,6 +3086,23 @@ Either the comparison is useless or there is division by zero.
if (x == 0) { } // warn
}
+.. _alpha-core-StoreToImmutable:
+
+alpha.core.StoreToImmutable (C, C++)
+""""""""""""""""""""""""""""""""""""
+Check for writes to immutable memory regions. This implements part of SEI CERT Rule ENV30-C.
+
+This checker detects attempts to write to memory regions that are marked as immutable,
+including const variables, string literals, and other const-qualified memory.
+
+.. literalinclude:: checkers/storetoimmutable_example.cpp
+ :language: cpp
+
+**Solution**
+
+Avoid writing to const-qualified memory regions. If you need to modify the data,
+remove the const qualifier from the original declaration or use a mutable copy.
+
alpha.cplusplus
^^^^^^^^^^^^^^^
diff --git a/clang/docs/analyzer/checkers/storetoimmutable_example.cpp b/clang/docs/analyzer/checkers/storetoimmutable_example.cpp
new file mode 100644
index 0000000000000..bfd67ef7de96d
--- /dev/null
+++ b/clang/docs/analyzer/checkers/storetoimmutable_example.cpp
@@ -0,0 +1,21 @@
+const int global_const = 42;
+
+struct TestStruct {
+ const int x;
+ int y;
+};
+
+void immutable_violation_examples() {
+ *(int *)&global_const = 100; // warn: Trying to write to immutable memory
+
+ const int local_const = 42;
+ *(int *)&local_const = 43; // warn: Trying to write to immutable memory
+
+ // NOTE: The following is reported in C++, but not in C, as the analyzer
+ // treats string literals as non-const char arrays in C mode.
+ char *ptr_to_str_literal = (char *)"hello";
+ ptr_to_str_literal[0] = 'H'; // warn: Trying to write to immutable memory
+
+ TestStruct s = {1, 2};
+ *(int *)&s.x = 10; // warn: Trying to write to immutable memory
+}
diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
index 36196cdf62c6f..73f702de581d9 100644
--- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
+++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
@@ -297,6 +297,11 @@ def StackAddrAsyncEscapeChecker
"Check that addresses to stack memory do not escape the function">,
Documentation<HasDocumentation>;
+def StoreToImmutableChecker : Checker<"StoreToImmutable">,
+ HelpText<"Check for writes to immutable memory regions. "
+ "This implements part of SEI CERT Rule ENV30-C.">,
+ Documentation<HasDocumentation>;
+
def PthreadLockBase : Checker<"PthreadLockBase">,
HelpText<"Helper registering multiple checks.">,
Documentation<NotDocumented>,
diff --git a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
index 22dd3f0374849..78360418a8b81 100644
--- a/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
+++ b/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
@@ -104,6 +104,7 @@ add_clang_library(clangStaticAnalyzerCheckers
SmartPtrChecker.cpp
SmartPtrModeling.cpp
StackAddrEscapeChecker.cpp
+ StoreToImmutableChecker.cpp
StdLibraryFunctionsChecker.cpp
StdVariantChecker.cpp
STLAlgorithmModeling.cpp
diff --git a/clang/lib/StaticAnalyzer/Checkers/StoreToImmutableChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StoreToImmutableChecker.cpp
new file mode 100644
index 0000000000000..afad41939cdca
--- /dev/null
+++ b/clang/lib/StaticAnalyzer/Checkers/StoreToImmutableChecker.cpp
@@ -0,0 +1,188 @@
+//=== 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
diff erent:
+ // ...
+ // `-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;
+}
diff --git a/clang/test/Analysis/store-to-immutable-basic.c b/clang/test/Analysis/store-to-immutable-basic.c
new file mode 100644
index 0000000000000..f7d887819714f
--- /dev/null
+++ b/clang/test/Analysis/store-to-immutable-basic.c
@@ -0,0 +1,121 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.core.StoreToImmutable -verify %s
+
+// Test basic functionality of StoreToImmutable checker for the C programming language.
+
+const int tentative_global_const; // expected-note {{Memory region is declared as immutable here}}
+
+void test_direct_write_to_tentative_const_global() {
+ *(int*)&tentative_global_const = 100; // expected-warning {{Trying to write to immutable memory in global read-only storage}}
+}
+
+const int global_const = 42; // expected-note {{Memory region is declared as immutable here}}
+
+void test_direct_write_to_const_global() {
+ // This should trigger a warning about writing to immutable memory
+ *(int*)&global_const = 100; // expected-warning {{Trying to write to immutable memory in global read-only storage}}
+}
+
+void test_write_through_const_pointer() {
+ const int local_const = 10; // expected-note {{Memory region is declared as immutable here}}
+ int *ptr = (int*)&local_const;
+ *ptr = 20; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_write_to_const_array() {
+ const int arr[5] = {1, 2, 3, 4, 5}; // expected-note {{Enclosing memory region is declared as immutable here}}
+ int *ptr = (int*)arr;
+ ptr[0] = 10; // expected-warning {{Trying to write to immutable memory}}
+}
+
+struct TestStruct {
+ const int x; // expected-note 2 {{Memory region is declared as immutable here}}
+ int y;
+};
+
+void test_write_to_const_struct_member() {
+ struct TestStruct s = {1, 2};
+ int *ptr = (int*)&s.x;
+ *ptr = 10; // expected-warning {{Trying to write to immutable memory}}
+}
+
+const int global_array[3] = {1, 2, 3}; // expected-note {{Enclosing memory region is declared as immutable here}}
+
+void test_write_to_const_global_array() {
+ int *ptr = (int*)global_array;
+ ptr[0] = 10; // expected-warning {{Trying to write to immutable memory in global read-only storage}}
+}
+
+const struct TestStruct global_struct = {1, 2};
+
+void test_write_to_const_global_struct() {
+ int *ptr = (int*)&global_struct.x;
+ *ptr = 10; // expected-warning {{Trying to write to immutable memory in global read-only storage}}
+}
+
+
+void test_write_to_const_param(const int param) { // expected-note {{Memory region is declared as immutable here}}
+ *(int*)¶m = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_write_to_const_ptr_param(const int *param) {
+ *(int*)param = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_write_to_const_array_param(const int arr[5]) {
+ *(int*)arr = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+struct ParamStruct {
+ const int z; // expected-note 2 {{Memory region is declared as immutable here}}
+ int w;
+};
+
+void test_write_to_const_struct_param(const struct ParamStruct s) {
+ *(int*)&s.z = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_write_to_const_struct_ptr_param(const struct ParamStruct *s) {
+ *(int*)&s->z = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_write_to_nonconst() {
+ int non_const = 42;
+ *(int*)&non_const = 100; // No warning expected
+}
+
+int global_non_const = 42;
+
+void test_write_to_nonconst_global() {
+ *(int*)&global_non_const = 100; // No warning expected
+}
+
+struct NonConstStruct {
+ int x;
+ int y;
+};
+
+void test_write_to_nonconst_struct_member() {
+ struct NonConstStruct s = {1, 2};
+ *(int*)&s.x = 100; // No warning expected
+}
+
+void test_write_to_nonconst_param(int param) {
+ *(int*)¶m = 100; // No warning expected
+}
+
+void test_normal_assignment() {
+ int x = 42;
+ x = 100; // No warning expected
+}
+
+void test_const_ptr_to_nonconst_data() {
+ int data = 42;
+ const int *ptr = &data;
+ *(int*)ptr = 100; // No warning expected
+}
+
+void test_const_ptr_to_const_data() {
+ const int data = 42; // expected-note {{Memory region is declared as immutable here}}
+ const int *ptr = &data;
+ *(int*)ptr = 100; // expected-warning {{Trying to write to immutable memory}}
+}
diff --git a/clang/test/Analysis/store-to-immutable-basic.cpp b/clang/test/Analysis/store-to-immutable-basic.cpp
new file mode 100644
index 0000000000000..63319d9dfb7b1
--- /dev/null
+++ b/clang/test/Analysis/store-to-immutable-basic.cpp
@@ -0,0 +1,72 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.core.StoreToImmutable -std=c++17 -verify %s
+
+void test_write_to_const_ref_param(const int ¶m) {
+ *(int*)¶m = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+// FIXME: This should warn in C mode too.
+void test_write_to_string_literal() {
+ char *str = (char*)"hello";
+ str[0] = 'H'; // expected-warning {{Trying to write to immutable memory}}
+}
+
+struct ParamStruct {
+ const int z; // expected-note {{Memory region is declared as immutable here}}
+ int w;
+};
+
+void test_write_to_const_struct_ref_param(const ParamStruct &s) {
+ *(int*)&s.z = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_const_ref_to_nonconst_data() {
+ int data = 42;
+ const int &ref = data;
+ *(int*)&ref = 100; // No warning expected
+}
+
+void test_const_ref_to_const_data() {
+ const int data = 42; // expected-note {{Memory region is declared as immutable here}}
+ const int &ref = data;
+ *(int*)&ref = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_ref_to_nonconst_data() {
+ int data = 42;
+ int &ref = data;
+ ref = 100; // No warning expected
+}
+
+void test_ref_to_const_data() {
+ const int data = 42; // expected-note {{Memory region is declared as immutable here}}
+ int &ref = *(int*)&data;
+ ref = 100; // expected-warning {{Trying to write to immutable memory}}
+}
+
+struct MultipleLayerStruct {
+ MultipleLayerStruct();
+ const int data; // expected-note {{Memory region is declared as immutable here}}
+ const int buf[10]; // expected-note {{Enclosing memory region is declared as immutable here}}
+};
+
+MultipleLayerStruct MLS[10];
+
+void test_multiple_layer_struct_array_member() {
+ int *p = (int*)&MLS[2].data;
+ *p = 4; // expected-warning {{Trying to write to immutable memory}}
+}
+
+void test_multiple_layer_struct_array_array_member() {
+ int *p = (int*)&MLS[2].buf[3];
+ *p = 4; // expected-warning {{Trying to write to immutable memory}}
+}
+
+struct StructWithNonConstMember {
+ int x;
+};
+
+const StructWithNonConstMember SWNCM{0}; // expected-note {{Enclosing memory region is declared as immutable here}}
+
+void test_write_to_non_const_member_of_const_struct() {
+ *(int*)&SWNCM.x = 100; // expected-warning {{Trying to write to immutable memory in global read-only storage}}
+}
diff --git a/clang/test/Analysis/store-to-immutable-lambda-init.cpp b/clang/test/Analysis/store-to-immutable-lambda-init.cpp
new file mode 100644
index 0000000000000..764ce3fdbeb50
--- /dev/null
+++ b/clang/test/Analysis/store-to-immutable-lambda-init.cpp
@@ -0,0 +1,13 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.core.StoreToImmutable -std=c++11 -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.core.StoreToImmutable -std=c++14 -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.core.StoreToImmutable -std=c++17 -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=alpha.core.StoreToImmutable -std=c++20 -verify %s
+
+// expected-no-diagnostics
+
+// In C++14 and before, when initializing a lambda, the statement given in the checkBind callback is not the whole DeclExpr, but the CXXConstructExpr of the lambda object.
+// FIXME: Once the API of checkBind provides more information about the statement, the checker should be simplified, and this test case will no longer be a cornercase in the checker.
+
+void test_const_lambda_initialization_pre_cpp17() {
+ const auto lambda = [](){}; // No warning expected
+}
More information about the cfe-commits
mailing list