[clang] 8570ba2 - [clang][analyzer] Add checker 'core.NullPointerArithm' (#157129)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Oct 15 23:44:59 PDT 2025
Author: Balázs Kéri
Date: 2025-10-16T08:44:56+02:00
New Revision: 8570ba2d87eb4877c428fa616b6fe4141684e467
URL: https://github.com/llvm/llvm-project/commit/8570ba2d87eb4877c428fa616b6fe4141684e467
DIFF: https://github.com/llvm/llvm-project/commit/8570ba2d87eb4877c428fa616b6fe4141684e467.diff
LOG: [clang][analyzer] Add checker 'core.NullPointerArithm' (#157129)
Added:
clang/test/Analysis/null-pointer-arithm.c
Modified:
clang/docs/analyzer/checkers.rst
clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
clang/lib/StaticAnalyzer/Checkers/DereferenceChecker.cpp
clang/test/Analysis/analyzer-enabled-checkers.c
clang/test/Analysis/std-c-library-functions-arg-enabled-checkers.c
Removed:
################################################################################
diff --git a/clang/docs/analyzer/checkers.rst b/clang/docs/analyzer/checkers.rst
index d942578dd7596..dcfa4e3ccc401 100644
--- a/clang/docs/analyzer/checkers.rst
+++ b/clang/docs/analyzer/checkers.rst
@@ -205,6 +205,50 @@ pointers with a specified address space. If the option is set to false, then
reports from the specific x86 address spaces 256, 257 and 258 are still
suppressed, but null dereferences from other address spaces are reported.
+.. _core-NullPointerArithm:
+
+core.NullPointerArithm (C, C++)
+"""""""""""""""""""""""""""""""
+Check for undefined arithmetic operations with null pointers.
+
+The checker can detect the following cases:
+
+ - ``p + x`` and ``x + p`` where ``p`` is a null pointer and ``x`` is a nonzero
+ integer value.
+ - ``p - x`` where ``p`` is a null pointer and ``x`` is a nonzero integer
+ value.
+ - ``p1 - p2`` where one of ``p1`` and ``p2`` is null and the other a
+ non-null pointer.
+
+Result of these operations is undefined according to the standard.
+In the above listed cases, the checker will warn even if the expression
+described to be "nonzero" or "non-null" has unknown value, because it is likely
+that it can have non-zero value during the program execution.
+
+.. code-block:: c
+
+ void test1(int *p, int offset) {
+ if (p)
+ return;
+
+ int *p1 = p + offset; // warn: 'p' is null, 'offset' is unknown but likely non-zero
+ }
+
+ void test2(int *p, int offset) {
+ if (p) { } // this indicates that it is possible for 'p' to be null
+ if (offset == 0)
+ return;
+
+ int *p1 = p - offset; // warn: 'p' is null, 'offset' is known to be non-zero
+ }
+
+ void test3(char *p1, char *p2) {
+ if (p1)
+ return;
+
+ int a = p1 - p2; // warn: 'p1' is null, 'p2' can be likely non-null
+ }
+
.. _core-StackAddressEscape:
core.StackAddressEscape (C)
diff --git a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
index 4473c54d8d6e3..b83bbcdb85a8f 100644
--- a/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
+++ b/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
@@ -195,6 +195,11 @@ def NullDereferenceChecker
HelpText<"Check for dereferences of null pointers">,
Documentation<HasDocumentation>;
+def NullPointerArithmChecker
+ : Checker<"NullPointerArithm">,
+ HelpText<"Check for undefined arithmetic operations on null pointers">,
+ Documentation<HasDocumentation>;
+
def NonNullParamChecker : Checker<"NonNullParamChecker">,
HelpText<"Check for null pointers passed as arguments to a function whose "
"arguments are references or marked with the 'nonnull' attribute">,
diff --git a/clang/lib/StaticAnalyzer/Checkers/DereferenceChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/DereferenceChecker.cpp
index 395d724cdfd11..37f5ec3557400 100644
--- a/clang/lib/StaticAnalyzer/Checkers/DereferenceChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/DereferenceChecker.cpp
@@ -19,6 +19,7 @@
#include "clang/StaticAnalyzer/Core/CheckerManager.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h"
+#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/raw_ostream.h"
using namespace clang;
@@ -39,9 +40,10 @@ class DerefBugType : public BugType {
class DereferenceChecker
: public CheckerFamily<check::Location, check::Bind,
+ check::PreStmt<BinaryOperator>,
EventDispatcher<ImplicitNullDerefEvent>> {
- void reportBug(const DerefBugType &BT, ProgramStateRef State, const Stmt *S,
- CheckerContext &C) const;
+ void reportDerefBug(const DerefBugType &BT, ProgramStateRef State,
+ const Stmt *S, CheckerContext &C) const;
bool suppressReport(CheckerContext &C, const Expr *E) const;
@@ -50,6 +52,7 @@ class DereferenceChecker
CheckerContext &C) const;
void checkBind(SVal L, SVal V, const Stmt *S, bool AtDeclInit,
CheckerContext &C) const;
+ void checkPreStmt(const BinaryOperator *Op, CheckerContext &C) const;
static void AddDerefSource(raw_ostream &os,
SmallVectorImpl<SourceRange> &Ranges,
@@ -57,7 +60,7 @@ class DereferenceChecker
const LocationContext *LCtx,
bool loadedFrom = false);
- CheckerFrontend NullDerefChecker, FixedDerefChecker;
+ CheckerFrontend NullDerefChecker, FixedDerefChecker, NullPointerArithmChecker;
const DerefBugType NullBug{&NullDerefChecker, "Dereference of null pointer",
"a null pointer dereference",
"a dereference of a null pointer"};
@@ -72,9 +75,22 @@ class DereferenceChecker
const DerefBugType FixedAddressBug{&FixedDerefChecker,
"Dereference of a fixed address",
"a dereference of a fixed address"};
+ const BugType NullPointerArithmBug{
+ &NullPointerArithmChecker,
+ "Possibly undefined arithmetic operation involving a null pointer"};
StringRef getDebugTag() const override { return "DereferenceChecker"; }
};
+
+struct ValueDescStr {
+ SmallVectorImpl<SourceRange> &Ranges;
+ const Expr *Ex;
+ const ProgramState *State;
+ const LocationContext *LCtx;
+ bool IsPointer;
+ ConditionTruthVal IsNull;
+};
+
} // end anonymous namespace
void
@@ -173,9 +189,9 @@ static bool isDeclRefExprToReference(const Expr *E) {
return false;
}
-void DereferenceChecker::reportBug(const DerefBugType &BT,
- ProgramStateRef State, const Stmt *S,
- CheckerContext &C) const {
+void DereferenceChecker::reportDerefBug(const DerefBugType &BT,
+ ProgramStateRef State, const Stmt *S,
+ CheckerContext &C) const {
if (&BT == &FixedAddressBug) {
if (!FixedDerefChecker.isEnabled())
// Deliberately don't add a sink node if check is disabled.
@@ -249,9 +265,8 @@ void DereferenceChecker::reportBug(const DerefBugType &BT,
bugreporter::trackExpressionValue(N, bugreporter::getDerefExpr(S), *BR);
- for (SmallVectorImpl<SourceRange>::iterator
- I = Ranges.begin(), E = Ranges.end(); I!=E; ++I)
- BR->addRange(*I);
+ for (const auto &R : Ranges)
+ BR->addRange(R);
C.emitReport(std::move(BR));
}
@@ -262,7 +277,7 @@ void DereferenceChecker::checkLocation(SVal l, bool isLoad, const Stmt* S,
if (l.isUndef()) {
const Expr *DerefExpr = getDereferenceExpr(S);
if (!suppressReport(C, DerefExpr))
- reportBug(UndefBug, C.getState(), DerefExpr, C);
+ reportDerefBug(UndefBug, C.getState(), DerefExpr, C);
return;
}
@@ -283,7 +298,7 @@ void DereferenceChecker::checkLocation(SVal l, bool isLoad, const Stmt* S,
// we call an "explicit" null dereference.
const Expr *expr = getDereferenceExpr(S);
if (!suppressReport(C, expr)) {
- reportBug(NullBug, nullState, expr, C);
+ reportDerefBug(NullBug, nullState, expr, C);
return;
}
}
@@ -301,7 +316,7 @@ void DereferenceChecker::checkLocation(SVal l, bool isLoad, const Stmt* S,
if (location.isConstant()) {
const Expr *DerefExpr = getDereferenceExpr(S, isLoad);
if (!suppressReport(C, DerefExpr))
- reportBug(FixedAddressBug, notNullState, DerefExpr, C);
+ reportDerefBug(FixedAddressBug, notNullState, DerefExpr, C);
return;
}
@@ -317,7 +332,7 @@ void DereferenceChecker::checkBind(SVal L, SVal V, const Stmt *S,
// One should never write to label addresses.
if (auto Label = L.getAs<loc::GotoLabel>()) {
- reportBug(LabelBug, C.getState(), S, C);
+ reportDerefBug(LabelBug, C.getState(), S, C);
return;
}
@@ -338,7 +353,7 @@ void DereferenceChecker::checkBind(SVal L, SVal V, const Stmt *S,
if (!StNonNull) {
const Expr *expr = getDereferenceExpr(S, /*IsBind=*/true);
if (!suppressReport(C, expr)) {
- reportBug(NullBug, StNull, expr, C);
+ reportDerefBug(NullBug, StNull, expr, C);
return;
}
}
@@ -356,7 +371,7 @@ void DereferenceChecker::checkBind(SVal L, SVal V, const Stmt *S,
if (V.isConstant()) {
const Expr *DerefExpr = getDereferenceExpr(S, true);
if (!suppressReport(C, DerefExpr))
- reportBug(FixedAddressBug, State, DerefExpr, C);
+ reportDerefBug(FixedAddressBug, State, DerefExpr, C);
return;
}
@@ -379,6 +394,96 @@ void DereferenceChecker::checkBind(SVal L, SVal V, const Stmt *S,
C.addTransition(State, this);
}
+namespace llvm {
+template <> struct format_provider<ValueDescStr> {
+ static void format(const ValueDescStr &V, raw_ostream &Stream,
+ StringRef Style) {
+ static const char *ValueStr[2][3] = {
+ {"zero", "nonzero integer value", "probably nonzero integer value"},
+ {"null pointer", "non-null pointer", "probably non-null pointer"},
+ };
+ Stream
+ << ValueStr[V.IsPointer][V.IsNull.isConstrainedTrue()
+ ? 0
+ : (V.IsNull.isConstrainedFalse() ? 1 : 2)];
+ DereferenceChecker::AddDerefSource(Stream, V.Ranges, V.Ex, V.State, V.LCtx,
+ false);
+ }
+};
+} // namespace llvm
+
+void DereferenceChecker::checkPreStmt(const BinaryOperator *Op,
+ CheckerContext &C) const {
+ if (!Op->isAdditiveOp() || !NullPointerArithmChecker.isEnabled())
+ return;
+ const Expr *E1 = Op->getLHS();
+ const Expr *E2 = Op->getRHS();
+ QualType T1 = E1->getType().getCanonicalType();
+ QualType T2 = E2->getType().getCanonicalType();
+ bool T1IsPointer = T1->isPointerType();
+ bool T2IsPointer = T2->isPointerType();
+ if (T1->isIntegerType() && T2->isIntegerType())
+ return;
+ if (!T1IsPointer && !T1->isIntegerType() && !T2IsPointer &&
+ !T2->isIntegerType())
+ return;
+
+ ProgramStateRef State = C.getState();
+ ConditionTruthVal V1IsNull = State->isNull(C.getSVal(E1));
+ ConditionTruthVal V2IsNull = State->isNull(C.getSVal(E2));
+ bool IsConstrained = true;
+
+ // Check cases 'NULL + x' and 'NULL - x'
+ if (T1IsPointer && !T2IsPointer) {
+ if (!V1IsNull.isConstrainedTrue() || V2IsNull.isConstrainedTrue())
+ return;
+ IsConstrained = V2IsNull.isConstrainedFalse();
+ }
+
+ // Check case 'x + NULL'
+ if (!T1IsPointer && T2IsPointer) {
+ if (V1IsNull.isConstrainedTrue() || !V2IsNull.isConstrainedTrue())
+ return;
+ IsConstrained = V1IsNull.isConstrainedFalse();
+ }
+
+ // Check case 'NULL - p' or 'p - NULL'
+ if (T1IsPointer && T2IsPointer) {
+ if (!V1IsNull.isConstrainedTrue() && !V2IsNull.isConstrainedTrue())
+ return;
+ if (V1IsNull.isConstrainedTrue() && V2IsNull.isConstrainedTrue())
+ return;
+ IsConstrained =
+ V1IsNull.isConstrainedFalse() || V2IsNull.isConstrainedFalse();
+ }
+
+ SmallVector<SourceRange, 2> Ranges;
+ const char *OpcodeStr =
+ Op->getOpcode() == BO_Add ? "Addition" : "Subtraction";
+ const char *ResultStr = IsConstrained ? "results" : "may result";
+ ValueDescStr DerefArg1{
+ Ranges, E1, State.get(), C.getLocationContext(), T1IsPointer, V1IsNull};
+ ValueDescStr DerefArg2{
+ Ranges, E2, State.get(), C.getLocationContext(), T2IsPointer, V2IsNull};
+ std::string Msg =
+ llvm::formatv("{0} of a {1} and a {2} {3} in undefined behavior",
+ OpcodeStr, DerefArg1, DerefArg2, ResultStr);
+
+ ExplodedNode *N = C.generateErrorNode(State);
+ if (!N)
+ return;
+ auto BR =
+ std::make_unique<PathSensitiveBugReport>(NullPointerArithmBug, Msg, N);
+ if (V1IsNull.isConstrainedTrue())
+ bugreporter::trackExpressionValue(N, E1, *BR);
+ if (V2IsNull.isConstrainedTrue())
+ bugreporter::trackExpressionValue(N, E2, *BR);
+ for (const auto &R : Ranges)
+ BR->addRange(R);
+
+ C.emitReport(std::move(BR));
+}
+
void ento::registerNullDereferenceChecker(CheckerManager &Mgr) {
Mgr.getChecker<DereferenceChecker>()->NullDerefChecker.enable(Mgr);
}
@@ -395,3 +500,11 @@ bool ento::shouldRegisterFixedAddressDereferenceChecker(
const CheckerManager &) {
return true;
}
+
+void ento::registerNullPointerArithmChecker(CheckerManager &Mgr) {
+ Mgr.getChecker<DereferenceChecker>()->NullPointerArithmChecker.enable(Mgr);
+}
+
+bool ento::shouldRegisterNullPointerArithmChecker(const CheckerManager &) {
+ return true;
+}
diff --git a/clang/test/Analysis/analyzer-enabled-checkers.c b/clang/test/Analysis/analyzer-enabled-checkers.c
index 009233108a70a..bfe418b112a9d 100644
--- a/clang/test/Analysis/analyzer-enabled-checkers.c
+++ b/clang/test/Analysis/analyzer-enabled-checkers.c
@@ -19,6 +19,7 @@
// CHECK-NEXT: core.NonNullParamChecker
// CHECK-NEXT: core.NonnilStringConstants
// CHECK-NEXT: core.NullDereference
+// CHECK-NEXT: core.NullPointerArithm
// CHECK-NEXT: core.StackAddressEscape
// CHECK-NEXT: core.UndefinedBinaryOperatorResult
// CHECK-NEXT: core.VLASize
diff --git a/clang/test/Analysis/null-pointer-arithm.c b/clang/test/Analysis/null-pointer-arithm.c
new file mode 100644
index 0000000000000..228824767937f
--- /dev/null
+++ b/clang/test/Analysis/null-pointer-arithm.c
@@ -0,0 +1,76 @@
+// RUN: %clang_analyze_cc1 -verify %s \
+// RUN: -analyzer-checker=core
+
+extern int *get_pointer();
+
+int *test_add1(int offset) {
+ int *p = get_pointer();
+ if (p) {}
+ return p + offset; // expected-warning{{Addition of a null pointer (from variable 'p') and a probably nonzero integer value (from variable 'offset') may result in undefined behavior}}
+}
+
+int *test_add2(int offset) {
+ int *p = get_pointer();
+ if (p) {}
+ if (offset) {}
+ return p + offset; // expected-warning{{Addition of a null pointer (from variable 'p') and a nonzero integer value (from variable 'offset') results in undefined behavior}}
+}
+
+int *test_add3(int offset) {
+ int *p = get_pointer();
+ if (p) {}
+ if (offset != 0) return 0;
+ return p + offset;
+}
+
+int *test_add4(int offset) {
+ int *p = get_pointer();
+ if (p) {}
+ if (offset == 0) return 0;
+ return p + offset; // expected-warning{{Addition of a null pointer (from variable 'p') and a nonzero integer value (from variable 'offset') results in undefined behavior}}
+}
+
+int *test_add5(int offset) {
+ int *p = get_pointer();
+ if (p) {}
+ return offset + p; // expected-warning{{Addition of a probably nonzero integer value (from variable 'offset') and a null pointer (from variable 'p') may result in undefined behavior}}
+}
+
+int *test_sub1(int offset) {
+ int *p = get_pointer();
+ if (p) {}
+ return p - offset; // expected-warning{{Subtraction of a null pointer (from variable 'p') and a probably nonzero integer value (from variable 'offset') may result in undefined behavior}}
+}
+
+int test_sub_p1() {
+ int *p = get_pointer();
+ if (p) {}
+ return p - p;
+}
+
+int test_sub_p2() {
+ int *p1 = get_pointer();
+ int *p2 = get_pointer();
+ if (p1) {}
+ if (p2) {}
+ return p1 - p2;
+ // expected-warning at -1{{Subtraction of a non-null pointer (from variable 'p1') and a null pointer (from variable 'p2') results in undefined behavior}}
+ // expected-warning at -2{{Subtraction of a null pointer (from variable 'p1') and a non-null pointer (from variable 'p2') results in undefined behavior}}
+}
+
+int test_sub_p3() {
+ int *p1 = get_pointer();
+ int *p2 = get_pointer();
+ if (p1) {}
+ return p1 - p2; // expected-warning{{Subtraction of a null pointer (from variable 'p1') and a probably non-null pointer (from variable 'p2') may result in undefined behavior}}
+}
+
+struct S {
+ char *p;
+ int offset;
+};
+
+char *test_struct(struct S s) {
+ if (s.p) {}
+ return s.p + s.offset; // expected-warning{{Addition of a null pointer (via field 'p') and a probably nonzero integer value (via field 'offset') may result in undefined behavior}}
+}
diff --git a/clang/test/Analysis/std-c-library-functions-arg-enabled-checkers.c b/clang/test/Analysis/std-c-library-functions-arg-enabled-checkers.c
index 7fae958f6afc6..9b3296064981f 100644
--- a/clang/test/Analysis/std-c-library-functions-arg-enabled-checkers.c
+++ b/clang/test/Analysis/std-c-library-functions-arg-enabled-checkers.c
@@ -27,6 +27,7 @@
// CHECK-NEXT: core.NonNullParamChecker
// CHECK-NEXT: core.NonnilStringConstants
// CHECK-NEXT: core.NullDereference
+// CHECK-NEXT: core.NullPointerArithm
// CHECK-NEXT: core.StackAddressEscape
// CHECK-NEXT: core.UndefinedBinaryOperatorResult
// CHECK-NEXT: core.VLASize
More information about the cfe-commits
mailing list