https://github.com/nuclearcat updated 
https://github.com/llvm/llvm-project/pull/196499

>From 4bc8453b9ddb4f99191239f115033da579367121 Mon Sep 17 00:00:00 2001
From: Colin Kinloch <[email protected]>
Date: Thu, 2 Oct 2025 22:01:40 +0100
Subject: [PATCH 1/8] [clang][Sema] Add fortify warnings for `unistd.h`

Define as builtin and check for overflows and over-reads in:
* `read`
* `write`
* `getcwd`
* `readlink`
* `readlinkat`

Also recognize `pread`/`pread64`/`pwrite`/`pwrite64` by name in the
fortify checker. They are deliberately not declared as builtins because
their prototypes use `off_t`, whose width is platform- and macro-dependent
(notably `_FILE_OFFSET_BITS`); a fixed builtin signature would clash with
the system header on some targets and silently disable fortify there.

It also enables `ssize_t` for use in builtin signatures.

Signed-off-by: Colin Kinloch <[email protected]>
---
 clang/include/clang/Basic/Builtins.td         |   2 +
 .../clang/Basic/DiagnosticSemaKinds.td        |   4 +
 clang/lib/AST/ASTContext.cpp                  |   9 +-
 clang/lib/Sema/SemaChecking.cpp               | 187 +++++++++++++++++-
 .../Sema/warn-fortify-source-prototype-gate.c |  62 ++++++
 clang/test/Sema/warn-fortify-source-typedef.c |  18 ++
 clang/test/Sema/warn-fortify-source.c         |  68 +++++++
 clang/utils/TableGen/ClangBuiltinsEmitter.cpp |   1 +
 8 files changed, 344 insertions(+), 7 deletions(-)
 create mode 100644 clang/test/Sema/warn-fortify-source-prototype-gate.c
 create mode 100644 clang/test/Sema/warn-fortify-source-typedef.c

diff --git a/clang/include/clang/Basic/Builtins.td 
b/clang/include/clang/Basic/Builtins.td
index 40ec94ab75046..95861fc1fd5d8 100644
--- a/clang/include/clang/Basic/Builtins.td
+++ b/clang/include/clang/Basic/Builtins.td
@@ -3759,6 +3759,8 @@ def StrnCaseCmp : GNULibBuiltin<"strings.h"> {
   let RequiresUndef = 1;
 }
 
+// POSIX unistd.h
+
 def GNU_Exit : GNULibBuiltin<"unistd.h"> {
   let Spellings = ["_exit"];
   let Attributes = [NoReturn];
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td 
b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 5c225dc56ddd8..857d08ed9787d 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -965,6 +965,10 @@ def warn_fortify_source_overflow
 def warn_fortify_source_size_mismatch : Warning<
   "'%0' size argument is too large; destination buffer has size %1,"
   " but size argument is %2">, InGroup<FortifySource>;
+def warn_fortify_destination_size_mismatch
+    : Warning<"'%0' size argument is too large; source buffer has size %2,"
+              " but size argument is %1">,
+      InGroup<FortifySource>;
 
 def warn_fortify_strlen_overflow: Warning<
   "'%0' will always overflow; destination buffer has size %1,"
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index a0894318dbd53..ec7eb82b8e76e 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -12696,9 +12696,12 @@ static QualType DecodeTypeFromStr(const char *&Str, 
const ASTContext &Context,
     assert(HowLong == 0 && !Signed && !Unsigned && "Bad modifiers for 'b'!");
     Type = Context.BoolTy;
     break;
-  case 'z':  // size_t.
-    assert(HowLong == 0 && !Signed && !Unsigned && "Bad modifiers for 'z'!");
-    Type = Context.getSizeType();
+  case 'z': // size_t and ssize_t.
+    assert(HowLong == 0 && "Bad modifiers for 'z'!");
+    if (Signed)
+      Type = Context.getSignedSizeType();
+    else
+      Type = Context.getSizeType();
     break;
   case 'w':  // wchar_t.
     assert(HowLong == 0 && !Signed && !Unsigned && "Bad modifiers for 'w'!");
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 530587208cce8..8c3d64e9d9f8b 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -1165,7 +1165,112 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
 
   unsigned BuiltinID = UseDecl->getBuiltinID(/*ConsiderWrappers=*/true);
 
-  if (!BuiltinID)
+  // Some libc I/O functions are intentionally not Clang builtins:
+  //   * "read", "write", "readlink", "readlinkat", and "getcwd" are common
+  //     identifiers (or names that appear in asm-label wrappers); declaring
+  //     them as builtins triggers -Wincompatible-library-redeclaration on
+  //     harmless local declarations (an error under -Werror).
+  //   * pread/pwrite prototypes use off_t, whose width is platform- and
+  //     macro-dependent (notably _FILE_OFFSET_BITS); a fixed builtin signature
+  //     would clash with the system header on some targets.
+  // Recognize them by name, but only after checking the full POSIX prototype
+  // so that an unrelated function happening to share the name is not
+  // diagnosed as if it were the libc function.
+  enum class LibCDispatch {
+    None,
+    Read,
+    Write,
+    PRead,
+    PWrite,
+    ReadLink,
+    ReadLinkAt,
+    GetCWD,
+  };
+  LibCDispatch LibC = LibCDispatch::None;
+  StringRef LibCName;
+  if (!BuiltinID && FD->isExternC() && FD->getIdentifier() &&
+      !FD->isVariadic()) {
+    ASTContext &Ctx = getASTContext();
+    QualType IntTy = Ctx.IntTy;
+    QualType SizeTy = Ctx.getSizeType();
+    unsigned SizeWidth = Ctx.getTypeSize(SizeTy);
+    QualType VoidPtrTy = Ctx.VoidPtrTy;
+    QualType ConstVoidPtrTy = Ctx.getPointerType(Ctx.VoidTy.withConst());
+    QualType CharPtrTy = Ctx.getPointerType(Ctx.CharTy);
+    QualType ConstCharPtrTy = Ctx.getPointerType(Ctx.CharTy.withConst());
+
+    auto Same = [&](QualType A, QualType B) {
+      return Ctx.hasSameUnqualifiedType(A, B);
+    };
+    // ssize_t is not canonicalized to a single Clang type: glibc declares it
+    // as `long` while other libcs (and Clang's getIntTypeForBitwidth for
+    // size_t-width) pick `int` on ILP32 targets. Both are valid ssize_t
+    // implementations as long as they are signed integers of size_t's width.
+    // Match structurally rather than by canonical-type equality so the gate
+    // is tolerant of either libc choice.
+    auto IsSSizeT = [&](QualType T) {
+      return T->isSignedIntegerType() && Ctx.getTypeSize(T) == SizeWidth;
+    };
+    auto P = [&](unsigned I) { return FD->getParamDecl(I)->getType(); };
+    QualType Ret = FD->getReturnType();
+    StringRef Name = FD->getIdentifier()->getName();
+    unsigned NumArgs = TheCall->getNumArgs();
+
+    if (FD->getNumParams() == 2 && NumArgs == 2) {
+      // char *getcwd(char *, size_t)
+      if (Name == "getcwd" && Same(Ret, CharPtrTy) && Same(P(0), CharPtrTy) &&
+          Same(P(1), SizeTy)) {
+        LibC = LibCDispatch::GetCWD;
+        LibCName = Name;
+      }
+    } else if (FD->getNumParams() == 3 && NumArgs == 3 && IsSSizeT(Ret)) {
+      if (Name == "read" && Same(P(0), IntTy) && Same(P(1), VoidPtrTy) &&
+          Same(P(2), SizeTy)) {
+        LibC = LibCDispatch::Read;
+        LibCName = Name;
+      } else if (Name == "write" && Same(P(0), IntTy) &&
+                 Same(P(1), ConstVoidPtrTy) && Same(P(2), SizeTy)) {
+        LibC = LibCDispatch::Write;
+        LibCName = Name;
+      } else if (Name == "readlink" && Same(P(0), ConstCharPtrTy) &&
+                 Same(P(1), CharPtrTy) && Same(P(2), SizeTy)) {
+        LibC = LibCDispatch::ReadLink;
+        LibCName = Name;
+      }
+    } else if (FD->getNumParams() == 4 && NumArgs == 4 && IsSSizeT(Ret)) {
+      // pread/pwrite take off_t (platform-dependent width, but at least as
+      // wide as size_t in practice); pread64/pwrite64 take off64_t which is
+      // always 64-bit signed. Require a signed integer of the appropriate
+      // width so unrelated declarations (e.g. taking int on a 64-bit target)
+      // do not get matched.
+      QualType Off = P(3);
+      bool OffOK = Off->isSignedIntegerType();
+      if (OffOK) {
+        unsigned OffWidth = Ctx.getTypeSize(Off);
+        if (Name == "pread64" || Name == "pwrite64")
+          OffOK = OffWidth == 64;
+        else
+          OffOK = OffWidth >= SizeWidth;
+      }
+      if ((Name == "pread" || Name == "pread64") && OffOK &&
+          Same(P(0), IntTy) && Same(P(1), VoidPtrTy) && Same(P(2), SizeTy)) {
+        LibC = LibCDispatch::PRead;
+        LibCName = Name;
+      } else if ((Name == "pwrite" || Name == "pwrite64") && OffOK &&
+                 Same(P(0), IntTy) && Same(P(1), ConstVoidPtrTy) &&
+                 Same(P(2), SizeTy)) {
+        LibC = LibCDispatch::PWrite;
+        LibCName = Name;
+      } else if (Name == "readlinkat" && Same(P(0), IntTy) &&
+                 Same(P(1), ConstCharPtrTy) && Same(P(2), CharPtrTy) &&
+                 Same(P(3), SizeTy)) {
+        LibC = LibCDispatch::ReadLinkAt;
+        LibCName = Name;
+      }
+    }
+  }
+
+  if (!BuiltinID && LibC == LibCDispatch::None)
     return;
 
   const TargetInfo &TI = getASTContext().getTargetInfo();
@@ -1253,8 +1358,19 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
   std::optional<llvm::APSInt> DestinationSize;
   unsigned DiagID = 0;
   bool IsChkVariant = false;
+  bool IsTriggered = false;
+
+  auto CompareSizeSourceToDest = [&]() {
+    return SourceSize && DestinationSize
+               ? std::optional<int>{llvm::APSInt::compareValues(
+                     *SourceSize, *DestinationSize)}
+               : std::nullopt;
+  };
+
+  auto GetFunctionName = [&]() -> std::string {
+    if (LibC != LibCDispatch::None)
+      return LibCName.str();
 
-  auto GetFunctionName = [&]() {
     std::string FunctionNameStr =
         getASTContext().BuiltinInfo.getName(BuiltinID);
     llvm::StringRef FunctionName = FunctionNameStr;
@@ -1270,6 +1386,58 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     return FunctionName.str();
   };
 
+  if (LibC == LibCDispatch::Read) {
+    // read: ssize_t(int fd, void buf[.count], size_t count);
+    // Up to count(2) bytes are written into buf(1).
+    DiagID = diag::warn_fortify_source_size_mismatch;
+    SourceSize = ComputeExplicitObjectSizeArgument(2);
+    DestinationSize = ComputeSizeArgument(1);
+    IsTriggered = CompareSizeSourceToDest() > 0;
+  } else if (LibC == LibCDispatch::Write) {
+    // write: ssize_t(int, const void buf[.count], size_t count);
+    // Up to count(2) bytes are read from buf(1).
+    DiagID = diag::warn_fortify_destination_size_mismatch;
+    SourceSize = ComputeSizeArgument(1);
+    DestinationSize = ComputeExplicitObjectSizeArgument(2);
+    IsTriggered = CompareSizeSourceToDest() < 0;
+  } else if (LibC == LibCDispatch::PRead) {
+    // pread/pread64: ssize_t(int fd, void buf[.count], size_t count, off_t);
+    // Up to count(2) bytes are written into buf(1).
+    DiagID = diag::warn_fortify_source_size_mismatch;
+    SourceSize = ComputeExplicitObjectSizeArgument(2);
+    DestinationSize = ComputeSizeArgument(1);
+    IsTriggered = CompareSizeSourceToDest() > 0;
+  } else if (LibC == LibCDispatch::PWrite) {
+    // pwrite/pwrite64: ssize_t(int, const void buf[.count], size_t count, 
off_t);
+    // Up to count(2) bytes are read from buf(1).
+    DiagID = diag::warn_fortify_destination_size_mismatch;
+    SourceSize = ComputeSizeArgument(1);
+    DestinationSize = ComputeExplicitObjectSizeArgument(2);
+    IsTriggered = CompareSizeSourceToDest() < 0;
+  } else if (LibC == LibCDispatch::ReadLink) {
+    // readlink:
+    //   ssize_t(const char *restrict, char buf[.bufsize], size_t bufsize);
+    // Up to bufsize(2) bytes are written into buf(1).
+    DiagID = diag::warn_fortify_source_size_mismatch;
+    SourceSize = ComputeExplicitObjectSizeArgument(2);
+    DestinationSize = ComputeSizeArgument(1);
+    IsTriggered = CompareSizeSourceToDest() > 0;
+  } else if (LibC == LibCDispatch::ReadLinkAt) {
+    // readlinkat:
+    //   ssize_t(int, const char *restrict, char buf[.bufsize], size_t 
bufsize);
+    // Up to bufsize(3) bytes are written into buf(2).
+    DiagID = diag::warn_fortify_source_size_mismatch;
+    SourceSize = ComputeExplicitObjectSizeArgument(3);
+    DestinationSize = ComputeSizeArgument(2);
+    IsTriggered = CompareSizeSourceToDest() > 0;
+  } else if (LibC == LibCDispatch::GetCWD) {
+    // char *getcwd(char buf[.size], size_t size);
+    // Up to size(1) bytes are written into buf(0).
+    DiagID = diag::warn_fortify_source_size_mismatch;
+    SourceSize = ComputeExplicitObjectSizeArgument(1);
+    DestinationSize = ComputeSizeArgument(0);
+    IsTriggered = CompareSizeSourceToDest() > 0;
+  } else
   switch (BuiltinID) {
   default:
     return;
@@ -1282,6 +1450,7 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     DiagID = diag::warn_fortify_strlen_overflow;
     SourceSize = ComputeStrLenArgument(1);
     DestinationSize = ComputeSizeArgument(0);
+    IsTriggered = CompareSizeSourceToDest() > 0;
     break;
   }
 
@@ -1292,6 +1461,7 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     SourceSize = ComputeStrLenArgument(1);
     DestinationSize = ComputeExplicitObjectSizeArgument(2);
     IsChkVariant = true;
+    IsTriggered = CompareSizeSourceToDest() > 0;
     break;
   }
 
@@ -1362,11 +1532,13 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
         } else {
           DestinationSize = ComputeSizeArgument(0);
         }
+        IsTriggered = CompareSizeSourceToDest() > 0;
         break;
       }
     }
     return;
   }
+
   case Builtin::BI__builtin___memcpy_chk:
   case Builtin::BI__builtin___memmove_chk:
   case Builtin::BI__builtin___memset_chk:
@@ -1382,6 +1554,7 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     DestinationSize =
         ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
     IsChkVariant = true;
+    IsTriggered = CompareSizeSourceToDest() > 0;
     break;
   }
 
@@ -1391,6 +1564,7 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     SourceSize = ComputeExplicitObjectSizeArgument(1);
     DestinationSize = ComputeExplicitObjectSizeArgument(3);
     IsChkVariant = true;
+    IsTriggered = CompareSizeSourceToDest() > 0;
     break;
   }
 
@@ -1408,6 +1582,7 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     DiagID = diag::warn_fortify_source_size_mismatch;
     SourceSize = ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
     DestinationSize = ComputeSizeArgument(0);
+    IsTriggered = CompareSizeSourceToDest() > 0;
     break;
   }
 
@@ -1424,6 +1599,7 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     DiagID = diag::warn_fortify_source_overflow;
     SourceSize = ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
     DestinationSize = ComputeSizeArgument(0);
+    IsTriggered = CompareSizeSourceToDest() > 0;
     break;
   }
   case Builtin::BIbcopy:
@@ -1431,8 +1607,10 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     DiagID = diag::warn_fortify_source_overflow;
     SourceSize = ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
     DestinationSize = ComputeSizeArgument(1);
+    IsTriggered = CompareSizeSourceToDest() > 0;
     break;
   }
+
   case Builtin::BIsnprintf:
   case Builtin::BI__builtin_snprintf:
   case Builtin::BIvsnprintf:
@@ -1472,11 +1650,12 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     const Expr *Dest = TheCall->getArg(0)->IgnoreCasts();
     IdentifierInfo *FnInfo = FD->getIdentifier();
     CheckSizeofMemaccessArgument(LenArg, Dest, FnInfo);
+    IsTriggered = CompareSizeSourceToDest() > 0;
+    break;
   }
   }
 
-  if (!SourceSize || !DestinationSize ||
-      llvm::APSInt::compareValues(*SourceSize, *DestinationSize) <= 0)
+  if (!IsTriggered)
     return;
 
   std::string FunctionName = GetFunctionName();
diff --git a/clang/test/Sema/warn-fortify-source-prototype-gate.c 
b/clang/test/Sema/warn-fortify-source-prototype-gate.c
new file mode 100644
index 0000000000000..2112af03e1734
--- /dev/null
+++ b/clang/test/Sema/warn-fortify-source-prototype-gate.c
@@ -0,0 +1,62 @@
+// RUN: %clang_cc1 -triple x86_64-apple-macosx10.14.0 %s -verify -Werror
+
+// Verify that the fortify dispatch for read/write/pread/pwrite/readlink/
+// readlinkat/getcwd is gated on the full POSIX prototype: a user-defined
+// function that happens to share a name with one of these libc functions but
+// has a different signature must not be diagnosed as if it were the libc
+// function. (Reviewer-reported regression: extern-C + arity match alone
+// would falsely diagnose unrelated code under -Werror.)
+
+typedef unsigned long size_t;
+typedef long ssize_t;
+
+// expected-no-diagnostics
+
+// Variadic: prefix happens to match POSIX read, but the varargs make this
+// an unrelated declaration.
+ssize_t read(int, void *, size_t, ...);
+
+void test_read_variadic(void) {
+  char b[4];
+  read(0, b, 8);
+}
+
+// Wrong return type (int) for write.
+int write(int fd, const char *buf, size_t n);
+
+void test_write_wrong_return(void) {
+  char buf[4];
+  write(0, buf, 8);
+}
+
+// Wrong return type (void) for readlink.
+void readlink(const char *p, char *b, size_t n);
+
+void test_readlink_wrong_return(void) {
+  char b[4];
+  readlink("/", b, 8);
+}
+
+// Wrong second parameter type (int instead of char*) for getcwd.
+char *getcwd(int x, size_t n);
+
+void test_getcwd_wrong_param(void) {
+  getcwd(42, 8);
+}
+
+// pread64 with int offset on x86_64: off64_t is required to be 64-bit signed,
+// so int (32-bit on x86_64) is not POSIX pread64.
+ssize_t pread64(int, void *, size_t, int);
+
+void test_pread64_narrow_offset(void) {
+  char b[4];
+  pread64(0, b, 8, 0);
+}
+
+// pwrite64 with int offset: same as above.
+ssize_t pwrite64(int, const void *, size_t, int);
+
+void test_pwrite64_narrow_offset(void) {
+  char b[4];
+  pwrite64(0, b, 8, 0);
+}
diff --git a/clang/test/Sema/warn-fortify-source-typedef.c 
b/clang/test/Sema/warn-fortify-source-typedef.c
new file mode 100644
index 0000000000000..af7c7565d7658
--- /dev/null
+++ b/clang/test/Sema/warn-fortify-source-typedef.c
@@ -0,0 +1,18 @@
+// RUN: %clang_cc1 -triple i686-unknown-linux %s -verify
+// RUN: %clang_cc1 -triple x86_64-unknown-linux %s -verify
+
+// Verify that the fortify dispatch is tolerant of libc typedef choices for
+// ssize_t. glibc uses `long` while other libcs (musl, bionic) may use a
+// type that matches Clang's signed counterpart of size_t. On ILP32 these
+// differ canonically (`long` vs `int`) even though both are 32-bit signed
+// integers; the gate must accept either as ssize_t.
+
+typedef __SIZE_TYPE__ size_t;
+typedef long ssize_t;
+
+ssize_t read(int, void *, size_t);
+
+void test_read(void) {
+  char b[4];
+  read(0, b, 8); // expected-warning {{'read' size argument is too large; 
destination buffer has size 4, but size argument is 8}}
+}
diff --git a/clang/test/Sema/warn-fortify-source.c 
b/clang/test/Sema/warn-fortify-source.c
index d0b519a516545..5c4df36d2c88d 100644
--- a/clang/test/Sema/warn-fortify-source.c
+++ b/clang/test/Sema/warn-fortify-source.c
@@ -24,6 +24,19 @@ void *memcpy(void *dst, const void *src, size_t c);
 void bcopy(const void *src, void *dst, size_t n);
 void bzero(void *dst, size_t n);
 
+typedef long ssize_t;
+typedef long off_t;
+typedef long long off64_t;
+ssize_t read(int fd, void *buf, size_t count);
+ssize_t write(int fd, const void *buf, size_t count);
+ssize_t pread(int fd, void *buf, size_t count, off_t offset);
+ssize_t pread64(int fd, void *buf, size_t count, off64_t offset);
+ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
+ssize_t pwrite64(int fd, const void *buf, size_t count, off64_t offset);
+char *getcwd(char *buf, size_t size);
+ssize_t readlink(const char *path, char *buf, size_t bufsize);
+ssize_t readlinkat(int fd, const char *path, char *buf, size_t bufsize);
+
 #ifdef __cplusplus
 }
 #endif
@@ -116,6 +129,61 @@ 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_read(void) {
+  char buf[10];
+  read(0, buf, 10);
+  read(0, buf, 20); // expected-warning {{'read' size argument is too large; 
destination buffer has size 10, but size argument is 20}}
+}
+
+void call_pread(void) {
+  char buf[10];
+  pread(0, buf, 10, 0);
+  pread(0, buf, 20, 0); // expected-warning {{'pread' size argument is too 
large; destination buffer has size 10, but size argument is 20}}
+}
+
+void call_pread64(void) {
+  char buf[10];
+  pread64(0, buf, 10, 0);
+  pread64(0, buf, 20, 0); // expected-warning {{'pread64' size argument is too 
large; destination buffer has size 10, but size argument is 20}}
+}
+
+void call_write(void) {
+  char buf[10];
+  write(0, buf, 10);
+  write(0, buf, 20); // expected-warning {{'write' size argument is too large; 
source buffer has size 10, but size argument is 20}}
+}
+
+void call_pwrite(void) {
+  char buf[10];
+  pwrite(0, buf, 10, 0);
+  pwrite(0, buf, 20, 0); // expected-warning {{'pwrite' size argument is too 
large; source buffer has size 10, but size argument is 20}}
+}
+
+void call_pwrite64(void) {
+  char buf[10];
+  pwrite64(0, buf, 10, 0);
+  pwrite64(0, buf, 20, 0); // expected-warning {{'pwrite64' size argument is 
too large; source buffer has size 10, but size argument is 20}}
+}
+
+void call_getcwd(void) {
+  char buf[10];
+  getcwd(buf, 10);
+  getcwd(buf, 20); // expected-warning {{'getcwd' size argument is too large; 
destination buffer has size 10, but size argument is 20}}
+}
+
+void call_readlink(void) {
+  char buf[10];
+  readlink("path", buf, 10);
+  readlink("path", buf, 20); // expected-warning {{'readlink' size argument is 
too large; destination buffer has size 10, but size argument is 20}}
+}
+
+void call_readlinkat(void) {
+  char buf[10];
+  readlinkat(0, "path", buf, 10);
+  readlinkat(0, "path", buf, 20); // expected-warning {{'readlinkat' size 
argument is too large; destination buffer has size 10, but size argument is 20}}
+}
+
+
 void call_snprintf(double d, int n) {
   char buf[10];
   __builtin_snprintf(buf, 10, "merp");
diff --git a/clang/utils/TableGen/ClangBuiltinsEmitter.cpp 
b/clang/utils/TableGen/ClangBuiltinsEmitter.cpp
index c2e38c0d6aeb8..ae7f99107c03b 100644
--- a/clang/utils/TableGen/ClangBuiltinsEmitter.cpp
+++ b/clang/utils/TableGen/ClangBuiltinsEmitter.cpp
@@ -371,6 +371,7 @@ class PrototypeParser {
                                .Case("short", "s")
                                .Case("sigjmp_buf", "SJ")
                                .Case("size_t", "z")
+                               .Case("ssize_t", "Sz")
                                .Case("ucontext_t", "K")
                                .Case("uint32_t", "UZi")
                                .Case("uint64_t", "UWi")

>From cfd2089f99b970b36c54ea58ddfe1f5e7f3dd248 Mon Sep 17 00:00:00 2001
From: Colin Kinloch <[email protected]>
Date: Mon, 3 Nov 2025 02:53:09 +0000
Subject: [PATCH 2/8] [clang][Sema] Add min/max operation size for fortify
 checks

Preserve existing bcopy/bzero fortify handling under the new min/max
operation-size model. bzero is handled like memset, while bcopy is handled
like memcpy with reversed source/destination arguments, allowing both
destination overflow and source over-read diagnostics.
---
 .../clang/Basic/DiagnosticSemaKinds.td        |   8 +-
 clang/lib/Sema/SemaChecking.cpp               | 699 +++++++++---------
 clang/test/Sema/builtin-memcpy.c              |   3 +-
 .../Sema/warn-fortify-source-prototype-gate.c |  15 +-
 clang/test/Sema/warn-fortify-source.c         |  43 +-
 5 files changed, 407 insertions(+), 361 deletions(-)

diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td 
b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 857d08ed9787d..3ee2ec39e94df 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -962,12 +962,16 @@ def warn_builtin_chk_overflow : Warning<
 
 def warn_fortify_source_overflow
   : Warning<warn_builtin_chk_overflow.Summary>, InGroup<FortifySource>;
+def warn_fortify_destination_over_read
+    : Warning<"'%0' will always over-read; source buffer has size %1,"
+              " but size argument is %2">,
+      InGroup<FortifySource>;
 def warn_fortify_source_size_mismatch : Warning<
   "'%0' size argument is too large; destination buffer has size %1,"
   " but size argument is %2">, InGroup<FortifySource>;
 def warn_fortify_destination_size_mismatch
-    : Warning<"'%0' size argument is too large; source buffer has size %2,"
-              " but size argument is %1">,
+    : Warning<"'%0' size argument is too large; source buffer has size %1,"
+              " but size argument is %2">,
       InGroup<FortifySource>;
 
 def warn_fortify_strlen_overflow: Warning<
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 8c3d64e9d9f8b..666f93517dff8 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -1176,101 +1176,92 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
   // Recognize them by name, but only after checking the full POSIX prototype
   // so that an unrelated function happening to share the name is not
   // diagnosed as if it were the libc function.
-  enum class LibCDispatch {
-    None,
-    Read,
-    Write,
-    PRead,
-    PWrite,
-    ReadLink,
-    ReadLinkAt,
-    GetCWD,
+  struct LibcFuncDesc {
+    StringRef Name;
+    QualType Return;
+    SmallVector<QualType, 4> Params;
+    std::optional<unsigned> SourceSizeIdx; // __bos buffer; reads from
+    std::optional<unsigned> MaxOpSizeIdx;  // count arg (constant-evaluated)
+    std::optional<unsigned> DestSizeIdx;   // __bos buffer; writes to
   };
-  LibCDispatch LibC = LibCDispatch::None;
-  StringRef LibCName;
+
+  std::optional<LibcFuncDesc> LibCMatch;
   if (!BuiltinID && FD->isExternC() && FD->getIdentifier() &&
       !FD->isVariadic()) {
     ASTContext &Ctx = getASTContext();
     QualType IntTy = Ctx.IntTy;
     QualType SizeTy = Ctx.getSizeType();
-    unsigned SizeWidth = Ctx.getTypeSize(SizeTy);
     QualType VoidPtrTy = Ctx.VoidPtrTy;
     QualType ConstVoidPtrTy = Ctx.getPointerType(Ctx.VoidTy.withConst());
     QualType CharPtrTy = Ctx.getPointerType(Ctx.CharTy);
     QualType ConstCharPtrTy = Ctx.getPointerType(Ctx.CharTy.withConst());
+    // ssize_t / off_t / off64_t aren't single Clang types (ssize_t differs
+    // across libcs, off_t depends on _FILE_OFFSET_BITS, off64_t is always
+    // 64-bit). The name match has already pinned us to POSIX intent, so
+    // MatchSlot below treats any integer-typed slot as compatible with any
+    // integer-typed argument; these aliases just keep the table readable.
+    QualType SSizeTy = IntTy;
+    QualType OffTy = IntTy;
+    QualType Off64Ty = IntTy;
+
+    // Same unqualified type, or both integer types: accepts libc typedef
+    // divergence (ssize_t/off_t/off64_t) while still rejecting pointer or
+    // aggregate mismatches.
+    auto MatchSlot = [&](QualType Expected, QualType Actual) {
+      if (Ctx.hasSameUnqualifiedType(Expected, Actual))
+        return true;
+      return Expected->isIntegerType() && Actual->isIntegerType();
+    };
 
-    auto Same = [&](QualType A, QualType B) {
-      return Ctx.hasSameUnqualifiedType(A, B);
+    auto Make = [](StringRef Name, QualType Ret,
+                   std::initializer_list<QualType> Params,
+                   std::optional<unsigned> Src, std::optional<unsigned> MaxOp,
+                   std::optional<unsigned> Dest) {
+      return LibcFuncDesc{Name, Ret, Params, Src, MaxOp, Dest};
     };
-    // ssize_t is not canonicalized to a single Clang type: glibc declares it
-    // as `long` while other libcs (and Clang's getIntTypeForBitwidth for
-    // size_t-width) pick `int` on ILP32 targets. Both are valid ssize_t
-    // implementations as long as they are signed integers of size_t's width.
-    // Match structurally rather than by canonical-type equality so the gate
-    // is tolerant of either libc choice.
-    auto IsSSizeT = [&](QualType T) {
-      return T->isSignedIntegerType() && Ctx.getTypeSize(T) == SizeWidth;
+
+    auto LookupLibc = [&](StringRef Name) -> std::optional<LibcFuncDesc> {
+      if (Name == "getcwd")
+        return Make(Name, CharPtrTy, {CharPtrTy, SizeTy}, std::nullopt, 1, 0);
+      if (Name == "read")
+        return Make(Name, SSizeTy, {IntTy, VoidPtrTy, SizeTy}, std::nullopt, 2,
+                    1);
+      if (Name == "write")
+        return Make(Name, SSizeTy, {IntTy, ConstVoidPtrTy, SizeTy}, 1, 2,
+                    std::nullopt);
+      if (Name == "readlink")
+        return Make(Name, SSizeTy, {ConstCharPtrTy, CharPtrTy, SizeTy},
+                    std::nullopt, 2, 1);
+      if (Name == "readlinkat")
+        return Make(Name, SSizeTy, {IntTy, ConstCharPtrTy, CharPtrTy, SizeTy},
+                    std::nullopt, 3, 2);
+      if (Name == "pread")
+        return Make(Name, SSizeTy, {IntTy, VoidPtrTy, SizeTy, OffTy},
+                    std::nullopt, 2, 1);
+      if (Name == "pread64")
+        return Make(Name, SSizeTy, {IntTy, VoidPtrTy, SizeTy, Off64Ty},
+                    std::nullopt, 2, 1);
+      if (Name == "pwrite")
+        return Make(Name, SSizeTy, {IntTy, ConstVoidPtrTy, SizeTy, OffTy}, 1, 
2,
+                    std::nullopt);
+      if (Name == "pwrite64")
+        return Make(Name, SSizeTy, {IntTy, ConstVoidPtrTy, SizeTy, Off64Ty}, 1,
+                    2, std::nullopt);
+      return std::nullopt;
     };
-    auto P = [&](unsigned I) { return FD->getParamDecl(I)->getType(); };
-    QualType Ret = FD->getReturnType();
-    StringRef Name = FD->getIdentifier()->getName();
-    unsigned NumArgs = TheCall->getNumArgs();
-
-    if (FD->getNumParams() == 2 && NumArgs == 2) {
-      // char *getcwd(char *, size_t)
-      if (Name == "getcwd" && Same(Ret, CharPtrTy) && Same(P(0), CharPtrTy) &&
-          Same(P(1), SizeTy)) {
-        LibC = LibCDispatch::GetCWD;
-        LibCName = Name;
-      }
-    } else if (FD->getNumParams() == 3 && NumArgs == 3 && IsSSizeT(Ret)) {
-      if (Name == "read" && Same(P(0), IntTy) && Same(P(1), VoidPtrTy) &&
-          Same(P(2), SizeTy)) {
-        LibC = LibCDispatch::Read;
-        LibCName = Name;
-      } else if (Name == "write" && Same(P(0), IntTy) &&
-                 Same(P(1), ConstVoidPtrTy) && Same(P(2), SizeTy)) {
-        LibC = LibCDispatch::Write;
-        LibCName = Name;
-      } else if (Name == "readlink" && Same(P(0), ConstCharPtrTy) &&
-                 Same(P(1), CharPtrTy) && Same(P(2), SizeTy)) {
-        LibC = LibCDispatch::ReadLink;
-        LibCName = Name;
-      }
-    } else if (FD->getNumParams() == 4 && NumArgs == 4 && IsSSizeT(Ret)) {
-      // pread/pwrite take off_t (platform-dependent width, but at least as
-      // wide as size_t in practice); pread64/pwrite64 take off64_t which is
-      // always 64-bit signed. Require a signed integer of the appropriate
-      // width so unrelated declarations (e.g. taking int on a 64-bit target)
-      // do not get matched.
-      QualType Off = P(3);
-      bool OffOK = Off->isSignedIntegerType();
-      if (OffOK) {
-        unsigned OffWidth = Ctx.getTypeSize(Off);
-        if (Name == "pread64" || Name == "pwrite64")
-          OffOK = OffWidth == 64;
-        else
-          OffOK = OffWidth >= SizeWidth;
-      }
-      if ((Name == "pread" || Name == "pread64") && OffOK &&
-          Same(P(0), IntTy) && Same(P(1), VoidPtrTy) && Same(P(2), SizeTy)) {
-        LibC = LibCDispatch::PRead;
-        LibCName = Name;
-      } else if ((Name == "pwrite" || Name == "pwrite64") && OffOK &&
-                 Same(P(0), IntTy) && Same(P(1), ConstVoidPtrTy) &&
-                 Same(P(2), SizeTy)) {
-        LibC = LibCDispatch::PWrite;
-        LibCName = Name;
-      } else if (Name == "readlinkat" && Same(P(0), IntTy) &&
-                 Same(P(1), ConstCharPtrTy) && Same(P(2), CharPtrTy) &&
-                 Same(P(3), SizeTy)) {
-        LibC = LibCDispatch::ReadLinkAt;
-        LibCName = Name;
-      }
+
+    if (auto Desc = LookupLibc(FD->getIdentifier()->getName())) {
+      bool Matches = FD->getNumParams() == Desc->Params.size() &&
+                     TheCall->getNumArgs() == Desc->Params.size() &&
+                     MatchSlot(Desc->Return, FD->getReturnType());
+      for (unsigned I = 0; Matches && I < Desc->Params.size(); ++I)
+        Matches = MatchSlot(Desc->Params[I], FD->getParamDecl(I)->getType());
+      if (Matches)
+        LibCMatch = std::move(*Desc);
     }
   }
 
-  if (!BuiltinID && LibC == LibCDispatch::None)
+  if (!BuiltinID && !LibCMatch)
     return;
 
   const TargetInfo &TI = getASTContext().getTargetInfo();
@@ -1354,22 +1345,24 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     return std::nullopt;
   };
 
+  // Size of the memory read from
   std::optional<llvm::APSInt> SourceSize;
+  // Size of the memory written to
   std::optional<llvm::APSInt> DestinationSize;
-  unsigned DiagID = 0;
+  // Maximum operation size for detecting possible out of bounds access
+  std::optional<llvm::APSInt> MaxOperationSize;
+  // Minimum operation size for detecting definite out of bounds access
+  std::optional<llvm::APSInt> MinOperationSize;
+
+  unsigned DiagOverflowID = diag::warn_fortify_source_overflow;
+  unsigned DiagMayOverflowID = diag::warn_fortify_source_size_mismatch;
+  unsigned DiagOverReadID = diag::warn_fortify_destination_over_read;
+  unsigned DiagMayOverReadID = diag::warn_fortify_destination_size_mismatch;
   bool IsChkVariant = false;
-  bool IsTriggered = false;
-
-  auto CompareSizeSourceToDest = [&]() {
-    return SourceSize && DestinationSize
-               ? std::optional<int>{llvm::APSInt::compareValues(
-                     *SourceSize, *DestinationSize)}
-               : std::nullopt;
-  };
 
   auto GetFunctionName = [&]() -> std::string {
-    if (LibC != LibCDispatch::None)
-      return LibCName.str();
+    if (LibCMatch)
+      return LibCMatch->Name.str();
 
     std::string FunctionNameStr =
         getASTContext().BuiltinInfo.getName(BuiltinID);
@@ -1386,287 +1379,297 @@ void 
Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
     return FunctionName.str();
   };
 
-  if (LibC == LibCDispatch::Read) {
-    // read: ssize_t(int fd, void buf[.count], size_t count);
-    // Up to count(2) bytes are written into buf(1).
-    DiagID = diag::warn_fortify_source_size_mismatch;
-    SourceSize = ComputeExplicitObjectSizeArgument(2);
-    DestinationSize = ComputeSizeArgument(1);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-  } else if (LibC == LibCDispatch::Write) {
-    // write: ssize_t(int, const void buf[.count], size_t count);
-    // Up to count(2) bytes are read from buf(1).
-    DiagID = diag::warn_fortify_destination_size_mismatch;
-    SourceSize = ComputeSizeArgument(1);
-    DestinationSize = ComputeExplicitObjectSizeArgument(2);
-    IsTriggered = CompareSizeSourceToDest() < 0;
-  } else if (LibC == LibCDispatch::PRead) {
-    // pread/pread64: ssize_t(int fd, void buf[.count], size_t count, off_t);
-    // Up to count(2) bytes are written into buf(1).
-    DiagID = diag::warn_fortify_source_size_mismatch;
-    SourceSize = ComputeExplicitObjectSizeArgument(2);
-    DestinationSize = ComputeSizeArgument(1);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-  } else if (LibC == LibCDispatch::PWrite) {
-    // pwrite/pwrite64: ssize_t(int, const void buf[.count], size_t count, 
off_t);
-    // Up to count(2) bytes are read from buf(1).
-    DiagID = diag::warn_fortify_destination_size_mismatch;
-    SourceSize = ComputeSizeArgument(1);
-    DestinationSize = ComputeExplicitObjectSizeArgument(2);
-    IsTriggered = CompareSizeSourceToDest() < 0;
-  } else if (LibC == LibCDispatch::ReadLink) {
-    // readlink:
-    //   ssize_t(const char *restrict, char buf[.bufsize], size_t bufsize);
-    // Up to bufsize(2) bytes are written into buf(1).
-    DiagID = diag::warn_fortify_source_size_mismatch;
-    SourceSize = ComputeExplicitObjectSizeArgument(2);
-    DestinationSize = ComputeSizeArgument(1);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-  } else if (LibC == LibCDispatch::ReadLinkAt) {
-    // readlinkat:
-    //   ssize_t(int, const char *restrict, char buf[.bufsize], size_t 
bufsize);
-    // Up to bufsize(3) bytes are written into buf(2).
-    DiagID = diag::warn_fortify_source_size_mismatch;
-    SourceSize = ComputeExplicitObjectSizeArgument(3);
-    DestinationSize = ComputeSizeArgument(2);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-  } else if (LibC == LibCDispatch::GetCWD) {
-    // char *getcwd(char buf[.size], size_t size);
-    // Up to size(1) bytes are written into buf(0).
-    DiagID = diag::warn_fortify_source_size_mismatch;
-    SourceSize = ComputeExplicitObjectSizeArgument(1);
-    DestinationSize = ComputeSizeArgument(0);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-  } else
-  switch (BuiltinID) {
-  default:
-    return;
-  case Builtin::BI__builtin_strcat:
-  case Builtin::BIstrcat:
-  case Builtin::BI__builtin_stpcpy:
-  case Builtin::BIstpcpy:
-  case Builtin::BI__builtin_strcpy:
-  case Builtin::BIstrcpy: {
-    DiagID = diag::warn_fortify_strlen_overflow;
-    SourceSize = ComputeStrLenArgument(1);
-    DestinationSize = ComputeSizeArgument(0);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
-
-  case Builtin::BI__builtin___strcat_chk:
-  case Builtin::BI__builtin___stpcpy_chk:
-  case Builtin::BI__builtin___strcpy_chk: {
-    DiagID = diag::warn_fortify_strlen_overflow;
-    SourceSize = ComputeStrLenArgument(1);
-    DestinationSize = ComputeExplicitObjectSizeArgument(2);
-    IsChkVariant = true;
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
-
-  case Builtin::BIscanf:
-  case Builtin::BIfscanf:
-  case Builtin::BIsscanf: {
-    unsigned FormatIndex = 1;
-    unsigned DataIndex = 2;
-    if (BuiltinID == Builtin::BIscanf) {
-      FormatIndex = 0;
-      DataIndex = 1;
+  if (LibCMatch) {
+    // All recognized libc I/O functions feed at most one buffer slot (read- or
+    // write-direction) plus a constant-evaluated count slot into the fortify
+    // checks; the per-function descriptor records which argument indices play
+    // each role.
+    if (auto Idx = LibCMatch->SourceSizeIdx)
+      SourceSize = ComputeSizeArgument(*Idx);
+    if (auto Idx = LibCMatch->MaxOpSizeIdx)
+      MaxOperationSize = ComputeExplicitObjectSizeArgument(*Idx);
+    if (auto Idx = LibCMatch->DestSizeIdx)
+      DestinationSize = ComputeSizeArgument(*Idx);
+  } else {
+    switch (BuiltinID) {
+    default:
+      return;
+    case Builtin::BI__builtin_strcat:
+    case Builtin::BIstrcat:
+    case Builtin::BI__builtin_stpcpy:
+    case Builtin::BIstpcpy:
+    case Builtin::BI__builtin_strcpy:
+    case Builtin::BIstrcpy: {
+      DiagOverflowID = diag::warn_fortify_strlen_overflow;
+      MinOperationSize = ComputeStrLenArgument(1);
+      DestinationSize = ComputeSizeArgument(0);
+      break;
     }
 
-    const auto *FormatExpr =
-        TheCall->getArg(FormatIndex)->IgnoreParenImpCasts();
+    case Builtin::BI__builtin___strcat_chk:
+    case Builtin::BI__builtin___stpcpy_chk:
+    case Builtin::BI__builtin___strcpy_chk: {
+      DiagOverflowID = diag::warn_fortify_strlen_overflow;
+      MinOperationSize = ComputeStrLenArgument(1);
+      DestinationSize = ComputeExplicitObjectSizeArgument(2);
+      IsChkVariant = true;
+      break;
+    }
 
-    StringRef FormatStrRef;
-    size_t StrLen;
-    if (!ProcessFormatStringLiteral(FormatExpr, FormatStrRef, StrLen, Context))
-      return;
+    case Builtin::BIscanf:
+    case Builtin::BIfscanf:
+    case Builtin::BIsscanf: {
+      unsigned FormatIndex = 1;
+      unsigned DataIndex = 2;
+      if (BuiltinID == Builtin::BIscanf) {
+        FormatIndex = 0;
+        DataIndex = 1;
+      }
 
-    auto Diagnose = [&](unsigned ArgIndex, unsigned DestSize,
-                        unsigned SourceSize) {
-      DiagID = diag::warn_fortify_scanf_overflow;
-      unsigned Index = ArgIndex + DataIndex;
-      std::string FunctionName = GetFunctionName();
-      DiagRuntimeBehavior(TheCall->getArg(Index)->getBeginLoc(), TheCall,
-                          PDiag(DiagID) << FunctionName << (Index + 1)
-                                        << DestSize << SourceSize);
-    };
+      const auto *FormatExpr =
+          TheCall->getArg(FormatIndex)->IgnoreParenImpCasts();
 
-    auto ShiftedComputeSizeArgument = [&](unsigned Index) {
-      return ComputeSizeArgument(Index + DataIndex);
-    };
-    ScanfDiagnosticFormatHandler H(ShiftedComputeSizeArgument, Diagnose);
-    const char *FormatBytes = FormatStrRef.data();
-    analyze_format_string::ParseScanfString(H, FormatBytes,
-                                            FormatBytes + StrLen, 
getLangOpts(),
-                                            Context.getTargetInfo());
-
-    // Unlike the other cases, in this one we have already issued the 
diagnostic
-    // here, so no need to continue (because unlike the other cases, here the
-    // diagnostic refers to the argument number).
-    return;
-  }
+      StringRef FormatStrRef;
+      size_t StrLen;
+      if (!ProcessFormatStringLiteral(FormatExpr, FormatStrRef, StrLen,
+                                      Context))
+        return;
 
-  case Builtin::BIsprintf:
-  case Builtin::BI__builtin___sprintf_chk: {
-    size_t FormatIndex = BuiltinID == Builtin::BIsprintf ? 1 : 3;
-    auto *FormatExpr = TheCall->getArg(FormatIndex)->IgnoreParenImpCasts();
+      auto Diagnose = [&](unsigned ArgIndex, unsigned DestSize,
+                          unsigned SourceSize) {
+        unsigned Index = ArgIndex + DataIndex;
+        std::string FunctionName = GetFunctionName();
+        DiagRuntimeBehavior(TheCall->getArg(Index)->getBeginLoc(), TheCall,
+                            PDiag(diag::warn_fortify_scanf_overflow)
+                                << FunctionName << (Index + 1) << DestSize
+                                << SourceSize);
+      };
 
-    StringRef FormatStrRef;
-    size_t StrLen;
-    if (ProcessFormatStringLiteral(FormatExpr, FormatStrRef, StrLen, Context)) 
{
-      EstimateSizeFormatHandler H(FormatStrRef);
+      auto ShiftedComputeSizeArgument = [&](unsigned Index) {
+        return ComputeSizeArgument(Index + DataIndex);
+      };
+      ScanfDiagnosticFormatHandler H(ShiftedComputeSizeArgument, Diagnose);
       const char *FormatBytes = FormatStrRef.data();
-      if (!analyze_format_string::ParsePrintfString(
-              H, FormatBytes, FormatBytes + StrLen, getLangOpts(),
-              Context.getTargetInfo(), false)) {
-        DiagID = H.isKernelCompatible()
-                     ? diag::warn_format_overflow
-                     : diag::warn_format_overflow_non_kprintf;
-        SourceSize = llvm::APSInt::getUnsigned(H.getSizeLowerBound())
-                         .extOrTrunc(SizeTypeWidth);
-        if (BuiltinID == Builtin::BI__builtin___sprintf_chk) {
-          DestinationSize = ComputeExplicitObjectSizeArgument(2);
-          IsChkVariant = true;
-        } else {
-          DestinationSize = ComputeSizeArgument(0);
+      analyze_format_string::ParseScanfString(
+          H, FormatBytes, FormatBytes + StrLen, getLangOpts(),
+          Context.getTargetInfo());
+
+      // Unlike the other cases, in this one we have already issued the
+      // diagnostic here, so no need to continue (because unlike the other
+      // cases, here the diagnostic refers to the argument number).
+      return;
+    }
+
+    case Builtin::BIsprintf:
+    case Builtin::BI__builtin___sprintf_chk: {
+      size_t FormatIndex = BuiltinID == Builtin::BIsprintf ? 1 : 3;
+      auto *FormatExpr = TheCall->getArg(FormatIndex)->IgnoreParenImpCasts();
+
+      StringRef FormatStrRef;
+      size_t StrLen;
+      if (ProcessFormatStringLiteral(FormatExpr, FormatStrRef, StrLen,
+                                     Context)) {
+        EstimateSizeFormatHandler H(FormatStrRef);
+        const char *FormatBytes = FormatStrRef.data();
+        if (!analyze_format_string::ParsePrintfString(
+                H, FormatBytes, FormatBytes + StrLen, getLangOpts(),
+                Context.getTargetInfo(), false)) {
+          DiagOverflowID = H.isKernelCompatible()
+                               ? diag::warn_format_overflow
+                               : diag::warn_format_overflow_non_kprintf;
+          MinOperationSize = llvm::APSInt::getUnsigned(H.getSizeLowerBound())
+                                 .extOrTrunc(SizeTypeWidth);
+          if (BuiltinID == Builtin::BI__builtin___sprintf_chk) {
+            DestinationSize = ComputeExplicitObjectSizeArgument(2);
+            IsChkVariant = true;
+          } else {
+            DestinationSize = ComputeSizeArgument(0);
+          }
+          break;
         }
-        IsTriggered = CompareSizeSourceToDest() > 0;
-        break;
       }
+      return;
     }
-    return;
-  }
 
-  case Builtin::BI__builtin___memcpy_chk:
-  case Builtin::BI__builtin___memmove_chk:
-  case Builtin::BI__builtin___memset_chk:
-  case Builtin::BI__builtin___strlcat_chk:
-  case Builtin::BI__builtin___strlcpy_chk:
-  case Builtin::BI__builtin___strncat_chk:
-  case Builtin::BI__builtin___strncpy_chk:
-  case Builtin::BI__builtin___stpncpy_chk:
-  case Builtin::BI__builtin___memccpy_chk:
-  case Builtin::BI__builtin___mempcpy_chk: {
-    DiagID = diag::warn_builtin_chk_overflow;
-    SourceSize = ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 2);
-    DestinationSize =
-        ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
-    IsChkVariant = true;
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
+    case Builtin::BI__builtin___memcpy_chk:
+    case Builtin::BI__builtin___memmove_chk:
+    case Builtin::BI__builtin___mempcpy_chk:
+    case Builtin::BI__builtin___memccpy_chk: {
+      // The source buffer is the second argument; the operation reads up to
+      // the user-supplied length from it.
+      DiagOverflowID = diag::warn_builtin_chk_overflow;
+      MinOperationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 2);
+      SourceSize = ComputeSizeArgument(1);
+      DestinationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
+      IsChkVariant = true;
+      break;
+    }
 
-  case Builtin::BI__builtin___snprintf_chk:
-  case Builtin::BI__builtin___vsnprintf_chk: {
-    DiagID = diag::warn_builtin_chk_overflow;
-    SourceSize = ComputeExplicitObjectSizeArgument(1);
-    DestinationSize = ComputeExplicitObjectSizeArgument(3);
-    IsChkVariant = true;
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
+    case Builtin::BI__builtin___memset_chk:
+    case Builtin::BI__builtin___strlcat_chk:
+    case Builtin::BI__builtin___strlcpy_chk:
+    case Builtin::BI__builtin___strncat_chk:
+    case Builtin::BI__builtin___strncpy_chk:
+    case Builtin::BI__builtin___stpncpy_chk: {
+      DiagOverflowID = diag::warn_builtin_chk_overflow;
+      MinOperationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 2);
+      DestinationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
+      IsChkVariant = true;
+      break;
+    }
 
-  case Builtin::BIstrncat:
-  case Builtin::BI__builtin_strncat:
-  case Builtin::BIstrncpy:
-  case Builtin::BI__builtin_strncpy:
-  case Builtin::BIstpncpy:
-  case Builtin::BI__builtin_stpncpy: {
-    // Whether these functions overflow depends on the runtime strlen of the
-    // string, not just the buffer size, so emitting the "always overflow"
-    // diagnostic isn't quite right. We should still diagnose passing a buffer
-    // size larger than the destination buffer though; this is a runtime abort
-    // in _FORTIFY_SOURCE mode, and is quite suspicious otherwise.
-    DiagID = diag::warn_fortify_source_size_mismatch;
-    SourceSize = ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
-    DestinationSize = ComputeSizeArgument(0);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
+    case Builtin::BI__builtin___snprintf_chk:
+    case Builtin::BI__builtin___vsnprintf_chk: {
+      DiagOverflowID = diag::warn_builtin_chk_overflow;
+      MinOperationSize = ComputeExplicitObjectSizeArgument(1);
+      DestinationSize = ComputeExplicitObjectSizeArgument(3);
+      IsChkVariant = true;
+      break;
+    }
 
-  case Builtin::BIbzero:
-  case Builtin::BI__builtin_bzero:
-  case Builtin::BImemcpy:
-  case Builtin::BI__builtin_memcpy:
-  case Builtin::BImemmove:
-  case Builtin::BI__builtin_memmove:
-  case Builtin::BImemset:
-  case Builtin::BI__builtin_memset:
-  case Builtin::BImempcpy:
-  case Builtin::BI__builtin_mempcpy: {
-    DiagID = diag::warn_fortify_source_overflow;
-    SourceSize = ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
-    DestinationSize = ComputeSizeArgument(0);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
-  case Builtin::BIbcopy:
-  case Builtin::BI__builtin_bcopy: {
-    DiagID = diag::warn_fortify_source_overflow;
-    SourceSize = ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
-    DestinationSize = ComputeSizeArgument(1);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
+    case Builtin::BIstrncat:
+    case Builtin::BI__builtin_strncat:
+    case Builtin::BIstrncpy:
+    case Builtin::BI__builtin_strncpy:
+    case Builtin::BIstpncpy:
+    case Builtin::BI__builtin_stpncpy: {
+      // Whether these functions overflow depends on the runtime strlen of the
+      // string, not just the buffer size, so emitting the "always overflow"
+      // diagnostic isn't quite right. We should still diagnose passing a 
buffer
+      // size larger than the destination buffer though; this is a runtime 
abort
+      // in _FORTIFY_SOURCE mode, and is quite suspicious otherwise.
+      MaxOperationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
+      DestinationSize = ComputeSizeArgument(0);
+      break;
+    }
 
-  case Builtin::BIsnprintf:
-  case Builtin::BI__builtin_snprintf:
-  case Builtin::BIvsnprintf:
-  case Builtin::BI__builtin_vsnprintf: {
-    DiagID = diag::warn_fortify_source_size_mismatch;
-    SourceSize = ComputeExplicitObjectSizeArgument(1);
-    const auto *FormatExpr = TheCall->getArg(2)->IgnoreParenImpCasts();
-    StringRef FormatStrRef;
-    size_t StrLen;
-    if (SourceSize &&
-        ProcessFormatStringLiteral(FormatExpr, FormatStrRef, StrLen, Context)) 
{
-      EstimateSizeFormatHandler H(FormatStrRef);
-      const char *FormatBytes = FormatStrRef.data();
-      if (!analyze_format_string::ParsePrintfString(
-              H, FormatBytes, FormatBytes + StrLen, getLangOpts(),
-              Context.getTargetInfo(), /*isFreeBSDKPrintf=*/false)) {
-        llvm::APSInt FormatSize =
-            llvm::APSInt::getUnsigned(H.getSizeLowerBound())
-                .extOrTrunc(SizeTypeWidth);
-        if (FormatSize > *SourceSize && *SourceSize != 0) {
-          unsigned TruncationDiagID =
-              H.isKernelCompatible() ? diag::warn_format_truncation
-                                     : 
diag::warn_format_truncation_non_kprintf;
-          SmallString<16> SpecifiedSizeStr;
-          SmallString<16> FormatSizeStr;
-          SourceSize->toString(SpecifiedSizeStr, /*Radix=*/10);
-          FormatSize.toString(FormatSizeStr, /*Radix=*/10);
-          DiagRuntimeBehavior(TheCall->getBeginLoc(), TheCall,
-                              PDiag(TruncationDiagID)
-                                  << GetFunctionName() << SpecifiedSizeStr
-                                  << FormatSizeStr);
+    case Builtin::BImemset:
+    case Builtin::BI__builtin_memset:
+    case Builtin::BIbzero:
+    case Builtin::BI__builtin_bzero: {
+      MinOperationSize = MaxOperationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
+      DestinationSize = ComputeSizeArgument(0);
+      break;
+    }
+
+    case Builtin::BImemcpy:
+    case Builtin::BI__builtin_memcpy:
+    case Builtin::BImemmove:
+    case Builtin::BI__builtin_memmove:
+    case Builtin::BImempcpy:
+    case Builtin::BI__builtin_mempcpy: {
+      MinOperationSize = MaxOperationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
+      DestinationSize = ComputeSizeArgument(0);
+      SourceSize = ComputeSizeArgument(1);
+      break;
+    }
+    case Builtin::BIbcopy:
+    case Builtin::BI__builtin_bcopy: {
+      MinOperationSize = MaxOperationSize =
+          ComputeExplicitObjectSizeArgument(TheCall->getNumArgs() - 1);
+      SourceSize = ComputeSizeArgument(0);
+      DestinationSize = ComputeSizeArgument(1);
+      break;
+    }
+
+    case Builtin::BIsnprintf:
+    case Builtin::BI__builtin_snprintf:
+    case Builtin::BIvsnprintf:
+    case Builtin::BI__builtin_vsnprintf: {
+      MaxOperationSize = ComputeExplicitObjectSizeArgument(1);
+      const auto *FormatExpr = TheCall->getArg(2)->IgnoreParenImpCasts();
+      StringRef FormatStrRef;
+      size_t StrLen;
+      if (MaxOperationSize && ProcessFormatStringLiteral(
+                                  FormatExpr, FormatStrRef, StrLen, Context)) {
+        EstimateSizeFormatHandler H(FormatStrRef);
+        const char *FormatBytes = FormatStrRef.data();
+        if (!analyze_format_string::ParsePrintfString(
+                H, FormatBytes, FormatBytes + StrLen, getLangOpts(),
+                Context.getTargetInfo(), /*isFreeBSDKPrintf=*/false)) {
+          llvm::APSInt FormatSize =
+              llvm::APSInt::getUnsigned(H.getSizeLowerBound())
+                  .extOrTrunc(SizeTypeWidth);
+          if (FormatSize > *MaxOperationSize && *MaxOperationSize != 0) {
+            unsigned TruncationDiagID =
+                H.isKernelCompatible()
+                    ? diag::warn_format_truncation
+                    : diag::warn_format_truncation_non_kprintf;
+            SmallString<16> SpecifiedSizeStr;
+            SmallString<16> FormatSizeStr;
+            MaxOperationSize->toString(SpecifiedSizeStr, /*Radix=*/10);
+            FormatSize.toString(FormatSizeStr, /*Radix=*/10);
+            DiagRuntimeBehavior(TheCall->getBeginLoc(), TheCall,
+                                PDiag(TruncationDiagID)
+                                    << GetFunctionName() << SpecifiedSizeStr
+                                    << FormatSizeStr);
+          }
         }
       }
+      DestinationSize = ComputeSizeArgument(0);
+      const Expr *LenArg = TheCall->getArg(1)->IgnoreCasts();
+      const Expr *Dest = TheCall->getArg(0)->IgnoreCasts();
+      IdentifierInfo *FnInfo = FD->getIdentifier();
+      CheckSizeofMemaccessArgument(LenArg, Dest, FnInfo);
+      break;
+    }
     }
-    DestinationSize = ComputeSizeArgument(0);
-    const Expr *LenArg = TheCall->getArg(1)->IgnoreCasts();
-    const Expr *Dest = TheCall->getArg(0)->IgnoreCasts();
-    IdentifierInfo *FnInfo = FD->getIdentifier();
-    CheckSizeofMemaccessArgument(LenArg, Dest, FnInfo);
-    IsTriggered = CompareSizeSourceToDest() > 0;
-    break;
-  }
   }
 
-  if (!IsTriggered)
-    return;
-
   std::string FunctionName = GetFunctionName();
+  SmallString<16> MaxOpStr;
+  SmallString<16> MinOpStr;
+
+  if (MinOperationSize)
+    MinOperationSize->toString(MinOpStr, /*Radix=*/10);
+  if (MaxOperationSize)
+    MaxOperationSize->toString(MaxOpStr, /*Radix=*/10);
+
+  if (DestinationSize) {
+    SmallString<16> DestinationStr;
+    DestinationSize->toString(DestinationStr, /*Radix=*/10);
+    // Check for definite overflow
+    if (MinOperationSize &&
+        llvm::APSInt::compareValues(*MinOperationSize, *DestinationSize) > 0) {
+      DiagRuntimeBehavior(TheCall->getBeginLoc(), TheCall,
+                          PDiag(DiagOverflowID)
+                              << FunctionName << DestinationStr << MinOpStr);
+    }
+    // Check for possible overflow
+    else if (MaxOperationSize && llvm::APSInt::compareValues(
+                                     *MaxOperationSize, *DestinationSize) > 0) 
{
+      DiagRuntimeBehavior(TheCall->getBeginLoc(), TheCall,
+                          PDiag(DiagMayOverflowID)
+                              << FunctionName << DestinationStr << MaxOpStr);
+    }
+  }
 
-  SmallString<16> DestinationStr;
-  SmallString<16> SourceStr;
-  DestinationSize->toString(DestinationStr, /*Radix=*/10);
-  SourceSize->toString(SourceStr, /*Radix=*/10);
-  DiagRuntimeBehavior(TheCall->getBeginLoc(), TheCall,
-                      PDiag(DiagID)
-                          << FunctionName << DestinationStr << SourceStr);
+  if (SourceSize) {
+    SmallString<16> SourceStr;
+    SourceSize->toString(SourceStr, /*Radix=*/10);
+    // Check for definite over-read
+    if (MinOperationSize &&
+        llvm::APSInt::compareValues(*MinOperationSize, *SourceSize) > 0) {
+      DiagRuntimeBehavior(TheCall->getBeginLoc(), TheCall,
+                          PDiag(DiagOverReadID)
+                              << FunctionName << SourceStr << MinOpStr);
+
+    }
+    // Check for possible over-read
+    else if (MaxOperationSize &&
+             llvm::APSInt::compareValues(*MaxOperationSize, *SourceSize) > 0) {
+      DiagRuntimeBehavior(TheCall->getBeginLoc(), TheCall,
+                          PDiag(DiagMayOverReadID)
+                              << FunctionName << SourceStr << MaxOpStr);
+    }
+  }
 }
 
 static bool BuiltinSEHScopeCheck(Sema &SemaRef, CallExpr *TheCall,
diff --git a/clang/test/Sema/builtin-memcpy.c b/clang/test/Sema/builtin-memcpy.c
index 2a55e78034a02..af7f2034f3c30 100644
--- a/clang/test/Sema/builtin-memcpy.c
+++ b/clang/test/Sema/builtin-memcpy.c
@@ -7,7 +7,8 @@
 /// Zero-sized structs should not crash.
 int b() {
   struct {      } a[10];
-  __builtin_memcpy(&a[2], a, 2); // c-warning {{buffer has size 0, but size 
argument is 2}}
+  __builtin_memcpy(&a[2], a, 2); // c-warning {{buffer has size 0, but size 
argument is 2}} \
+                                 // c-warning {{buffer has size 0, but size 
argument is 2}}
   return 0;
 }
 
diff --git a/clang/test/Sema/warn-fortify-source-prototype-gate.c 
b/clang/test/Sema/warn-fortify-source-prototype-gate.c
index 2112af03e1734..575162dfb8664 100644
--- a/clang/test/Sema/warn-fortify-source-prototype-gate.c
+++ b/clang/test/Sema/warn-fortify-source-prototype-gate.c
@@ -44,19 +44,20 @@ void test_getcwd_wrong_param(void) {
   getcwd(42, 8);
 }
 
-// pread64 with int offset on x86_64: off64_t is required to be 64-bit signed,
-// so int (32-bit on x86_64) is not POSIX pread64.
-ssize_t pread64(int, void *, size_t, int);
+// pread64 with 'char *' buffer instead of POSIX 'void *'. Newlib-style
+// syscall stubs commonly use this shape; the pointer-type mismatch trips
+// the prototype gate.
+int pread64(int, char *, size_t, long long);
 
-void test_pread64_narrow_offset(void) {
+void test_pread64_newlib_buffer(void) {
   char b[4];
   pread64(0, b, 8, 0);
 }
 
-// pwrite64 with int offset: same as above.
-ssize_t pwrite64(int, const void *, size_t, int);
+// pwrite64 with 'const char *' instead of POSIX 'const void *': same shape.
+int pwrite64(int, const char *, size_t, long long);
 
-void test_pwrite64_narrow_offset(void) {
+void test_pwrite64_newlib_buffer(void) {
   char b[4];
   pwrite64(0, b, 8, 0);
 }
diff --git a/clang/test/Sema/warn-fortify-source.c 
b/clang/test/Sema/warn-fortify-source.c
index 5c4df36d2c88d..14ffd163c5d09 100644
--- a/clang/test/Sema/warn-fortify-source.c
+++ b/clang/test/Sema/warn-fortify-source.c
@@ -45,6 +45,8 @@ void call_memcpy(void) {
   char dst[10];
   char src[20];
   memcpy(dst, src, 20); // expected-warning {{memcpy' will always overflow; 
destination buffer has size 10, but size argument is 20}}
+  memcpy(dst, src, 21); // expected-warning {{memcpy' will always overflow; 
destination buffer has size 10, but size argument is 21}}
+                        // expected-warning@-1 {{memcpy' will always 
over-read; source buffer has size 20, but size argument is 21}}
 
   if (sizeof(dst) == sizeof(src))
     memcpy(dst, src, 20); // no warning, unreachable
@@ -58,18 +60,33 @@ void call_memcpy_type(void) {
   struct pair p;
   char buf[20];
   memcpy(&p.first, buf, 20); // expected-warning {{memcpy' will always 
overflow; destination buffer has size 8, but size argument is 20}}
+  memcpy(&p.first, buf, 21); // expected-warning {{memcpy' will always 
overflow; destination buffer has size 8, but size argument is 21}}
+                             // expected-warning@-1 {{memcpy' will always 
over-read; source buffer has size 20, but size argument is 21}}
+}
+
+void call_memcpy_chk(void) {
+  char dst[10];
+  char src[10];
+  char src4[4];
+  __builtin___memcpy_chk(dst, src, 10, 10);
+  __builtin___memcpy_chk(dst, src, 10, 9); // expected-warning {{memcpy' will 
always overflow; destination buffer has size 9, but size argument is 10}}
+  __builtin___memcpy_chk(dst, src4, 5, 10); // expected-warning {{'memcpy' 
will always over-read; source buffer has size 4, but size argument is 5}}
+  __builtin___memmove_chk(dst, src4, 5, 10); // expected-warning {{'memmove' 
will always over-read; source buffer has size 4, but size argument is 5}}
+  __builtin___mempcpy_chk(dst, src4, 5, 10); // expected-warning {{'mempcpy' 
will always over-read; source buffer has size 4, but size argument is 5}}
 }
 
 void call_strncat(void) {
   char s1[10], s2[20];
   __builtin_strncat(s2, s1, 20);
   __builtin_strncat(s1, s2, 20); // expected-warning {{'strncat' size argument 
is too large; destination buffer has size 10, but size argument is 20}}
+  __builtin_strncat(s1, "abcd", 20); // expected-warning {{'strncat' size 
argument is too large; destination buffer has size 10, but size argument is 20}}
 }
 
 void call_strncpy(void) {
   char s1[10], s2[20];
   __builtin_strncpy(s2, s1, 20);
   __builtin_strncpy(s1, s2, 20); // expected-warning {{'strncpy' size argument 
is too large; destination buffer has size 10, but size argument is 20}}
+  __builtin_strncpy(s1, "abcd", 20); // expected-warning {{'strncpy' size 
argument is too large; destination buffer has size 10, but size argument is 20}}
 }
 
 void call_stpncpy(void) {
@@ -107,9 +124,17 @@ void call_stpcpy(void) {
   __builtin_stpcpy(dst2, src); // expected-warning {{'stpcpy' will always 
overflow; destination buffer has size 4, but the source string has length 5 
(including NUL byte)}}
 }
 
+void call_stpcpy_chk(void) {
+  const char *const src = "abcd";
+  char dst1[5];
+  __builtin___stpcpy_chk(dst1, src, 5);
+  __builtin___stpcpy_chk(dst1, src, 4); // expected-warning {{'stpcpy' will 
always overflow; destination buffer has size 4, but the source string has 
length 5 (including NUL byte)}}
+}
+
 void call_memmove(void) {
   char s1[10], s2[20];
-  __builtin_memmove(s2, s1, 20);
+  __builtin_memmove(s2, s1, 10);
+  __builtin_memmove(s2, s1, 20); // expected-warning {{'memmove' will always 
over-read; source buffer has size 10, but size argument is 20}}
   __builtin_memmove(s1, s2, 20); // expected-warning {{'memmove' will always 
overflow; destination buffer has size 10, but size argument is 20}}
 }
 
@@ -323,11 +348,23 @@ template <int A, int B>
 void call_memcpy_dep() {
   char bufferA[A];
   char bufferB[B];
-  memcpy(bufferA, bufferB, 10); // expected-warning{{'memcpy' will always 
overflow; destination buffer has size 9, but size argument is 10}}
+  if (sizeof(bufferA) < 10 && sizeof(bufferB) < 10) {
+    memcpy(bufferA, bufferB, 10); // expected-warning{{'memcpy' will always 
overflow; destination buffer has size 9, but size argument is 10}} \
+                                  // expected-warning{{'memcpy' will always 
over-read; source buffer has size 9, but size argument is 10}}
+  } else if (sizeof(bufferA) < 10) {
+    memcpy(bufferA, bufferB, 10); // expected-warning{{'memcpy' will always 
overflow; destination buffer has size 9, but size argument is 10}}
+  } else if (sizeof(bufferB) < 10) {
+    memcpy(bufferA, bufferB, 10); // expected-warning{{'memcpy' will always 
over-read; source buffer has size 9, but size argument is 10}}
+  } else {
+    memcpy(bufferA, bufferB, 10);
+  }
+
 }
 
 void call_call_memcpy() {
-  call_memcpy_dep<10, 9>();
+  call_memcpy_dep<10, 10>();
+  call_memcpy_dep<10, 9>(); // expected-note {{in instantiation of function 
template specialization 'call_memcpy_dep<10, 9>' requested here}}
   call_memcpy_dep<9, 10>(); // expected-note {{in instantiation of function 
template specialization 'call_memcpy_dep<9, 10>' requested here}}
+  call_memcpy_dep<9, 9>(); // expected-note {{in instantiation of function 
template specialization 'call_memcpy_dep<9, 9>' requested here}}
 }
 #endif

>From f5c9663ecc5da8018d25c6cb6dbf9c6331d6cfe4 Mon Sep 17 00:00:00 2001
From: Colin Kinloch <[email protected]>
Date: Thu, 6 Nov 2025 00:00:53 +0000
Subject: [PATCH 3/8] [clang][Sema] Use relative line number for template
 warnings

---
 clang/test/Sema/warn-fortify-source.c | 12 +++++-------
 1 file changed, 5 insertions(+), 7 deletions(-)

diff --git a/clang/test/Sema/warn-fortify-source.c 
b/clang/test/Sema/warn-fortify-source.c
index 14ffd163c5d09..9137bf433073b 100644
--- a/clang/test/Sema/warn-fortify-source.c
+++ b/clang/test/Sema/warn-fortify-source.c
@@ -348,17 +348,15 @@ template <int A, int B>
 void call_memcpy_dep() {
   char bufferA[A];
   char bufferB[B];
+  memcpy(bufferA, bufferB, 10);
   if (sizeof(bufferA) < 10 && sizeof(bufferB) < 10) {
-    memcpy(bufferA, bufferB, 10); // expected-warning{{'memcpy' will always 
overflow; destination buffer has size 9, but size argument is 10}} \
-                                  // expected-warning{{'memcpy' will always 
over-read; source buffer has size 9, but size argument is 10}}
+    // expected-warning@-2{{'memcpy' will always overflow; destination buffer 
has size 9, but size argument is 10}}
+    // expected-warning@-3{{'memcpy' will always over-read; source buffer has 
size 9, but size argument is 10}}
   } else if (sizeof(bufferA) < 10) {
-    memcpy(bufferA, bufferB, 10); // expected-warning{{'memcpy' will always 
overflow; destination buffer has size 9, but size argument is 10}}
+    // expected-warning@-5{{'memcpy' will always overflow; destination buffer 
has size 9, but size argument is 10}}
   } else if (sizeof(bufferB) < 10) {
-    memcpy(bufferA, bufferB, 10); // expected-warning{{'memcpy' will always 
over-read; source buffer has size 9, but size argument is 10}}
-  } else {
-    memcpy(bufferA, bufferB, 10);
+    // expected-warning@-7{{'memcpy' will always over-read; source buffer has 
size 9, but size argument is 10}}
   }
-
 }
 
 void call_call_memcpy() {

>From 2fa23a9f4019b1e3a2e35e3b605e5edc788b204c Mon Sep 17 00:00:00 2001
From: Colin Kinloch <[email protected]>
Date: Thu, 6 Nov 2025 01:58:37 +0000
Subject: [PATCH 4/8] [clang][test] Add over-read warnings to analysis tests

---
 clang/test/Analysis/array-struct.c    |  4 ++--
 clang/test/Analysis/bstring.c         | 19 ++++++++++++++++++-
 clang/test/Analysis/malloc.c          |  2 +-
 clang/test/Analysis/pr22954.c         |  1 +
 clang/test/Sema/builtin-object-size.c |  2 +-
 5 files changed, 23 insertions(+), 5 deletions(-)

diff --git a/clang/test/Analysis/array-struct.c 
b/clang/test/Analysis/array-struct.c
index f0eba86fe71bf..691bb1348a44f 100644
--- a/clang/test/Analysis/array-struct.c
+++ b/clang/test/Analysis/array-struct.c
@@ -175,12 +175,12 @@ void f17(void) {
     x = 1;
 }
 
-void read(char*);
+void readp(char*);
 
 void f18(void) {
   char *q;
   char *p = (char *) __builtin_alloca(10);
-  read(p);
+  readp(p);
   q = p;
   q++;
   if (*q) { // no-warning
diff --git a/clang/test/Analysis/bstring.c b/clang/test/Analysis/bstring.c
index 810241accffa2..f940cd957457c 100644
--- a/clang/test/Analysis/bstring.c
+++ b/clang/test/Analysis/bstring.c
@@ -93,6 +93,9 @@ void memcpy1 (void) {
   char dst[10];
 
   memcpy(dst, src, 5); // expected-warning{{Memory copy function accesses 
out-of-bound array element}}
+#if !defined(VARIANT) || defined(USE_BUILTINS)
+  // expected-warning@-2{{memcpy' will always over-read; source buffer has 
size 4, but size argument is 5}}
+#endif
 }
 
 void memcpy2 (void) {
@@ -117,6 +120,9 @@ void memcpy4 (void) {
   char dst[10];
 
   memcpy(dst+2, src+2, 3); // expected-warning{{Memory copy function accesses 
out-of-bound array element}}
+#if !defined(VARIANT) || defined(USE_BUILTINS)
+  // expected-warning@-2{{memcpy' will always over-read; source buffer has 
size 2, but size argument is 3}}
+#endif
 }
 
 void memcpy5(void) {
@@ -219,6 +225,9 @@ void mempcpy1 (void) {
   char dst[10];
 
   mempcpy(dst, src, 5); // expected-warning{{Memory copy function accesses 
out-of-bound array element}}
+#if !defined(VARIANT) || defined(USE_BUILTINS)
+  // expected-warning@-2{{mempcpy' will always over-read; source buffer has 
size 4, but size argument is 5}}
+#endif
 }
 
 void mempcpy2 (void) {
@@ -243,6 +252,9 @@ void mempcpy4 (void) {
   char dst[10];
 
   mempcpy(dst+2, src+2, 3); // expected-warning{{Memory copy function accesses 
out-of-bound array element}}
+#if !defined(VARIANT) || defined(USE_BUILTINS)
+  // expected-warning@-2{{mempcpy' will always over-read; source buffer has 
size 2, but size argument is 3}}
+#endif
 }
 
 void mempcpy5(void) {
@@ -384,6 +396,9 @@ void memmove1 (void) {
   char dst[10];
 
   memmove(dst, src, 5); // expected-warning{{out-of-bound}}
+#if !defined(VARIANT) || defined(USE_BUILTINS)
+  // expected-warning@-2{{memmove' will always over-read; source buffer has 
size 4, but size argument is 5}}
+#endif
 }
 
 void memmove2 (void) {
@@ -502,6 +517,7 @@ void bcopy1 (void) {
   char dst[10];
 
   bcopy(src, dst, 5); // expected-warning{{out-of-bound}}
+                      // expected-warning@-1{{bcopy' will always over-read; 
source buffer has size 4, but size argument is 5}}
 }
 
 void bcopy2 (void) {
@@ -541,6 +557,7 @@ void nocrash_on_empty_struct_memcpy(void) {
   __builtin_memcpy(&a[2], a, 2); // no-crash
 #if !defined(_WIN32) || defined(__MINGW32__)
   // expected-warning@-2 {{'memcpy' will always overflow; destination buffer 
has size 0, but size argument is 2}}
-  // expected-warning@-3 {{Memory copy function overflows the destination 
buffer}}
+  // expected-warning@-3 {{'memcpy' will always over-read; source buffer has 
size 0, but size argument is 2}}
+  // expected-warning@-4 {{Memory copy function overflows the destination 
buffer}}
 #endif
 }
diff --git a/clang/test/Analysis/malloc.c b/clang/test/Analysis/malloc.c
index 849ab3a3a0f37..199deda3db877 100644
--- a/clang/test/Analysis/malloc.c
+++ b/clang/test/Analysis/malloc.c
@@ -890,7 +890,7 @@ void overlappingMemcpyDoesNotSinkPath(char *s) {
 // Treat source buffer contents as escaped.
 void escapeSourceContents(char *s) {
   char *p = malloc(12);
-  memcpy(s, &p, 12); // no warning
+  memcpy(s, &p, 12); // expected-warning{{memcpy' will always over-read; 
source buffer has size}}
 
   void *p1 = malloc(7);
   char *a;
diff --git a/clang/test/Analysis/pr22954.c b/clang/test/Analysis/pr22954.c
index 3d1cac1972066..629c293c81784 100644
--- a/clang/test/Analysis/pr22954.c
+++ b/clang/test/Analysis/pr22954.c
@@ -537,6 +537,7 @@ int f262(void) {
   a262.s2 = strdup("hello");
   char input[] = {'a', 'b', 'c', 'd'};
   memcpy(a262.s1, input, -1); // expected-warning{{'memcpy' will always 
overflow; destination buffer has size 16, but size argument is 
18446744073709551615}}
+                              // expected-warning@-1{{'memcpy' will always 
over-read; source buffer has size 4, but size argument is 18446744073709551615}}
   clang_analyzer_eval(a262.s1[0] == 1); // expected-warning{{UNKNOWN}}\
   expected-warning{{Potential leak of memory pointed to by 'a262.s2'}}
   clang_analyzer_eval(a262.s1[1] == 1); // expected-warning{{UNKNOWN}}
diff --git a/clang/test/Sema/builtin-object-size.c 
b/clang/test/Sema/builtin-object-size.c
index a763c24fd6620..b8a8407b54b3b 100644
--- a/clang/test/Sema/builtin-object-size.c
+++ b/clang/test/Sema/builtin-object-size.c
@@ -50,7 +50,7 @@ void f5(void)
 {
   char buf[10];
   memset((void *)0x100000000ULL, 0, 0x1000);
-  memcpy((char *)NULL + 0x10000, buf, 0x10);
+  memcpy((char *)NULL + 0x10000, buf, 0x10); // expected-warning {{'memcpy' 
will always over-read; source buffer has size 10, but size argument is 16}}
   memcpy1((char *)NULL + 0x10000, buf, 0x10); // expected-error {{argument 
value 4 is outside the valid range [0, 3]}}
 }
 

>From 9d2bbd7ff3e27e9bbe1fa8f4e362a0403e7ecde3 Mon Sep 17 00:00:00 2001
From: Colin Kinloch <[email protected]>
Date: Thu, 6 Nov 2025 01:58:37 +0000
Subject: [PATCH 5/8] [test] Adjust libcxx and asan tests for new fortify
 warnings

Two tests outside clang/test/Analysis need adjustment after introducing
fortify warnings for unistd.h I/O:

* The Windows asan EH-codegen test now triggers new fortify warnings on
  non-MSVC builds; pass -Wno-fortify-source there (MSVC mode is unaffected).

* The libcxx __constexpr_wmemchr test passed an int character constant to
  a wchar_t parameter; use L'n' so the literal type matches the parameter.
---
 compiler-rt/test/asan/TestCases/Windows/issue64990.cpp          | 2 +-
 .../libcxx/strings/c.strings/constexpr.cwchar.compile.pass.cpp  | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/compiler-rt/test/asan/TestCases/Windows/issue64990.cpp 
b/compiler-rt/test/asan/TestCases/Windows/issue64990.cpp
index 5222ec6e08191..785e027a31be8 100644
--- a/compiler-rt/test/asan/TestCases/Windows/issue64990.cpp
+++ b/compiler-rt/test/asan/TestCases/Windows/issue64990.cpp
@@ -1,5 +1,5 @@
 // Repro for the issue #64990: Asan with Windows EH generates __asan_xxx 
runtime calls without required funclet tokens
-// RUN: %clang_cl_asan %Od %if MSVC %{ /Oi %} %s -EHsc %Fe%t
+// RUN: %clang_cl_asan %Od %if MSVC %{ /Oi %} %else %{ -Wno-fortify-source %} 
%s -EHsc %Fe%t
 // RUN: not %run %t 2>&1 | FileCheck %s
 
 // UNSUPPORTED: target={{.*-windows-gnu}}
diff --git 
a/libcxx/test/libcxx/strings/c.strings/constexpr.cwchar.compile.pass.cpp 
b/libcxx/test/libcxx/strings/c.strings/constexpr.cwchar.compile.pass.cpp
index 02feed064eacc..7d9548e500da8 100644
--- a/libcxx/test/libcxx/strings/c.strings/constexpr.cwchar.compile.pass.cpp
+++ b/libcxx/test/libcxx/strings/c.strings/constexpr.cwchar.compile.pass.cpp
@@ -21,6 +21,6 @@ static_assert(std::__constexpr_wmemcmp(L"Banane", L"Bananf", 
6) == -1, "");
 
 constexpr bool test_constexpr_wmemchr() {
   const wchar_t str[] = L"Banane";
-  return std::__constexpr_wmemchr(str, 'n', 6) == str + 2;
+  return std::__constexpr_wmemchr(str, L'n', 6) == str + 2;
 }
 static_assert(test_constexpr_wmemchr(), "");

>From b8cfedf708d6770a68608c526cb4f3a91df739fe Mon Sep 17 00:00:00 2001
From: Denys Fedoryshchenko <[email protected]>
Date: Tue, 5 May 2026 15:11:53 +0300
Subject: [PATCH 6/8] [clang][analyzer] Check SSIZE_MAX bounds for unistd I/O
 sizes

Add StdLibraryFunctionsChecker argument constraints that reject size arguments 
greater than SSIZE_MAX for read, write, readlink, and readlinkat.

This catches common problematic cases such as passing -1 to a size_t parameter, 
using very large constants, or relying on a size that is valid on one platform 
but exceeds SSIZE_MAX on another.

This may produce new warnings for existing code that passes size arguments 
larger than SSIZE_MAX to these functions.

Use the visible ssize_t type to derive the platform-specific maximum. For 
readlink and readlinkat, tighten the existing bufsize constraint from the full 
size_t range to SSIZE_MAX.
---
 .../Checkers/StdLibraryFunctionsChecker.cpp   | 17 ++++++-----
 .../std-c-library-functions-arg-constraints.c | 30 +++++++++++++++++++
 2 files changed, 40 insertions(+), 7 deletions(-)

diff --git a/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp 
b/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp
index 8a3ee4443eb16..666caf31e4610 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp
@@ -2073,12 +2073,17 @@ void StdLibraryFunctionsChecker::initFunctionSummaries(
 
   std::optional<QualType> Ssize_tTy = lookupTy("ssize_t");
   std::optional<RangeInt> Ssize_tMax = getMaxValue(Ssize_tTy);
+  auto ValidSsize_tSize = [&](ArgNo ArgN) {
+    return ArgumentCondition(ArgN, WithinRange, Range(0, Ssize_tMax),
+                             "a value not greater than SSIZE_MAX");
+  };
 
   auto ReadSummary =
       Summary(NoEvalCall)
           .Case({ReturnValueCondition(LessThanOrEq, ArgNo(2)),
                  ReturnValueCondition(WithinRange, Range(-1, Ssize_tMax))},
-                ErrnoIrrelevant);
+                ErrnoIrrelevant)
+          .ArgConstraint(ValidSsize_tSize(ArgNo(2)));
 
   // FIXME these are actually defined by POSIX and not by the C standard, we
   // should handle them together with the rest of the POSIX functions.
@@ -3012,7 +3017,7 @@ void StdLibraryFunctionsChecker::initFunctionSummaries(
             ArgTypes{ConstCharPtrRestrictTy, CharPtrRestrictTy, SizeTyCanonTy},
             RetType{Ssize_tTy}),
         Summary(NoEvalCall)
-            .Case({ArgumentCondition(2, WithinRange, Range(1, IntMax)),
+            .Case({ArgumentCondition(2, WithinRange, Range(1, Ssize_tMax)),
                    ReturnValueCondition(LessThanOrEq, ArgNo(2)),
                    ReturnValueCondition(WithinRange, Range(1, Ssize_tMax))},
                   ErrnoMustNotBeChecked, GenericSuccessMsg)
@@ -3025,8 +3030,7 @@ void StdLibraryFunctionsChecker::initFunctionSummaries(
             .ArgConstraint(NotNull(ArgNo(1)))
             .ArgConstraint(BufferSize(/*Buffer=*/ArgNo(1),
                                       /*BufSize=*/ArgNo(2)))
-            .ArgConstraint(
-                ArgumentCondition(2, WithinRange, Range(0, SizeMax))));
+            .ArgConstraint(ValidSsize_tSize(ArgNo(2))));
 
     // ssize_t readlinkat(int fd, const char *restrict path,
     //                    char *restrict buf, size_t bufsize);
@@ -3036,7 +3040,7 @@ void StdLibraryFunctionsChecker::initFunctionSummaries(
                            SizeTyCanonTy},
                   RetType{Ssize_tTy}),
         Summary(NoEvalCall)
-            .Case({ArgumentCondition(3, WithinRange, Range(1, IntMax)),
+            .Case({ArgumentCondition(3, WithinRange, Range(1, Ssize_tMax)),
                    ReturnValueCondition(LessThanOrEq, ArgNo(3)),
                    ReturnValueCondition(WithinRange, Range(1, Ssize_tMax))},
                   ErrnoMustNotBeChecked, GenericSuccessMsg)
@@ -3050,8 +3054,7 @@ void StdLibraryFunctionsChecker::initFunctionSummaries(
             .ArgConstraint(NotNull(ArgNo(2)))
             .ArgConstraint(BufferSize(/*Buffer=*/ArgNo(2),
                                       /*BufSize=*/ArgNo(3)))
-            .ArgConstraint(
-                ArgumentCondition(3, WithinRange, Range(0, SizeMax))));
+            .ArgConstraint(ValidSsize_tSize(ArgNo(3))));
 
     // int renameat(int olddirfd, const char *oldpath, int newdirfd, const char
     // *newpath);
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 0b817dda98c72..c04d7921afb9f 100644
--- a/clang/test/Analysis/std-c-library-functions-arg-constraints.c
+++ b/clang/test/Analysis/std-c-library-functions-arg-constraints.c
@@ -370,3 +370,33 @@ void test_file_fd_at_functions() {
   (void)readlinkat(AT_FDCWD, "newpath", Buf, 10);
   (void)renameat(AT_FDCWD, "oldpath", AT_FDCWD, "newpath");
 }
+
+#define SSIZE_MAX_PLUS_ONE ((size_t)1 << (sizeof(size_t) * __CHAR_BIT__ - 1))
+
+void test_read_ssize_max_io_size(int fd, char *Buf) {
+  read(fd, Buf, SSIZE_MAX_PLUS_ONE);
+  // report-warning@-1{{The 3rd argument to 'read' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 3rd argument to 'read' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 3rd argument to 'read' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+}
+
+void test_write_ssize_max_io_size(int fd, char *Buf) {
+  write(fd, Buf, SSIZE_MAX_PLUS_ONE);
+  // report-warning@-1{{The 3rd argument to 'write' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 3rd argument to 'write' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 3rd argument to 'write' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+}
+
+void test_readlink_ssize_max_io_size(char *Buf) {
+  readlink("path", Buf, SSIZE_MAX_PLUS_ONE);
+  // report-warning@-1{{The 3rd argument to 'readlink' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 3rd argument to 'readlink' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 3rd argument to 'readlink' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+}
+
+void test_readlinkat_ssize_max_io_size(char *Buf) {
+  readlinkat(AT_FDCWD, "path", Buf, SSIZE_MAX_PLUS_ONE);
+  // report-warning@-1{{The 4th argument to 'readlinkat' is 
9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 4th argument to 'readlinkat' is 
9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 4th argument to 'readlinkat' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+}

>From 0465c9241cb351c36c45b04c16730bfb5b01f590 Mon Sep 17 00:00:00 2001
From: Denys Fedoryshchenko <[email protected]>
Date: Tue, 5 May 2026 15:14:48 +0300
Subject: [PATCH 7/8] [clang][analyzer] Add summaries for pread and pwrite

Add StdLibraryFunctionsChecker summaries for pread, pread64, pwrite, and 
pwrite64.

Reuse the existing read/write summary so the new modeled functions inherit the 
SSIZE_MAX size constraint and return-value bounds. Add POSIX test declarations, 
loaded-summary checks, and diagnostic coverage for the new summaries.

Signed-off-by: Denys Fedoryshchenko <[email protected]>
---
 .../Checkers/StdLibraryFunctionsChecker.cpp   | 29 +++++++++++++++++++
 .../Inputs/std-c-library-functions-POSIX.h    |  4 +++
 .../Analysis/std-c-library-functions-POSIX.c  |  4 +++
 .../std-c-library-functions-arg-constraints.c | 28 ++++++++++++++++++
 4 files changed, 65 insertions(+)

diff --git a/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp 
b/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp
index 666caf31e4610..b579255892084 100644
--- a/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp
+++ b/clang/lib/StaticAnalyzer/Checkers/StdLibraryFunctionsChecker.cpp
@@ -3009,6 +3009,35 @@ void StdLibraryFunctionsChecker::initFunctionSummaries(
             .ArgConstraint(
                 ArgumentCondition(0, WithinRange, Range(0, IntMax))));
 
+    // ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
+    addToFunctionSummaryMap(
+        "pread",
+        Signature(ArgTypes{IntTy, VoidPtrTy, SizeTyCanonTy, Off_tTy},
+                  RetType{Ssize_tTy}),
+        ReadSummary);
+
+    // ssize_t pread64(int fildes, void *buf, size_t nbyte, off64_t offset);
+    addToFunctionSummaryMap(
+        "pread64",
+        Signature(ArgTypes{IntTy, VoidPtrTy, SizeTyCanonTy, Off64_tTy},
+                  RetType{Ssize_tTy}),
+        ReadSummary);
+
+    // ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
+    addToFunctionSummaryMap(
+        "pwrite",
+        Signature(ArgTypes{IntTy, ConstVoidPtrTy, SizeTyCanonTy, Off_tTy},
+                  RetType{Ssize_tTy}),
+        ReadSummary);
+
+    // ssize_t pwrite64(int fildes, const void *buf, size_t nbyte,
+    //                  off64_t offset);
+    addToFunctionSummaryMap(
+        "pwrite64",
+        Signature(ArgTypes{IntTy, ConstVoidPtrTy, SizeTyCanonTy, Off64_tTy},
+                  RetType{Ssize_tTy}),
+        ReadSummary);
+
     // ssize_t readlink(const char *restrict path, char *restrict buf,
     //                  size_t bufsize);
     addToFunctionSummaryMap(
diff --git a/clang/test/Analysis/Inputs/std-c-library-functions-POSIX.h 
b/clang/test/Analysis/Inputs/std-c-library-functions-POSIX.h
index b146068eedb08..83d753a22b347 100644
--- a/clang/test/Analysis/Inputs/std-c-library-functions-POSIX.h
+++ b/clang/test/Analysis/Inputs/std-c-library-functions-POSIX.h
@@ -124,6 +124,10 @@ void *mmap(void *addr, size_t length, int prot, int flags, 
int fd, off_t offset)
 void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off64_t 
offset);
 int pipe(int fildes[2]);
 off_t lseek(int fildes, off_t offset, int whence);
+ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
+ssize_t pread64(int fildes, void *buf, size_t nbyte, off64_t offset);
+ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset);
+ssize_t pwrite64(int fildes, const void *buf, size_t nbyte, off64_t offset);
 ssize_t readlink(const char *restrict path, char *restrict buf, size_t 
bufsize);
 ssize_t readlinkat(int fd, const char *restrict path, char *restrict buf, 
size_t bufsize);
 int renameat(int olddirfd, const char *oldpath, int newdirfd, const char 
*newpath);
diff --git a/clang/test/Analysis/std-c-library-functions-POSIX.c 
b/clang/test/Analysis/std-c-library-functions-POSIX.c
index f6d88e6c1502d..619a049d79055 100644
--- a/clang/test/Analysis/std-c-library-functions-POSIX.c
+++ b/clang/test/Analysis/std-c-library-functions-POSIX.c
@@ -98,6 +98,10 @@
 // CHECK: Loaded summary for: void *mmap64(void *addr, size_t length, int 
prot, int flags, int fd, off64_t offset)
 // CHECK: Loaded summary for: int pipe(int fildes[2])
 // CHECK: Loaded summary for: off_t lseek(int fildes, off_t offset, int whence)
+// CHECK: Loaded summary for: ssize_t pread(int fildes, void *buf, size_t 
nbyte, off_t offset)
+// CHECK: Loaded summary for: ssize_t pread64(int fildes, void *buf, size_t 
nbyte, off64_t offset)
+// CHECK: Loaded summary for: ssize_t pwrite(int fildes, const void *buf, 
size_t nbyte, off_t offset)
+// CHECK: Loaded summary for: ssize_t pwrite64(int fildes, const void *buf, 
size_t nbyte, off64_t offset)
 // CHECK: Loaded summary for: ssize_t readlink(const char *restrict path, char 
*restrict buf, size_t bufsize)
 // CHECK: Loaded summary for: ssize_t readlinkat(int fd, const char *restrict 
path, char *restrict buf, size_t bufsize)
 // CHECK: Loaded summary for: int renameat(int olddirfd, const char *oldpath, 
int newdirfd, const char *newpath)
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 c04d7921afb9f..1247b4c43e589 100644
--- a/clang/test/Analysis/std-c-library-functions-arg-constraints.c
+++ b/clang/test/Analysis/std-c-library-functions-arg-constraints.c
@@ -387,6 +387,34 @@ void test_write_ssize_max_io_size(int fd, char *Buf) {
   // bugpath-note@-3{{The 3rd argument to 'write' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
 }
 
+void test_pread_ssize_max_io_size(int fd, char *Buf) {
+  pread(fd, Buf, SSIZE_MAX_PLUS_ONE, 0);
+  // report-warning@-1{{The 3rd argument to 'pread' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 3rd argument to 'pread' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 3rd argument to 'pread' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+}
+
+void test_pread64_ssize_max_io_size(int fd, char *Buf) {
+  pread64(fd, Buf, SSIZE_MAX_PLUS_ONE, 0);
+  // report-warning@-1{{The 3rd argument to 'pread64' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 3rd argument to 'pread64' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 3rd argument to 'pread64' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+}
+
+void test_pwrite_ssize_max_io_size(int fd, char *Buf) {
+  pwrite(fd, Buf, SSIZE_MAX_PLUS_ONE, 0);
+  // report-warning@-1{{The 3rd argument to 'pwrite' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 3rd argument to 'pwrite' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 3rd argument to 'pwrite' is 9223372036854775808 but 
should be a value not greater than SSIZE_MAX}}
+}
+
+void test_pwrite64_ssize_max_io_size(int fd, char *Buf) {
+  pwrite64(fd, Buf, SSIZE_MAX_PLUS_ONE, 0);
+  // report-warning@-1{{The 3rd argument to 'pwrite64' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-warning@-2{{The 3rd argument to 'pwrite64' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+  // bugpath-note@-3{{The 3rd argument to 'pwrite64' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}
+}
+
 void test_readlink_ssize_max_io_size(char *Buf) {
   readlink("path", Buf, SSIZE_MAX_PLUS_ONE);
   // report-warning@-1{{The 3rd argument to 'readlink' is 9223372036854775808 
but should be a value not greater than SSIZE_MAX}}

>From a7dc27223bf19e32c28e4ebc2f939530122682a5 Mon Sep 17 00:00:00 2001
From: Denys Fedoryshchenko <[email protected]>
Date: Tue, 12 May 2026 01:10:22 +0300
Subject: [PATCH 8/8] [clang][docs] Add release notes for fortify-source and
 analyzer changes

Note the new -Wfortify-source coverage of POSIX unistd.h I/O functions
(read/write/pread/pwrite/readlink/readlinkat/getcwd) and the fix for
source over-read detection on memcpy_chk/memmove_chk/mempcpy_chk/
memccpy_chk. Note the new unix.StdCLibraryFunctions SSIZE_MAX
constraints on read/write/readlink/readlinkat and the new pread/pwrite
summaries.
---
 clang/docs/ReleaseNotes.rst | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index c71f7b173259f..58fc2e5b26c19 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -504,6 +504,17 @@ Improvements to Clang's diagnostics
 
 - Added ``-Wattribute-alias`` to diagnose type mismatches between an alias and 
its aliased function. (#GH195550)
 
+- ``-Wfortify-source`` now diagnoses buffer-size mismatches in calls to the
+  POSIX ``unistd.h`` I/O functions ``read``, ``write``, ``pread``,
+  ``pread64``, ``pwrite``, ``pwrite64``, ``readlink``, ``readlinkat``, and
+  ``getcwd`` when the buffer and the size argument are both statically
+  known.
+
+- ``-Wfortify-source`` now flags source-buffer over-reads through
+  ``__builtin___memcpy_chk``, ``__builtin___memmove_chk``,
+  ``__builtin___mempcpy_chk``, and ``__builtin___memccpy_chk``, matching the
+  behavior already provided for the non-``_chk`` variants.
+
 Improvements to Clang's time-trace
 ----------------------------------
 
@@ -790,6 +801,19 @@ Crash and bug fixes
 - Fixed ``security.VAList`` checker producing false positives when analyzing
   C23 code where ``va_start`` expands to ``__builtin_c23_va_start``.
 
+Improvements
+^^^^^^^^^^^^
+
+- ``unix.StdCLibraryFunctions`` now diagnoses size arguments greater than
+  ``SSIZE_MAX`` passed to ``read``, ``write``, ``readlink``, and
+  ``readlinkat``. This catches common mistakes such as passing ``-1`` to a
+  ``size_t`` parameter or using a constant that is valid on one platform
+  but exceeds ``SSIZE_MAX`` on another.
+
+- ``unix.StdCLibraryFunctions`` now models ``pread``, ``pread64``,
+  ``pwrite``, and ``pwrite64`` with the same size constraint and
+  return-value bounds as ``read`` and ``write``.
+
 .. comment:
   This is for the Static Analyzer.
   Using the caret `^^^` underlining for subsections:

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to