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.alva...@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.alva...@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.alva...@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 <benicsbal...@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.alva...@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 <benicsbal...@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.alva...@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.alva...@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.alva...@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 <benicsbal...@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.alva...@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.alva...@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.alva...@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.alva...@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.alva...@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.alva...@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.alva...@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, _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits