[clang] [clang][analyzer] Model more getline/getdelim pre and postconditions (PR #83027)
Alejandro Álvarez Ayllón via cfe-commits
cfe-commits at lists.llvm.org
Mon Mar 11 08:11:10 PDT 2024
https://github.com/alejandro-alvarez-sonarsource updated https://github.com/llvm/llvm-project/pull/83027
>From 5c919832f9176d4b1af1312a4ee7cf30b788958a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Wed, 21 Feb 2024 14:46:01 +0100
Subject: [PATCH 01/14] [clang][analyzer] Model getline/getdelim preconditions
1. lineptr, n and stream can not be NULL
2. if *lineptr is NULL, *n must be 0
---
.../StaticAnalyzer/Checkers/StreamChecker.cpp | 155 ++++++++-
clang/test/Analysis/getline-stream.c | 327 ++++++++++++++++++
2 files changed, 480 insertions(+), 2 deletions(-)
create mode 100644 clang/test/Analysis/getline-stream.c
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 2ec47bf55df76b..bacac7613f880c 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -21,6 +21,7 @@
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h"
+#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/Sequence.h"
#include <functional>
#include <optional>
@@ -234,6 +235,9 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
BugType BT_StreamEof{this, "Stream already in EOF", "Stream handling error"};
BugType BT_ResourceLeak{this, "Resource leak", "Stream handling error",
/*SuppressOnSink =*/true};
+ BugType BT_SizeNull{this, "NULL size pointer", "Stream handling error"};
+ BugType BT_SizeNotZero{this, "NULL line pointer and size not zero",
+ "Stream handling error"};
public:
void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
@@ -346,10 +350,10 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
{std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, false),
std::bind(&StreamChecker::evalUngetc, _1, _2, _3, _4), 1}},
{{{"getdelim"}, 4},
- {std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, true),
+ {std::bind(&StreamChecker::preGetdelim, _1, _2, _3, _4),
std::bind(&StreamChecker::evalGetdelim, _1, _2, _3, _4), 3}},
{{{"getline"}, 3},
- {std::bind(&StreamChecker::preReadWrite, _1, _2, _3, _4, true),
+ {std::bind(&StreamChecker::preGetdelim, _1, _2, _3, _4),
std::bind(&StreamChecker::evalGetdelim, _1, _2, _3, _4), 2}},
{{{"fseek"}, 3},
{&StreamChecker::preFseek, &StreamChecker::evalFseek, 0}},
@@ -436,6 +440,9 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
void evalUngetc(const FnDescription *Desc, const CallEvent &Call,
CheckerContext &C) const;
+ void preGetdelim(const FnDescription *Desc, const CallEvent &Call,
+ CheckerContext &C) const;
+
void evalGetdelim(const FnDescription *Desc, const CallEvent &Call,
CheckerContext &C) const;
@@ -510,6 +517,14 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
ProgramStateRef ensureFseekWhenceCorrect(SVal WhenceVal, CheckerContext &C,
ProgramStateRef State) const;
+ ProgramStateRef ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
+ CheckerContext &C, ProgramStateRef State,
+ const StringRef PtrDescr) const;
+ ProgramStateRef
+ ensureSizeZeroIfLineNull(SVal LinePtrPtrSVal, SVal SizePtrSVal,
+ const Expr *LinePtrPtrExpr, const Expr *SizePtrExpr,
+ CheckerContext &C, ProgramStateRef State) const;
+
/// Generate warning about stream in EOF state.
/// There will be always a state transition into the passed State,
/// by the new non-fatal error node or (if failed) a normal transition,
@@ -1158,6 +1173,123 @@ void StreamChecker::evalUngetc(const FnDescription *Desc, const CallEvent &Call,
C.addTransition(StateFailed);
}
+ProgramStateRef
+StreamChecker::ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
+ CheckerContext &C, ProgramStateRef State,
+ const StringRef PtrDescr) const {
+ const auto Ptr = PtrVal.getAs<DefinedSVal>();
+ if (!Ptr)
+ return nullptr;
+
+ assert(PtrExpr && "Expected an argument");
+
+ const auto [PtrNotNull, PtrNull] = State->assume(*Ptr);
+ if (!PtrNotNull && PtrNull) {
+ if (ExplodedNode *N = C.generateErrorNode(PtrNull)) {
+ SmallString<256> buf;
+ llvm::raw_svector_ostream os(buf);
+ os << PtrDescr << " pointer might be NULL.";
+
+ auto R = std::make_unique<PathSensitiveBugReport>(BT_SizeNull, buf, N);
+ bugreporter::trackExpressionValue(N, PtrExpr, *R);
+ C.emitReport(std::move(R));
+ }
+ return nullptr;
+ }
+
+ return PtrNotNull;
+}
+
+ProgramStateRef StreamChecker::ensureSizeZeroIfLineNull(
+ SVal LinePtrPtrSVal, SVal SizePtrSVal, const Expr *LinePtrPtrExpr,
+ const Expr *SizePtrExpr, CheckerContext &C, ProgramStateRef State) const {
+ static constexpr char SizeNotZeroMsg[] =
+ "Line pointer might be null while n value is not zero";
+
+ // We have a pointer to a pointer to the buffer, and a pointer to the size.
+ // We want what they point at.
+ auto LinePtrSVal = getPointeeDefVal(LinePtrPtrSVal, State);
+ auto NSVal = getPointeeDefVal(SizePtrSVal, State);
+ if (!LinePtrSVal || !NSVal)
+ return nullptr;
+
+ assert(LinePtrPtrExpr &&
+ "Expected an argument with a pointer to a pointer to the buffer.");
+ assert(SizePtrExpr &&
+ "Expected an argument with a pointer to the buffer size.");
+
+ // If the line pointer is null, and n is > 0, there is UB.
+ const auto [LinePtrNotNull, LinePtrNull] = State->assume(*LinePtrSVal);
+ if (LinePtrNull && !LinePtrNotNull) {
+ const auto [NIsNotZero, NIsZero] = LinePtrNull->assume(*NSVal);
+ if (NIsNotZero && !NIsZero) {
+ if (ExplodedNode *N = C.generateErrorNode(NIsNotZero)) {
+ auto R = std::make_unique<PathSensitiveBugReport>(BT_SizeNotZero,
+ SizeNotZeroMsg, N);
+ bugreporter::trackExpressionValue(N, SizePtrExpr, *R);
+ bugreporter::trackExpressionValue(N, LinePtrPtrExpr, *R);
+ C.emitReport(std::move(R));
+ }
+ return nullptr;
+ }
+ return NIsZero;
+ }
+ return LinePtrNotNull;
+}
+
+void StreamChecker::preGetdelim(const FnDescription *Desc,
+ const CallEvent &Call,
+ CheckerContext &C) const {
+ ProgramStateRef State = C.getState();
+ SVal StreamVal = getStreamArg(Desc, Call);
+
+ auto AddTransitionOnReturn = llvm::make_scope_exit([&] {
+ if (State != nullptr) {
+ C.addTransition(State);
+ }
+ });
+
+ State = ensureStreamNonNull(StreamVal, Call.getArgExpr(Desc->StreamArgNo), C,
+ State);
+ if (!State)
+ return;
+ State = ensureStreamOpened(StreamVal, C, State);
+ if (!State)
+ return;
+ State = ensureNoFilePositionIndeterminate(StreamVal, C, State);
+ if (!State)
+ return;
+
+ // n must not be NULL
+ SVal SizePtrSval = Call.getArgSVal(1);
+ State = ensurePtrNotNull(SizePtrSval, Call.getArgExpr(1), C, State, "Size");
+ if (!State)
+ return;
+
+ // lineptr must not be NULL
+ SVal LinePtrPtrSVal = Call.getArgSVal(0);
+ State =
+ ensurePtrNotNull(LinePtrPtrSVal, Call.getArgExpr(0), C, State, "Line");
+ if (!State)
+ return;
+
+ // If lineptr points to a NULL pointer, *n must be 0
+ State =
+ ensureSizeZeroIfLineNull(LinePtrPtrSVal, SizePtrSval, Call.getArgExpr(0),
+ Call.getArgExpr(1), C, State);
+ if (!State)
+ return;
+
+ SymbolRef Sym = StreamVal.getAsSymbol();
+ if (Sym && State->get<StreamMap>(Sym)) {
+ const StreamState *SS = State->get<StreamMap>(Sym);
+ if (SS->ErrorState & ErrorFEof)
+ reportFEofWarning(Sym, C, State);
+ } else {
+ C.addTransition(State);
+ }
+}
+
void StreamChecker::evalGetdelim(const FnDescription *Desc,
const CallEvent &Call,
CheckerContext &C) const {
@@ -1183,6 +1315,20 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
State->BindExpr(E.CE, C.getLocationContext(), RetVal);
StateNotFailed =
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal, E.getZeroVal(Call));
+ // The buffer size *n must be enough to hold the whole line, and
+ // greater than the return value, since it has to account for \0
+ auto SizePtrSval = Call.getArgSVal(1);
+ auto NVal = getPointeeDefVal(SizePtrSval, State);
+ if (NVal) {
+ StateNotFailed = StateNotFailed->assume(
+ E.SVB
+ .evalBinOp(StateNotFailed, BinaryOperator::Opcode::BO_GT, *NVal,
+ RetVal, E.SVB.getConditionType())
+ .castAs<DefinedOrUnknownSVal>(),
+ true);
+ StateNotFailed =
+ StateNotFailed->BindExpr(E.CE, C.getLocationContext(), RetVal);
+ }
if (!StateNotFailed)
return;
C.addTransition(StateNotFailed);
@@ -1196,6 +1342,11 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
E.isStreamEof() ? ErrorFEof : ErrorFEof | ErrorFError;
StateFailed = E.setStreamState(
StateFailed, StreamState::getOpened(Desc, NewES, !NewES.isFEof()));
+ // On failure, the content of the buffer is undefined
+ if (auto NewLinePtr = getPointeeDefVal(Call.getArgSVal(0), State)) {
+ StateFailed = StateFailed->bindLoc(*NewLinePtr, UndefinedVal(),
+ C.getLocationContext());
+ }
C.addTransition(StateFailed, E.getFailureNoteTag(this, C));
}
diff --git a/clang/test/Analysis/getline-stream.c b/clang/test/Analysis/getline-stream.c
new file mode 100644
index 00000000000000..b4f389e4505f73
--- /dev/null
+++ b/clang/test/Analysis/getline-stream.c
@@ -0,0 +1,327 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.unix.Stream,debug.ExprInspection -verify %s
+
+#include "Inputs/system-header-simulator.h"
+#include "Inputs/system-header-simulator-for-malloc.h"
+#include "Inputs/system-header-simulator-for-valist.h"
+
+void clang_analyzer_eval(int);
+void clang_analyzer_dump_int(int);
+extern void clang_analyzer_dump_ptr(void*);
+extern void clang_analyzer_warnIfReached();
+
+void test_getline_null_file() {
+ char *buffer = NULL;
+ size_t n = 0;
+ getline(&buffer, &n, NULL); // expected-warning {{Stream pointer might be NULL}}
+}
+
+void test_getline_null_lineptr() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+
+ char **buffer = NULL;
+ size_t n = 0;
+ getline(buffer, &n, F1); // expected-warning {{Line pointer might be NULL}}
+ fclose(F1);
+}
+
+void test_getline_null_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ getline(&buffer, NULL, F1); // expected-warning {{Size pointer might be NULL}}
+ fclose(F1);
+}
+
+void test_getline_null_buffer_bad_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ size_t n = 8;
+ getline(&buffer, &n, F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ fclose(F1);
+}
+
+void test_getline_null_buffer_bad_size_2(size_t n) {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ if (n > 0) {
+ getline(&buffer, &n, F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ }
+ fclose(F1);
+}
+
+void test_getline_null_buffer_unknown_size(size_t n) {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+
+ getline(&buffer, &n, F1); // ok
+ fclose(F1);
+ free(buffer);
+}
+
+void test_getline_null_buffer() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ size_t n = 0;
+ ssize_t r = getline(&buffer, &n, F1);
+ // getline returns -1 on failure, number of char reads on success (>= 0)
+ if (r < -1) {
+ clang_analyzer_warnIfReached(); // must not happen
+ } else {
+ // The buffer could be allocated both on failure and success
+ clang_analyzer_dump_int(n); // expected-warning {{conj_$}}
+ clang_analyzer_dump_ptr(buffer); // expected-warning {{conj_$}}
+ }
+ free(buffer);
+ fclose(F1);
+}
+
+void test_getdelim_null_file() {
+ char *buffer = NULL;
+ size_t n = 0;
+ getdelim(&buffer, &n, '\n', NULL); // expected-warning {{Stream pointer might be NULL}}
+}
+
+void test_getdelim_null_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ getdelim(&buffer, NULL, ',', F1); // expected-warning {{Size pointer might be NULL}}
+ fclose(F1);
+}
+
+void test_getdelim_null_buffer_bad_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ size_t n = 8;
+ getdelim(&buffer, &n, ';', F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ fclose(F1);
+}
+
+void test_getdelim_null_buffer_bad_size_2(size_t n) {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ if (n > 0) {
+ getdelim(&buffer, &n, ' ', F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ }
+ fclose(F1);
+}
+
+void test_getdelim_null_buffer_unknown_size(size_t n) {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ getdelim(&buffer, &n, '-', F1); // ok
+ fclose(F1);
+ free(buffer);
+}
+
+void test_getdelim_null_buffer() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+ char *buffer = NULL;
+ size_t n = 0;
+ ssize_t r = getdelim(&buffer, &n, '\r', F1);
+ // getdelim returns -1 on failure, number of char reads on success (>= 0)
+ if (r < -1) {
+ clang_analyzer_warnIfReached(); // must not happen
+ }
+ else {
+ // The buffer could be allocated both on failure and success
+ clang_analyzer_dump_int(n); // expected-warning {{conj_$}}
+ clang_analyzer_dump_ptr(buffer); // expected-warning {{conj_$}}
+ }
+ free(buffer);
+ fclose(F1);
+}
+
+void test_getline_while() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ char *line = NULL;
+ size_t len = 0;
+ ssize_t read;
+
+ while ((read = getline(&line, &len, file)) != -1) {
+ printf("%s\n", line);
+ }
+
+ free(line);
+ fclose(file);
+}
+
+void test_getline_no_return_check() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ char *line = NULL;
+ size_t len = 0;
+ getline(&line, &len, file);
+
+ if (line[0] == '\0') {} // expected-warning {{The left operand of '==' is a garbage value}}
+
+ free(line);
+ fclose(file);
+}
+
+void test_getline_return_check() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ char *line = NULL;
+ size_t len = 0;
+ ssize_t r = getline(&line, &len, file);
+
+ if (r != -1) {
+ if (line[0] == '\0') {} // ok
+ }
+ free(line);
+ fclose(file);
+}
+
+void test_getline_feof_check() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ char *line = NULL;
+ size_t len = 0;
+ ssize_t r = getline(&line, &len, file);
+
+ if (r != -1) {
+ // success, end-of-file is not possible
+ int f = feof(file);
+ clang_analyzer_eval(f == 0); // expected-warning {{TRUE}}
+ } else {
+ // failure, end-of-file is possible, but not the only reason to fail
+ int f = feof(file);
+ clang_analyzer_eval(f == 0); // expected-warning {{TRUE}} \\
+ expected-warning {{FALSE}}
+ }
+ free(line);
+ fclose(file);
+}
+
+void test_getline_after_eof() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ size_t n = 10;
+ char *buffer = malloc(n);
+ ssize_t read = fread(buffer, n, 1, file);
+ if (!feof(file)) {
+ getline(&buffer, &n, file); // expected-warning {{File position of the stream might be 'indeterminate' after a failed operation. Can cause undefined behavior}}
+ }
+ fclose(file);
+ free(buffer);
+}
+
+void test_getline_feof() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ size_t n = 10;
+ char *buffer = malloc(n);
+ ssize_t read = fread(buffer, n, 1, file);
+ getline(&buffer, &n, file); // expected-warning {{File position of the stream might be 'indeterminate' after a failed operation. Can cause undefined behavior}} \\
+ expected-warning {{Read function called when stream is in EOF state. Function has no effect}}
+ fclose(file);
+ free(buffer);
+}
+
+void test_getline_clear_eof() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ size_t n = 10;
+ char *buffer = malloc(n);
+ ssize_t read = fread(buffer, n, 1, file);
+ if (feof(file)) {
+ clearerr(file);
+ getline(&buffer, &n, file); // ok
+ }
+ fclose(file);
+ free(buffer);
+}
+
+void test_getline_not_null(char **buffer, size_t *size) {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ getline(buffer, size, file);
+ fclose(file);
+
+ if (size == NULL || buffer == NULL) {
+ clang_analyzer_warnIfReached(); // must not happen
+ }
+}
+
+void test_getline_size_0(size_t size) {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ size_t old_size = size;
+ char *buffer = NULL;
+ ssize_t r = getline(&buffer, &size, file);
+ if (r >= 0) {
+ // Since buffer is NULL, old_size should be 0. Otherwise, there would be UB.
+ clang_analyzer_eval(old_size == 0); // expected-warning{{TRUE}}
+ }
+ fclose(file);
+ free(buffer);
+}
+
+void test_getline_ret_value() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ size_t n = 0;
+ char *buffer = NULL;
+ ssize_t r = getline(&buffer, &n, file);
+
+ if (r > -1) {
+ // The return value does *not* include the terminating null byte.
+ // The buffer must be large enough to include it.
+ clang_analyzer_eval(n > r); // expected-warning{{TRUE}}
+ }
+
+ fclose(file);
+ free(buffer);
+}
>From 414fd9bcbfefeca0d5f030ec759f5ddd1b2b8d78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Thu, 7 Mar 2024 09:55:00 +0100
Subject: [PATCH 02/14] Feedback from PR
---
.../Core/PathSensitive/CheckerHelpers.h | 1 -
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 12 ++++++------
clang/test/Analysis/getline-stream.c | 4 ++--
3 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h
index 60421e5437d82f..759caaf61b891c 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h
@@ -15,7 +15,6 @@
#include "ProgramState_Fwd.h"
#include "SVals.h"
-
#include "clang/AST/OperationKinds.h"
#include "clang/AST/Stmt.h"
#include "clang/Basic/OperatorKinds.h"
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index bacac7613f880c..92bcf66e5129db 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -1260,20 +1260,20 @@ void StreamChecker::preGetdelim(const FnDescription *Desc,
if (!State)
return;
- // n must not be NULL
+ // The parameter `n` must not be NULL.
SVal SizePtrSval = Call.getArgSVal(1);
State = ensurePtrNotNull(SizePtrSval, Call.getArgExpr(1), C, State, "Size");
if (!State)
return;
- // lineptr must not be NULL
+ // The parameter `lineptr` must not be NULL.
SVal LinePtrPtrSVal = Call.getArgSVal(0);
State =
ensurePtrNotNull(LinePtrPtrSVal, Call.getArgExpr(0), C, State, "Line");
if (!State)
return;
- // If lineptr points to a NULL pointer, *n must be 0
+ // If `lineptr` points to a NULL pointer, `*n` must be 0.
State =
ensureSizeZeroIfLineNull(LinePtrPtrSVal, SizePtrSval, Call.getArgExpr(0),
Call.getArgExpr(1), C, State);
@@ -1315,8 +1315,8 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
State->BindExpr(E.CE, C.getLocationContext(), RetVal);
StateNotFailed =
E.assumeBinOpNN(StateNotFailed, BO_GE, RetVal, E.getZeroVal(Call));
- // The buffer size *n must be enough to hold the whole line, and
- // greater than the return value, since it has to account for \0
+ // The buffer size `*n` must be enough to hold the whole line, and
+ // greater than the return value, since it has to account for '\0'.
auto SizePtrSval = Call.getArgSVal(1);
auto NVal = getPointeeDefVal(SizePtrSval, State);
if (NVal) {
@@ -1342,7 +1342,7 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
E.isStreamEof() ? ErrorFEof : ErrorFEof | ErrorFError;
StateFailed = E.setStreamState(
StateFailed, StreamState::getOpened(Desc, NewES, !NewES.isFEof()));
- // On failure, the content of the buffer is undefined
+ // On failure, the content of the buffer is undefined.
if (auto NewLinePtr = getPointeeDefVal(Call.getArgSVal(0), State)) {
StateFailed = StateFailed->bindLoc(*NewLinePtr, UndefinedVal(),
C.getLocationContext());
diff --git a/clang/test/Analysis/getline-stream.c b/clang/test/Analysis/getline-stream.c
index b4f389e4505f73..8677e35eb10d97 100644
--- a/clang/test/Analysis/getline-stream.c
+++ b/clang/test/Analysis/getline-stream.c
@@ -6,8 +6,8 @@
void clang_analyzer_eval(int);
void clang_analyzer_dump_int(int);
-extern void clang_analyzer_dump_ptr(void*);
-extern void clang_analyzer_warnIfReached();
+void clang_analyzer_dump_ptr(void*);
+void clang_analyzer_warnIfReached();
void test_getline_null_file() {
char *buffer = NULL;
>From 32dd108899522c5c4c7fa2c4ae7ed7127a71c5ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Thu, 7 Mar 2024 13:48:59 +0100
Subject: [PATCH 03/14] Update
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
Co-authored-by: Balazs Benics <benicsbalazs at gmail.com>
---
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 92bcf66e5129db..0a7618c75a834f 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -1186,11 +1186,7 @@ StreamChecker::ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
const auto [PtrNotNull, PtrNull] = State->assume(*Ptr);
if (!PtrNotNull && PtrNull) {
if (ExplodedNode *N = C.generateErrorNode(PtrNull)) {
- SmallString<256> buf;
- llvm::raw_svector_ostream os(buf);
- os << PtrDescr << " pointer might be NULL.";
-
- auto R = std::make_unique<PathSensitiveBugReport>(BT_SizeNull, buf, N);
+ auto R = std::make_unique<PathSensitiveBugReport>(BT_SizeNull, (PtrDescr + " pointer might be NULL.").str(), N);
bugreporter::trackExpressionValue(N, PtrExpr, *R);
C.emitReport(std::move(R));
}
>From 253cfa84b0a0e140ce9cbdd9626ce165d24bc9e4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Thu, 7 Mar 2024 13:49:14 +0100
Subject: [PATCH 04/14] Update
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
Co-authored-by: Balazs Benics <benicsbalazs at gmail.com>
---
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 2 --
1 file changed, 2 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 0a7618c75a834f..f028fff712bcaa 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -1281,8 +1281,6 @@ void StreamChecker::preGetdelim(const FnDescription *Desc,
const StreamState *SS = State->get<StreamMap>(Sym);
if (SS->ErrorState & ErrorFEof)
reportFEofWarning(Sym, C, State);
- } else {
- C.addTransition(State);
}
}
>From e957eb1ebeba8ed88661c112740fbefda9883073 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Thu, 7 Mar 2024 13:53:25 +0100
Subject: [PATCH 05/14] Fix formatting
---
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index f028fff712bcaa..fdca76b8359238 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -1186,7 +1186,8 @@ StreamChecker::ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
const auto [PtrNotNull, PtrNull] = State->assume(*Ptr);
if (!PtrNotNull && PtrNull) {
if (ExplodedNode *N = C.generateErrorNode(PtrNull)) {
- auto R = std::make_unique<PathSensitiveBugReport>(BT_SizeNull, (PtrDescr + " pointer might be NULL.").str(), N);
+ auto R = std::make_unique<PathSensitiveBugReport>(
+ BT_SizeNull, (PtrDescr + " pointer might be NULL.").str(), N);
bugreporter::trackExpressionValue(N, PtrExpr, *R);
C.emitReport(std::move(R));
}
>From 51d3a0d23cd6424cee55607294bc6be88a3ba7e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Thu, 7 Mar 2024 14:01:15 +0100
Subject: [PATCH 06/14] Add tests for a negative n parameter
---
clang/test/Analysis/getline-stream.c | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/clang/test/Analysis/getline-stream.c b/clang/test/Analysis/getline-stream.c
index 8677e35eb10d97..cc730c375e14e8 100644
--- a/clang/test/Analysis/getline-stream.c
+++ b/clang/test/Analysis/getline-stream.c
@@ -325,3 +325,28 @@ void test_getline_ret_value() {
fclose(file);
free(buffer);
}
+
+void test_getline_negative_buffer() {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ char *buffer = NULL;
+ size_t n = -1;
+ getline(&buffer, &n, file); // expected-warning {{Line pointer might be null while n value is not zero}}
+}
+
+void test_getline_negative_buffer_2(char *buffer) {
+ FILE *file = fopen("file.txt", "r");
+ if (file == NULL) {
+ return;
+ }
+
+ size_t n = -1;
+ ssize_t ret = getline(&buffer, &n, file);
+ if (ret > 0) {
+ clang_analyzer_eval(ret < n); // expected-warning {{TRUE}}
+ }
+ fclose(file);
+}
>From d1607c600c1017d872420b95c7a390a60fcc34ec Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Thu, 7 Mar 2024 14:03:39 +0100
Subject: [PATCH 07/14] Update
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
Co-authored-by: Balazs Benics <benicsbalazs at gmail.com>
---
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index fdca76b8359238..edf9152656defe 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -1317,8 +1317,7 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
if (NVal) {
StateNotFailed = StateNotFailed->assume(
E.SVB
- .evalBinOp(StateNotFailed, BinaryOperator::Opcode::BO_GT, *NVal,
- RetVal, E.SVB.getConditionType())
+ .evalBinOp(StateNotFailed, BO_GT, *NVal, RetVal, E.SVB.getConditionType())
.castAs<DefinedOrUnknownSVal>(),
true);
StateNotFailed =
>From 19a4c0f7bdc9732cf79284ced743369a443a3450 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Thu, 7 Mar 2024 14:07:00 +0100
Subject: [PATCH 08/14] Fix formatting
---
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index edf9152656defe..cc5bcb321d1e11 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -1317,7 +1317,8 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
if (NVal) {
StateNotFailed = StateNotFailed->assume(
E.SVB
- .evalBinOp(StateNotFailed, BO_GT, *NVal, RetVal, E.SVB.getConditionType())
+ .evalBinOp(StateNotFailed, BO_GT, *NVal, RetVal,
+ E.SVB.getConditionType())
.castAs<DefinedOrUnknownSVal>(),
true);
StateNotFailed =
>From 47c16010e13a1fb75b5243e56cc6bbd0cbb02de6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Mon, 11 Mar 2024 11:57:35 +0100
Subject: [PATCH 09/14] Rename ensureSizeZeroIfLineNull
---
.../StaticAnalyzer/Checkers/StreamChecker.cpp | 16 +++++++---------
1 file changed, 7 insertions(+), 9 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 4d2de55655115e..f2ded074a2090a 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -529,10 +529,9 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
ProgramStateRef ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
CheckerContext &C, ProgramStateRef State,
const StringRef PtrDescr) const;
- ProgramStateRef
- ensureSizeZeroIfLineNull(SVal LinePtrPtrSVal, SVal SizePtrSVal,
- const Expr *LinePtrPtrExpr, const Expr *SizePtrExpr,
- CheckerContext &C, ProgramStateRef State) const;
+ ProgramStateRef ensureGetdelimBufferAndSizeCorrect(
+ SVal LinePtrPtrSVal, SVal SizePtrSVal, const Expr *LinePtrPtrExpr,
+ const Expr *SizePtrExpr, CheckerContext &C, ProgramStateRef State) const;
/// Generate warning about stream in EOF state.
/// There will be always a state transition into the passed State,
@@ -1218,7 +1217,7 @@ StreamChecker::ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
return PtrNotNull;
}
-ProgramStateRef StreamChecker::ensureSizeZeroIfLineNull(
+ProgramStateRef StreamChecker::ensureGetdelimBufferAndSizeCorrect(
SVal LinePtrPtrSVal, SVal SizePtrSVal, const Expr *LinePtrPtrExpr,
const Expr *SizePtrExpr, CheckerContext &C, ProgramStateRef State) const {
static constexpr char SizeNotZeroMsg[] =
@@ -1291,10 +1290,9 @@ void StreamChecker::preGetdelim(const FnDescription *Desc,
if (!State)
return;
- // If `lineptr` points to a NULL pointer, `*n` must be 0.
- State =
- ensureSizeZeroIfLineNull(LinePtrPtrSVal, SizePtrSval, Call.getArgExpr(0),
- Call.getArgExpr(1), C, State);
+ State = ensureGetdelimBufferAndSizeCorrect(LinePtrPtrSVal, SizePtrSval,
+ Call.getArgExpr(0),
+ Call.getArgExpr(1), C, State);
if (!State)
return;
>From e9c7010a0a280d013daf748dc731e56b9ad019be Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Mon, 11 Mar 2024 11:59:35 +0100
Subject: [PATCH 10/14] Simplify assert
---
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index f2ded074a2090a..d57b0b5f657c2c 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -1230,10 +1230,7 @@ ProgramStateRef StreamChecker::ensureGetdelimBufferAndSizeCorrect(
if (!LinePtrSVal || !NSVal)
return nullptr;
- assert(LinePtrPtrExpr &&
- "Expected an argument with a pointer to a pointer to the buffer.");
- assert(SizePtrExpr &&
- "Expected an argument with a pointer to the buffer size.");
+ assert(LinePtrPtrExpr && SizePtrExpr);
// If the line pointer is null, and n is > 0, there is UB.
const auto [LinePtrNotNull, LinePtrNull] = State->assume(*LinePtrSVal);
>From 25e7bb3d905651533e88e09720a93f2d9e2b231c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Mon, 11 Mar 2024 12:02:26 +0100
Subject: [PATCH 11/14] Remove use of llvm::make_scope_exit
---
clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index d57b0b5f657c2c..360a26f3b5c097 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -21,7 +21,6 @@
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h"
-#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/Sequence.h"
#include <functional>
#include <optional>
@@ -1257,12 +1256,6 @@ void StreamChecker::preGetdelim(const FnDescription *Desc,
ProgramStateRef State = C.getState();
SVal StreamVal = getStreamArg(Desc, Call);
- auto AddTransitionOnReturn = llvm::make_scope_exit([&] {
- if (State != nullptr) {
- C.addTransition(State);
- }
- });
-
State = ensureStreamNonNull(StreamVal, Call.getArgExpr(Desc->StreamArgNo), C,
State);
if (!State)
@@ -1299,6 +1292,8 @@ void StreamChecker::preGetdelim(const FnDescription *Desc,
if (SS->ErrorState & ErrorFEof)
reportFEofWarning(Sym, C, State);
}
+
+ C.addTransition(State);
}
void StreamChecker::evalGetdelim(const FnDescription *Desc,
>From 9db5a4a261655c6825cf83c3ace545129060b7df Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Mon, 11 Mar 2024 14:16:10 +0100
Subject: [PATCH 12/14] Model getline/getdelim according to posix 2018
---
.../StaticAnalyzer/Checkers/StreamChecker.cpp | 49 ++++++----
clang/test/Analysis/getline-stream.c | 94 +++++++++++++++----
2 files changed, 109 insertions(+), 34 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 360a26f3b5c097..7d3e60a1fb983b 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -18,6 +18,7 @@
#include "clang/StaticAnalyzer/Core/PathSensitive/CallEvent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerContext.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h"
+#include "clang/StaticAnalyzer/Core/PathSensitive/DynamicExtent.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramState.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/ProgramStateTrait.h"
#include "clang/StaticAnalyzer/Core/PathSensitive/SymbolManager.h"
@@ -235,8 +236,9 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
BugType BT_ResourceLeak{this, "Resource leak", "Stream handling error",
/*SuppressOnSink =*/true};
BugType BT_SizeNull{this, "NULL size pointer", "Stream handling error"};
- BugType BT_SizeNotZero{this, "NULL line pointer and size not zero",
- "Stream handling error"};
+ BugType BT_SizeGreaterThanBufferSize{
+ this, "Size greater than the allocated buffer size",
+ categories::MemoryError};
public:
void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
@@ -1219,8 +1221,11 @@ StreamChecker::ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
ProgramStateRef StreamChecker::ensureGetdelimBufferAndSizeCorrect(
SVal LinePtrPtrSVal, SVal SizePtrSVal, const Expr *LinePtrPtrExpr,
const Expr *SizePtrExpr, CheckerContext &C, ProgramStateRef State) const {
- static constexpr char SizeNotZeroMsg[] =
- "Line pointer might be null while n value is not zero";
+ // If the argument `*n` is non-zero, `*lineptr` must point to an object of
+ // size at least `*n` bytes, or a `NULL` pointer.
+ static constexpr char SizeGreaterThanBufferSize[] =
+ "The buffer from the first argument is smaller than the size "
+ "specified by the second parameter";
// We have a pointer to a pointer to the buffer, and a pointer to the size.
// We want what they point at.
@@ -1231,23 +1236,31 @@ ProgramStateRef StreamChecker::ensureGetdelimBufferAndSizeCorrect(
assert(LinePtrPtrExpr && SizePtrExpr);
- // If the line pointer is null, and n is > 0, there is UB.
const auto [LinePtrNotNull, LinePtrNull] = State->assume(*LinePtrSVal);
- if (LinePtrNull && !LinePtrNotNull) {
- const auto [NIsNotZero, NIsZero] = LinePtrNull->assume(*NSVal);
- if (NIsNotZero && !NIsZero) {
- if (ExplodedNode *N = C.generateErrorNode(NIsNotZero)) {
- auto R = std::make_unique<PathSensitiveBugReport>(BT_SizeNotZero,
- SizeNotZeroMsg, N);
- bugreporter::trackExpressionValue(N, SizePtrExpr, *R);
- bugreporter::trackExpressionValue(N, LinePtrPtrExpr, *R);
- C.emitReport(std::move(R));
- }
- return nullptr;
+ if (LinePtrNotNull && !LinePtrNull) {
+ auto &SVB = C.getSValBuilder();
+ auto LineBufSize =
+ getDynamicExtent(LinePtrNotNull, LinePtrSVal->getAsRegion(), SVB);
+ auto LineBufSizeGtN = SVB.evalBinOp(LinePtrNotNull, BO_GE, LineBufSize,
+ *NSVal, SVB.getConditionType())
+ .getAs<DefinedOrUnknownSVal>();
+ if (!LineBufSizeGtN) {
+ return LinePtrNotNull;
+ }
+ if (auto LineBufSizeOk = LinePtrNotNull->assume(*LineBufSizeGtN, true)) {
+ return LineBufSizeOk;
}
- return NIsZero;
+
+ if (ExplodedNode *N = C.generateErrorNode(LinePtrNotNull)) {
+ auto R = std::make_unique<PathSensitiveBugReport>(
+ BT_SizeGreaterThanBufferSize, SizeGreaterThanBufferSize, N);
+ bugreporter::trackExpressionValue(N, SizePtrExpr, *R);
+ bugreporter::trackExpressionValue(N, LinePtrPtrExpr, *R);
+ C.emitReport(std::move(R));
+ }
+ return nullptr;
}
- return LinePtrNotNull;
+ return State;
}
void StreamChecker::preGetdelim(const FnDescription *Desc,
diff --git a/clang/test/Analysis/getline-stream.c b/clang/test/Analysis/getline-stream.c
index cc730c375e14e8..fb09f6006b4e70 100644
--- a/clang/test/Analysis/getline-stream.c
+++ b/clang/test/Analysis/getline-stream.c
@@ -1,4 +1,4 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=core,alpha.unix.Stream,debug.ExprInspection -verify %s
+// RUN: %clang_analyze_cc1 -analyzer-checker=core,unix,alpha.unix.Stream,debug.ExprInspection -verify %s
#include "Inputs/system-header-simulator.h"
#include "Inputs/system-header-simulator-for-malloc.h"
@@ -35,24 +35,26 @@ void test_getline_null_size() {
fclose(F1);
}
-void test_getline_null_buffer_bad_size() {
+void test_getline_null_buffer_size_gt0() {
FILE *F1 = tmpfile();
if (!F1)
return;
char *buffer = NULL;
size_t n = 8;
- getline(&buffer, &n, F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ getline(&buffer, &n, F1); // ok since posix 2018
+ free(buffer);
fclose(F1);
}
-void test_getline_null_buffer_bad_size_2(size_t n) {
+void test_getline_null_buffer_size_gt0_2(size_t n) {
FILE *F1 = tmpfile();
if (!F1)
return;
char *buffer = NULL;
if (n > 0) {
- getline(&buffer, &n, F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ getline(&buffer, &n, F1); // ok since posix 2018
}
+ free(buffer);
fclose(F1);
}
@@ -67,6 +69,58 @@ void test_getline_null_buffer_unknown_size(size_t n) {
free(buffer);
}
+void test_getline_null_buffer_undef_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+
+ char *buffer = NULL;
+ size_t n;
+
+ getline(&buffer, &n, F1); // ok since posix 2018
+ fclose(F1);
+ free(buffer);
+}
+
+void test_getline_buffer_size_0() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+
+ char *buffer = malloc(10);
+ size_t n = 0;
+ if (buffer != NULL)
+ getline(&buffer, &n, F1); // ok, the buffer is enough for 0 character
+ fclose(F1);
+ free(buffer);
+}
+
+void test_getline_buffer_bad_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+
+ char *buffer = malloc(10);
+ size_t n = 100;
+ if (buffer != NULL)
+ getline(&buffer, &n, F1); // expected-warning {{The buffer from the first argument is smaller than the size specified by the second parameter}}
+ fclose(F1);
+ free(buffer);
+}
+
+void test_getline_buffer_smaller_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+
+ char *buffer = malloc(100);
+ size_t n = 10;
+ if (buffer != NULL)
+ getline(&buffer, &n, F1); // ok, there is enough space for 10 characters
+ fclose(F1);
+ free(buffer);
+}
+
void test_getline_null_buffer() {
FILE *F1 = tmpfile();
if (!F1)
@@ -101,24 +155,26 @@ void test_getdelim_null_size() {
fclose(F1);
}
-void test_getdelim_null_buffer_bad_size() {
+void test_getdelim_null_buffer_size_gt0() {
FILE *F1 = tmpfile();
if (!F1)
return;
char *buffer = NULL;
size_t n = 8;
- getdelim(&buffer, &n, ';', F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ getdelim(&buffer, &n, ';', F1); // ok since posix 2018
+ free(buffer);
fclose(F1);
}
-void test_getdelim_null_buffer_bad_size_2(size_t n) {
+void test_getdelim_null_buffer_size_gt0_2(size_t n) {
FILE *F1 = tmpfile();
if (!F1)
return;
char *buffer = NULL;
if (n > 0) {
- getdelim(&buffer, &n, ' ', F1); // expected-warning {{Line pointer might be null while n value is not zero}}
+ getdelim(&buffer, &n, ' ', F1); // ok since posix 2018
}
+ free(buffer);
fclose(F1);
}
@@ -289,18 +345,21 @@ void test_getline_not_null(char **buffer, size_t *size) {
}
}
-void test_getline_size_0(size_t size) {
+void test_getline_size_constraint(size_t size) {
FILE *file = fopen("file.txt", "r");
if (file == NULL) {
return;
}
size_t old_size = size;
- char *buffer = NULL;
- ssize_t r = getline(&buffer, &size, file);
- if (r >= 0) {
- // Since buffer is NULL, old_size should be 0. Otherwise, there would be UB.
- clang_analyzer_eval(old_size == 0); // expected-warning{{TRUE}}
+ char *buffer = malloc(10);
+ if (buffer != NULL) {
+ ssize_t r = getline(&buffer, &size, file);
+ if (r >= 0) {
+ // Since buffer has a size of 10, old_size must be less than or equal to 10.
+ // Otherwise, there would be UB.
+ clang_analyzer_eval(old_size <= 10); // expected-warning{{TRUE}}
+ }
}
fclose(file);
free(buffer);
@@ -334,7 +393,9 @@ void test_getline_negative_buffer() {
char *buffer = NULL;
size_t n = -1;
- getline(&buffer, &n, file); // expected-warning {{Line pointer might be null while n value is not zero}}
+ getline(&buffer, &n, file); // ok since posix 2018
+ free(buffer);
+ fclose(file);
}
void test_getline_negative_buffer_2(char *buffer) {
@@ -348,5 +409,6 @@ void test_getline_negative_buffer_2(char *buffer) {
if (ret > 0) {
clang_analyzer_eval(ret < n); // expected-warning {{TRUE}}
}
+ free(buffer);
fclose(file);
}
>From f048c55e0cfc455b3bf43bb3ae8a92ae8d75a9c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Mon, 11 Mar 2024 15:16:59 +0100
Subject: [PATCH 13/14] Report on undefined size with non-null buffer
---
.../Core/PathSensitive/CheckerHelpers.h | 3 +-
.../StaticAnalyzer/Checkers/MallocChecker.cpp | 8 +--
.../StaticAnalyzer/Checkers/StreamChecker.cpp | 50 ++++++++++++-------
.../StaticAnalyzer/Core/CheckerHelpers.cpp | 5 +-
clang/test/Analysis/getline-stream.c | 14 ++++++
5 files changed, 54 insertions(+), 26 deletions(-)
diff --git a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h
index 759caaf61b891c..d053a97189123a 100644
--- a/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h
+++ b/clang/include/clang/StaticAnalyzer/Core/PathSensitive/CheckerHelpers.h
@@ -112,8 +112,7 @@ class OperatorKind {
OperatorKind operationKindFromOverloadedOperator(OverloadedOperatorKind OOK,
bool IsBinary);
-std::optional<DefinedSVal> getPointeeDefVal(SVal PtrSVal,
- ProgramStateRef State);
+std::optional<SVal> getPointeeVal(SVal PtrSVal, ProgramStateRef State);
} // namespace ento
diff --git a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
index 03cb7696707fe2..c2d96f59260906 100644
--- a/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/MallocChecker.cpp
@@ -1441,7 +1441,7 @@ void MallocChecker::preGetdelim(const CallEvent &Call,
return;
ProgramStateRef State = C.getState();
- const auto LinePtr = getPointeeDefVal(Call.getArgSVal(0), State);
+ const auto LinePtr = getPointeeVal(Call.getArgSVal(0), State);
if (!LinePtr)
return;
@@ -1470,8 +1470,10 @@ void MallocChecker::checkGetdelim(const CallEvent &Call,
SValBuilder &SVB = C.getSValBuilder();
- const auto LinePtr = getPointeeDefVal(Call.getArgSVal(0), State);
- const auto Size = getPointeeDefVal(Call.getArgSVal(1), State);
+ const auto LinePtr =
+ getPointeeVal(Call.getArgSVal(0), State)->getAs<DefinedSVal>();
+ const auto Size =
+ getPointeeVal(Call.getArgSVal(1), State)->getAs<DefinedSVal>();
if (!LinePtr || !Size || !LinePtr->getAsRegion())
return;
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 7d3e60a1fb983b..89b30dbcddc025 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -236,9 +236,7 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
BugType BT_ResourceLeak{this, "Resource leak", "Stream handling error",
/*SuppressOnSink =*/true};
BugType BT_SizeNull{this, "NULL size pointer", "Stream handling error"};
- BugType BT_SizeGreaterThanBufferSize{
- this, "Size greater than the allocated buffer size",
- categories::MemoryError};
+ BugType BT_IllegalSize{this, "Invalid buffer size", categories::MemoryError};
public:
void checkPreCall(const CallEvent &Call, CheckerContext &C) const;
@@ -1221,28 +1219,50 @@ StreamChecker::ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
ProgramStateRef StreamChecker::ensureGetdelimBufferAndSizeCorrect(
SVal LinePtrPtrSVal, SVal SizePtrSVal, const Expr *LinePtrPtrExpr,
const Expr *SizePtrExpr, CheckerContext &C, ProgramStateRef State) const {
- // If the argument `*n` is non-zero, `*lineptr` must point to an object of
- // size at least `*n` bytes, or a `NULL` pointer.
static constexpr char SizeGreaterThanBufferSize[] =
"The buffer from the first argument is smaller than the size "
"specified by the second parameter";
+ static constexpr char SizeUndef[] =
+ "The buffer from the first argument is not NULL, but the size specified "
+ "by the second parameter is undefined.";
+
+ auto EmitBugReport = [this, &C, SizePtrExpr,
+ LinePtrPtrExpr](const ProgramStateRef &BugState,
+ const char *ErrMsg) {
+ if (ExplodedNode *N = C.generateErrorNode(BugState)) {
+ auto R =
+ std::make_unique<PathSensitiveBugReport>(BT_IllegalSize, ErrMsg, N);
+ bugreporter::trackExpressionValue(N, SizePtrExpr, *R);
+ bugreporter::trackExpressionValue(N, LinePtrPtrExpr, *R);
+ C.emitReport(std::move(R));
+ }
+ };
// We have a pointer to a pointer to the buffer, and a pointer to the size.
// We want what they point at.
- auto LinePtrSVal = getPointeeDefVal(LinePtrPtrSVal, State);
- auto NSVal = getPointeeDefVal(SizePtrSVal, State);
- if (!LinePtrSVal || !NSVal)
+ auto LinePtrSVal = getPointeeVal(LinePtrPtrSVal, State)->getAs<DefinedSVal>();
+ auto NSVal = getPointeeVal(SizePtrSVal, State);
+ if (!LinePtrSVal || !NSVal || NSVal->isUnknown())
return nullptr;
assert(LinePtrPtrExpr && SizePtrExpr);
const auto [LinePtrNotNull, LinePtrNull] = State->assume(*LinePtrSVal);
if (LinePtrNotNull && !LinePtrNull) {
+ // If `*lineptr` is not null, but `*n` is undefined, there is UB.
+ if (NSVal->isUndef()) {
+ EmitBugReport(LinePtrNotNull, SizeUndef);
+ return nullptr;
+ }
+
+ // If it is defined, and known, its size must be less than or equal to
+ // the buffer size.
+ auto NDefSVal = NSVal->getAs<DefinedSVal>();
auto &SVB = C.getSValBuilder();
auto LineBufSize =
getDynamicExtent(LinePtrNotNull, LinePtrSVal->getAsRegion(), SVB);
auto LineBufSizeGtN = SVB.evalBinOp(LinePtrNotNull, BO_GE, LineBufSize,
- *NSVal, SVB.getConditionType())
+ *NDefSVal, SVB.getConditionType())
.getAs<DefinedOrUnknownSVal>();
if (!LineBufSizeGtN) {
return LinePtrNotNull;
@@ -1251,13 +1271,7 @@ ProgramStateRef StreamChecker::ensureGetdelimBufferAndSizeCorrect(
return LineBufSizeOk;
}
- if (ExplodedNode *N = C.generateErrorNode(LinePtrNotNull)) {
- auto R = std::make_unique<PathSensitiveBugReport>(
- BT_SizeGreaterThanBufferSize, SizeGreaterThanBufferSize, N);
- bugreporter::trackExpressionValue(N, SizePtrExpr, *R);
- bugreporter::trackExpressionValue(N, LinePtrPtrExpr, *R);
- C.emitReport(std::move(R));
- }
+ EmitBugReport(LinePtrNotNull, SizeGreaterThanBufferSize);
return nullptr;
}
return State;
@@ -1337,7 +1351,7 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
// The buffer size `*n` must be enough to hold the whole line, and
// greater than the return value, since it has to account for '\0'.
auto SizePtrSval = Call.getArgSVal(1);
- auto NVal = getPointeeDefVal(SizePtrSval, State);
+ auto NVal = getPointeeVal(SizePtrSval, State);
if (NVal) {
StateNotFailed = StateNotFailed->assume(
E.SVB
@@ -1362,7 +1376,7 @@ void StreamChecker::evalGetdelim(const FnDescription *Desc,
StateFailed = E.setStreamState(
StateFailed, StreamState::getOpened(Desc, NewES, !NewES.isFEof()));
// On failure, the content of the buffer is undefined.
- if (auto NewLinePtr = getPointeeDefVal(Call.getArgSVal(0), State)) {
+ if (auto NewLinePtr = getPointeeVal(Call.getArgSVal(0), State)) {
StateFailed = StateFailed->bindLoc(*NewLinePtr, UndefinedVal(),
C.getLocationContext());
}
diff --git a/clang/lib/StaticAnalyzer/Core/CheckerHelpers.cpp b/clang/lib/StaticAnalyzer/Core/CheckerHelpers.cpp
index 364c87e910b7b5..d7137a915b3d3d 100644
--- a/clang/lib/StaticAnalyzer/Core/CheckerHelpers.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CheckerHelpers.cpp
@@ -183,10 +183,9 @@ OperatorKind operationKindFromOverloadedOperator(OverloadedOperatorKind OOK,
}
}
-std::optional<DefinedSVal> getPointeeDefVal(SVal PtrSVal,
- ProgramStateRef State) {
+std::optional<SVal> getPointeeVal(SVal PtrSVal, ProgramStateRef State) {
if (const auto *Ptr = PtrSVal.getAsRegion()) {
- return State->getSVal(Ptr).getAs<DefinedSVal>();
+ return State->getSVal(Ptr);
}
return std::nullopt;
}
diff --git a/clang/test/Analysis/getline-stream.c b/clang/test/Analysis/getline-stream.c
index fb09f6006b4e70..996e8228847ab1 100644
--- a/clang/test/Analysis/getline-stream.c
+++ b/clang/test/Analysis/getline-stream.c
@@ -121,6 +121,20 @@ void test_getline_buffer_smaller_size() {
free(buffer);
}
+void test_getline_buffer_undef_size() {
+ FILE *F1 = tmpfile();
+ if (!F1)
+ return;
+
+ char *buffer = malloc(100);
+ size_t n;
+ if (buffer != NULL)
+ getline(&buffer, &n, F1); // expected-warning {{The buffer from the first argument is not NULL, but the size specified by the second parameter is undefined}}
+ fclose(F1);
+ free(buffer);
+}
+
+
void test_getline_null_buffer() {
FILE *F1 = tmpfile();
if (!F1)
>From 6d440b763b7b37e7714b4d9f73ffd4cce114d006 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20=C3=81lvarez=20Ayll=C3=B3n?=
<alejandro.alvarez at sonarsource.com>
Date: Mon, 11 Mar 2024 15:54:57 +0100
Subject: [PATCH 14/14] Refactor so ensureStreamNonNull calls ensurePtrNotNull
---
.../StaticAnalyzer/Checkers/StreamChecker.cpp | 65 +++++++------------
1 file changed, 24 insertions(+), 41 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
index 89b30dbcddc025..3be0500a4d8d6f 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StreamChecker.cpp
@@ -235,7 +235,7 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
BugType BT_StreamEof{this, "Stream already in EOF", "Stream handling error"};
BugType BT_ResourceLeak{this, "Resource leak", "Stream handling error",
/*SuppressOnSink =*/true};
- BugType BT_SizeNull{this, "NULL size pointer", "Stream handling error"};
+ BugType BT_ArgumentNull{this, "NULL pointer", categories::UnixAPI};
BugType BT_IllegalSize{this, "Invalid buffer size", categories::MemoryError};
public:
@@ -502,6 +502,12 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
CheckerContext &C,
ProgramStateRef State) const;
+ ProgramStateRef
+ ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr, CheckerContext &C,
+ ProgramStateRef State, const StringRef PtrDescr,
+ std::optional<std::reference_wrapper<const BugType>> BT =
+ std::nullopt) const;
+
/// Check that the stream is the opened state.
/// If the stream is known to be not opened an error is generated
/// and nullptr returned, otherwise the original state is returned.
@@ -525,9 +531,6 @@ class StreamChecker : public Checker<check::PreCall, eval::Call,
ProgramStateRef ensureFseekWhenceCorrect(SVal WhenceVal, CheckerContext &C,
ProgramStateRef State) const;
- ProgramStateRef ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
- CheckerContext &C, ProgramStateRef State,
- const StringRef PtrDescr) const;
ProgramStateRef ensureGetdelimBufferAndSizeCorrect(
SVal LinePtrPtrSVal, SVal SizePtrSVal, const Expr *LinePtrPtrExpr,
const Expr *SizePtrExpr, CheckerContext &C, ProgramStateRef State) const;
@@ -1192,30 +1195,6 @@ void StreamChecker::evalUngetc(const FnDescription *Desc, const CallEvent &Call,
C.addTransition(StateFailed);
}
-ProgramStateRef
-StreamChecker::ensurePtrNotNull(SVal PtrVal, const Expr *PtrExpr,
- CheckerContext &C, ProgramStateRef State,
- const StringRef PtrDescr) const {
- const auto Ptr = PtrVal.getAs<DefinedSVal>();
- if (!Ptr)
- return nullptr;
-
- assert(PtrExpr && "Expected an argument");
-
- const auto [PtrNotNull, PtrNull] = State->assume(*Ptr);
- if (!PtrNotNull && PtrNull) {
- if (ExplodedNode *N = C.generateErrorNode(PtrNull)) {
- auto R = std::make_unique<PathSensitiveBugReport>(
- BT_SizeNull, (PtrDescr + " pointer might be NULL.").str(), N);
- bugreporter::trackExpressionValue(N, PtrExpr, *R);
- C.emitReport(std::move(R));
- }
- return nullptr;
- }
-
- return PtrNotNull;
-}
-
ProgramStateRef StreamChecker::ensureGetdelimBufferAndSizeCorrect(
SVal LinePtrPtrSVal, SVal SizePtrSVal, const Expr *LinePtrPtrExpr,
const Expr *SizePtrExpr, CheckerContext &C, ProgramStateRef State) const {
@@ -1697,27 +1676,31 @@ ProgramStateRef
StreamChecker::ensureStreamNonNull(SVal StreamVal, const Expr *StreamE,
CheckerContext &C,
ProgramStateRef State) const {
- auto Stream = StreamVal.getAs<DefinedSVal>();
- if (!Stream)
- return State;
-
- ConstraintManager &CM = C.getConstraintManager();
+ return ensurePtrNotNull(StreamVal, StreamE, C, State, "Stream", BT_FileNull);
+}
- ProgramStateRef StateNotNull, StateNull;
- std::tie(StateNotNull, StateNull) = CM.assumeDual(State, *Stream);
+ProgramStateRef StreamChecker::ensurePtrNotNull(
+ SVal PtrVal, const Expr *PtrExpr, CheckerContext &C, ProgramStateRef State,
+ const StringRef PtrDescr,
+ std::optional<std::reference_wrapper<const BugType>> BT) const {
+ const auto Ptr = PtrVal.getAs<DefinedSVal>();
+ if (!Ptr)
+ return State;
- if (!StateNotNull && StateNull) {
- if (ExplodedNode *N = C.generateErrorNode(StateNull)) {
+ const auto [PtrNotNull, PtrNull] = State->assume(*Ptr);
+ if (!PtrNotNull && PtrNull) {
+ if (ExplodedNode *N = C.generateErrorNode(PtrNull)) {
auto R = std::make_unique<PathSensitiveBugReport>(
- BT_FileNull, "Stream pointer might be NULL.", N);
- if (StreamE)
- bugreporter::trackExpressionValue(N, StreamE, *R);
+ BT.value_or(std::cref(BT_ArgumentNull)),
+ (PtrDescr + " pointer might be NULL.").str(), N);
+ if (PtrExpr)
+ bugreporter::trackExpressionValue(N, PtrExpr, *R);
C.emitReport(std::move(R));
}
return nullptr;
}
- return StateNotNull;
+ return PtrNotNull;
}
ProgramStateRef StreamChecker::ensureStreamOpened(SVal StreamVal,
More information about the cfe-commits
mailing list