https://github.com/bozicrHT updated https://github.com/llvm/llvm-project/pull/204337
From dd12a31fc6ce5d035a12258c29072faa4b13fdca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Bo=C5=BEi=C4=87?= <[email protected]> Date: Wed, 17 Jun 2026 13:17:28 +0200 Subject: [PATCH 1/3] [Clang][Sema] Add fortify warnings for fread, fwrite, and fgets --- clang/include/clang/Basic/Builtins.td | 5 +++ .../clang/Basic/DiagnosticSemaKinds.td | 5 +++ clang/lib/Sema/SemaChecking.cpp | 38 ++++++++++++++++++- clang/test/Sema/warn-fortify-source.c | 14 +++++++ 4 files changed, 61 insertions(+), 1 deletion(-) diff --git a/clang/include/clang/Basic/Builtins.td b/clang/include/clang/Basic/Builtins.td index 0aec57f201301..26faebe9b4343 100644 --- a/clang/include/clang/Basic/Builtins.td +++ b/clang/include/clang/Basic/Builtins.td @@ -3570,6 +3570,11 @@ def Fwrite : LibBuiltin<"stdio.h"> { let Prototype = "size_t(void const*, size_t, size_t, FILE*)"; } +def Fgets : LibBuiltin<"stdio.h"> { + let Spellings = ["fgets"]; + let Prototype = "char*(char*, int, FILE*)"; +} + // C99 ctype.h def IsAlNum : LibBuiltin<"ctype.h"> { diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index cfb2ee3368201..16f08ebd2e006 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -1026,6 +1026,11 @@ def warn_fortify_scanf_overflow : Warning< "%2, but the corresponding specifier may require size %3">, InGroup<FortifySource>; +def warn_fortify_source_overread : Warning< + "'%0' will always read past the source buffer; source buffer has " + "size %1, but size argument is %2">, + InGroup<FortifySource>; + def err_function_start_invalid_type: Error< "argument must be a function">; diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp index 5c831e6cdebce..a7a8a82f00d23 100644 --- a/clang/lib/Sema/SemaChecking.cpp +++ b/clang/lib/Sema/SemaChecking.cpp @@ -1219,6 +1219,25 @@ class FortifiedBufferChecker { return std::nullopt; } + std::optional<llvm::APSInt> + ComputeExplicitObjectSizeArgumentProduct(unsigned LIndex, unsigned RIndex) { + auto L = ComputeExplicitObjectSizeArgument(LIndex); + auto R = ComputeExplicitObjectSizeArgument(RIndex); + if (!L || !R) + return std::nullopt; + + unsigned W = + 2 * std::max({L->getBitWidth(), R->getBitWidth(), SizeTypeWidth}); + + llvm::APSInt LE = L->extOrTrunc(W); + llvm::APSInt RE = R->extOrTrunc(W); + + LE.setIsUnsigned(true); + RE.setIsUnsigned(true); + + return LE * RE; + }; + std::optional<llvm::APSInt> ComputeStrLenArgument(unsigned Index) { std::optional<unsigned> IndexOptional = TranslateIndex(Index); if (!IndexOptional) @@ -1496,7 +1515,24 @@ void Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD, Checker.checkSourceOverread(/*SrcArgIdx=*/0, /*SizeArgIdx=*/2); break; } - + case Builtin::BIfread: { + DiagID = diag::warn_fortify_source_overflow; + SourceSize = Checker.ComputeExplicitObjectSizeArgumentProduct(1, 2); + DestinationSize = Checker.ComputeSizeArgument(0); + break; + } + case Builtin::BIfwrite: { + DiagID = diag::warn_fortify_source_overread; + SourceSize = Checker.ComputeExplicitObjectSizeArgumentProduct(1, 2); + DestinationSize = Checker.ComputeSizeArgument(0); + break; + } + case Builtin::BIfgets: { + DiagID = diag::warn_fortify_source_size_mismatch; + SourceSize = Checker.ComputeExplicitObjectSizeArgument(1); + DestinationSize = Checker.ComputeSizeArgument(0); + break; + } // memchr(buf, val, size) case Builtin::BImemchr: case Builtin::BI__builtin_memchr: { diff --git a/clang/test/Sema/warn-fortify-source.c b/clang/test/Sema/warn-fortify-source.c index 0a6c44f59af9e..0227d6703cfda 100644 --- a/clang/test/Sema/warn-fortify-source.c +++ b/clang/test/Sema/warn-fortify-source.c @@ -9,6 +9,8 @@ // RUN: %clang_cc1 -xc++ -triple x86_64-apple-macosx10.14.0 %s -verify -DUSE_BUILTINS -fexperimental-new-constant-interpreter typedef unsigned long size_t; +typedef struct _IO_FILE FILE; + #ifdef __cplusplus extern "C" { @@ -23,6 +25,10 @@ void *memcpy(void *dst, const void *src, size_t c); #endif void bcopy(const void *src, void *dst, size_t n); void bzero(void *dst, size_t n); +size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); +size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream); +char *fgets(char *s, int size, FILE *stream); + #ifdef __cplusplus } @@ -120,6 +126,14 @@ void call_bcopy_bzero(void) { __builtin_bzero(dst, 11); // expected-warning {{'bzero' will always overflow; destination buffer has size 10, but size argument is 11}} } +void call_fread_fwrite_fgets(FILE *fp) { + char src[4]; + fread(src, 2, 3, fp); // expected-warning {{'fread' will always overflow; destination buffer has size 4, but size argument is 6}} + fwrite(src, 2, 3, fp); // expected-warning {{'fwrite' will always read past the source buffer; source buffer has size 4, but size argument is 6}} + fgets(src, 5, fp); // expected-warning {{'fgets' size argument is too large; destination buffer has size 4, but size argument is 5}} + +} + void call_snprintf(double d, int n) { char buf[10]; __builtin_snprintf(buf, 10, "merp"); From 56ae89c1768cb853f938507daee113e1eb2eeacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Bo=C5=BEi=C4=87?= <[email protected]> Date: Wed, 17 Jun 2026 20:04:55 +0200 Subject: [PATCH 2/3] Fix failing tests --- clang/test/Analysis/std-c-library-functions-arg-constraints.c | 2 ++ clang/test/Analysis/stream-noopen.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/clang/test/Analysis/std-c-library-functions-arg-constraints.c b/clang/test/Analysis/std-c-library-functions-arg-constraints.c index 2cefa80341fc4..646bfff7d9e4b 100644 --- a/clang/test/Analysis/std-c-library-functions-arg-constraints.c +++ b/clang/test/Analysis/std-c-library-functions-arg-constraints.c @@ -248,6 +248,8 @@ void ARR38_C_F(FILE *file) { // report-warning{{The 1st argument to 'fread' is a buffer with size 4096 but should be a buffer with size equal to or greater than the value of the 2nd argument (which is 4) times the 3rd argument (which is 4096)}} \ // bugpath-warning{{The 1st argument to 'fread' is a buffer with size 4096 but should be a buffer with size equal to or greater than the value of the 2nd argument (which is 4) times the 3rd argument (which is 4096)}} \ // bugpath-note{{The 1st argument to 'fread' is a buffer with size 4096 but should be a buffer with size equal to or greater than the value of the 2nd argument (which is 4) times the 3rd argument (which is 4096)}} + // report-warning@-4{{'fread' will always overflow; destination buffer has size 4096, but size argument is 16384}} + // bugpath-warning@-5{{'fread' will always overflow; destination buffer has size 4096, but size argument is 16384}} } int __two_constrained_args(int, int); diff --git a/clang/test/Analysis/stream-noopen.c b/clang/test/Analysis/stream-noopen.c index 87761b3afb76b..3f291b6164596 100644 --- a/clang/test/Analysis/stream-noopen.c +++ b/clang/test/Analysis/stream-noopen.c @@ -100,11 +100,13 @@ void test_fgets(char *Buf, int N, FILE *F) { char Buf1[10]; Ret = fgets(Buf1, 11, F); // expected-warning {{The 1st argument to 'fgets' is a buffer with size 10}} + // expected-warning@-1 {{'fgets' size argument is too large; destination buffer has size 10, but size argument is 11}} } void test_fgets_bufsize(FILE *F) { char Buf[10]; fgets(Buf, 11, F); // expected-warning {{The 1st argument to 'fgets' is a buffer with size 10}} + // expected-warning@-1 {{'fgets' size argument is too large; destination buffer has size 10, but size argument is 11}} } void test_fputs(char *Buf, FILE *F) { From 577356d9ceb8018d6dcffcf5a46369efa0dd4486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Radovan=20Bo=C5=BEi=C4=87?= <[email protected]> Date: Mon, 31 Aug 2026 15:58:57 +0200 Subject: [PATCH 3/3] Fix signed integer evaluation for fgets fortify checks Factor integer argument evaluation into `EvaluateIntegerArgument` so that `fgets` signed int size parameter can be handled without violating the `size_t` invariant of `ComputeExplicitObjectSizeArgument`. --- clang/docs/ReleaseNotes.md | 3 ++ .../clang/Basic/DiagnosticSemaKinds.td | 4 +-- clang/lib/Sema/SemaChecking.cpp | 29 ++++++++++++------- clang/test/Sema/warn-fortify-source.c | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md index a96b0af93ab0b..1b373e715914b 100644 --- a/clang/docs/ReleaseNotes.md +++ b/clang/docs/ReleaseNotes.md @@ -391,6 +391,9 @@ features cannot lower the translation-unit ABI level; - Diagnostics for the C++11 range-based for statement now report the correct iterator type in notes for invalid iterator types. +- `-Wfortify-source` now diagnoses calls to `fread`, `fwrite`, and `fgets` + when the requested size exceeds the corresponding buffer. (#GH204337) + - `-Wfortify-source` now warns when the constant-evaluated argument to `umask` has bits set outside `0777`. Those bits are silently discarded by the kernel, so setting them is almost always a typo (matching the diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 16f08ebd2e006..78e6fb5357e22 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -1027,8 +1027,8 @@ def warn_fortify_scanf_overflow : Warning< InGroup<FortifySource>; def warn_fortify_source_overread : Warning< - "'%0' will always read past the source buffer; source buffer has " - "size %1, but size argument is %2">, + "'%0' will always read past the end of the source buffer; source buffer has " + "size %1, but the size is %2">, InGroup<FortifySource>; def err_function_start_invalid_type: Error< diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp index a7a8a82f00d23..34002436c7a1d 100644 --- a/clang/lib/Sema/SemaChecking.cpp +++ b/clang/lib/Sema/SemaChecking.cpp @@ -1174,18 +1174,28 @@ class FortifiedBufferChecker { return NewIndex; } - std::optional<llvm::APSInt> - ComputeExplicitObjectSizeArgument(unsigned Index) { + /// Evaluate the argument at Index as an integer constant while preserving + /// its signedness, or return std::nullopt if it cannot be evaluated. + std::optional<llvm::APSInt> EvaluateIntegerArgument(unsigned Index) { std::optional<unsigned> IndexOptional = TranslateIndex(Index); if (!IndexOptional) return std::nullopt; - unsigned NewIndex = *IndexOptional; + Expr::EvalResult Result; - Expr *SizeArg = TheCall->getArg(NewIndex); - if (!SizeArg->EvaluateAsInt(Result, S.getASTContext())) + Expr *Arg = TheCall->getArg(*IndexOptional); + if (!Arg->EvaluateAsInt(Result, S.getASTContext())) return std::nullopt; - llvm::APSInt Integer = Result.Val.getInt(); - assert(Integer.isUnsigned() && + + return Result.Val.getInt(); + } + + std::optional<llvm::APSInt> + ComputeExplicitObjectSizeArgument(unsigned Index) { + std::optional<llvm::APSInt> Integer = EvaluateIntegerArgument(Index); + if (!Integer) + return std::nullopt; + + assert(Integer->isUnsigned() && "size arg should be unsigned after implicit conversion to size_t"); return Integer; } @@ -1232,9 +1242,6 @@ class FortifiedBufferChecker { llvm::APSInt LE = L->extOrTrunc(W); llvm::APSInt RE = R->extOrTrunc(W); - LE.setIsUnsigned(true); - RE.setIsUnsigned(true); - return LE * RE; }; @@ -1529,7 +1536,7 @@ void Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD, } case Builtin::BIfgets: { DiagID = diag::warn_fortify_source_size_mismatch; - SourceSize = Checker.ComputeExplicitObjectSizeArgument(1); + SourceSize = Checker.EvaluateIntegerArgument(1); DestinationSize = Checker.ComputeSizeArgument(0); break; } diff --git a/clang/test/Sema/warn-fortify-source.c b/clang/test/Sema/warn-fortify-source.c index 0227d6703cfda..17dbc4ecde2fa 100644 --- a/clang/test/Sema/warn-fortify-source.c +++ b/clang/test/Sema/warn-fortify-source.c @@ -129,7 +129,7 @@ void call_bcopy_bzero(void) { void call_fread_fwrite_fgets(FILE *fp) { char src[4]; fread(src, 2, 3, fp); // expected-warning {{'fread' will always overflow; destination buffer has size 4, but size argument is 6}} - fwrite(src, 2, 3, fp); // expected-warning {{'fwrite' will always read past the source buffer; source buffer has size 4, but size argument is 6}} + fwrite(src, 2, 3, fp); // expected-warning {{'fwrite' will always read past the end of the source buffer; source buffer has size 4, but the size is 6}} fgets(src, 5, fp); // expected-warning {{'fgets' size argument is too large; destination buffer has size 4, but size argument is 5}} } _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
