[libcxx-commits] [clang] [compiler-rt] [libcxx] [clang] Add unistd fortify checks and analyzer size constraints (PR #196499)
Denys Fedoryshchenko via libcxx-commits
libcxx-commits at lists.llvm.org
Fri May 15 01:37:09 PDT 2026
https://github.com/nuclearcat updated https://github.com/llvm/llvm-project/pull/196499
>From 385487ec17c583cfe18656f06d951235a913698e Mon Sep 17 00:00:00 2001
From: Colin Kinloch <colin.kinloch at collabora.com>
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 <colin.kinloch at collabora.com>
---
.../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 +
7 files changed, 342 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/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 56df01d7ce06e..4b6f5a517060d 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 10509c85f488eb762f8d0a825493193e38359359 Mon Sep 17 00:00:00 2001
From: Colin Kinloch <colin.kinloch at collabora.com>
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 | 765 +++++++++---------
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, 453 insertions(+), 381 deletions(-)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 4b6f5a517060d..a20b4b8a9f163 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..00dd75d3621ac 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -1147,6 +1147,103 @@ static bool ProcessFormatStringLiteral(const Expr *FormatExpr,
return false;
}
+namespace {
+// 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.
+// They are recognized here by name, but only after the caller verifies the
+// full POSIX prototype so an unrelated function happening to share the name
+// is not diagnosed as if it were the libc function.
+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
+};
+} // namespace
+
+static std::optional<LibcFuncDesc> lookupLibCFunctionDesc(ASTContext &Ctx,
+ StringRef Name) {
+ QualType IntTy = Ctx.IntTy;
+ QualType SizeTy = Ctx.getSizeType();
+ 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 the
+ // caller's slot-match 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;
+
+ auto Table = std::make_unique<llvm::DenseMap<StringRef, LibcFuncDesc>>();
+ Table->insert_range(std::initializer_list<std::pair<StringRef, LibcFuncDesc>>{
+ {"getcwd",
+ {"getcwd", CharPtrTy, {CharPtrTy, SizeTy}, std::nullopt, 1, 0}},
+ {"read",
+ {"read", SSizeTy, {IntTy, VoidPtrTy, SizeTy}, std::nullopt, 2, 1}},
+ {"write",
+ {"write", SSizeTy, {IntTy, ConstVoidPtrTy, SizeTy}, 1, 2, std::nullopt}},
+ {"readlink",
+ {"readlink",
+ SSizeTy,
+ {ConstCharPtrTy, CharPtrTy, SizeTy},
+ std::nullopt,
+ 2,
+ 1}},
+ {"readlinkat",
+ {"readlinkat",
+ SSizeTy,
+ {IntTy, ConstCharPtrTy, CharPtrTy, SizeTy},
+ std::nullopt,
+ 3,
+ 2}},
+ {"pread",
+ {"pread",
+ SSizeTy,
+ {IntTy, VoidPtrTy, SizeTy, OffTy},
+ std::nullopt,
+ 2,
+ 1}},
+ {"pread64",
+ {"pread64",
+ SSizeTy,
+ {IntTy, VoidPtrTy, SizeTy, Off64Ty},
+ std::nullopt,
+ 2,
+ 1}},
+ {"pwrite",
+ {"pwrite",
+ SSizeTy,
+ {IntTy, ConstVoidPtrTy, SizeTy, OffTy},
+ 1,
+ 2,
+ std::nullopt}},
+ {"pwrite64",
+ {"pwrite64",
+ SSizeTy,
+ {IntTy, ConstVoidPtrTy, SizeTy, Off64Ty},
+ 1,
+ 2,
+ std::nullopt}},
+ });
+
+ auto It = Table->find(Name);
+ if (It == Table->end())
+ return std::nullopt;
+ return It->second;
+}
+
void Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
CallExpr *TheCall) {
if (TheCall->isValueDependent() || TheCall->isTypeDependent() ||
@@ -1165,112 +1262,32 @@ void Sema::checkFortifiedBuiltinMemoryFunction(FunctionDecl *FD,
unsigned BuiltinID = UseDecl->getBuiltinID(/*ConsiderWrappers=*/true);
- // 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;
+ 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());
-
- 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;
+ // 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 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 =
+ lookupLibCFunctionDesc(Ctx, 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 +1371,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 +1405,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 at -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 at -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 861c6d590e7fd284285340d24eef0f1764d2a058 Mon Sep 17 00:00:00 2001
From: Colin Kinloch <colin.kinloch at collabora.com>
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 at -2{{'memcpy' will always overflow; destination buffer has size 9, but size argument is 10}}
+ // expected-warning at -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 at -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 at -7{{'memcpy' will always over-read; source buffer has size 9, but size argument is 10}}
}
-
}
void call_call_memcpy() {
>From f51a2d8338741d90b7935fc62d1a2e514cf6422d Mon Sep 17 00:00:00 2001
From: Colin Kinloch <colin.kinloch at collabora.com>
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 at -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 at -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 at -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 at -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 at -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 at -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 at -2 {{'memcpy' will always overflow; destination buffer has size 0, but size argument is 2}}
- // expected-warning at -3 {{Memory copy function overflows the destination buffer}}
+ // expected-warning at -3 {{'memcpy' will always over-read; source buffer has size 0, but size argument is 2}}
+ // expected-warning at -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 at -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 178d993446d8c78c38d6e9bb377c9903e791f771 Mon Sep 17 00:00:00 2001
From: Colin Kinloch <colin.kinloch at collabora.com>
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 770335724dcdbe176bf0ec3062a55c03c4e8a99a Mon Sep 17 00:00:00 2001
From: Denys Fedoryshchenko <denys.f at collabora.com>
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 at -1{{The 3rd argument to 'read' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 3rd argument to 'read' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -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 at -1{{The 3rd argument to 'write' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 3rd argument to 'write' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -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 at -1{{The 3rd argument to 'readlink' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 3rd argument to 'readlink' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -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 at -1{{The 4th argument to 'readlinkat' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 4th argument to 'readlinkat' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -3{{The 4th argument to 'readlinkat' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+}
>From 98a63a33c3d3f03497326fc4baf61ed9a96c1dca Mon Sep 17 00:00:00 2001
From: Denys Fedoryshchenko <denys.f at collabora.com>
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 <denys.f at collabora.com>
---
.../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 at -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 at -1{{The 3rd argument to 'pread' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 3rd argument to 'pread' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -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 at -1{{The 3rd argument to 'pread64' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 3rd argument to 'pread64' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -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 at -1{{The 3rd argument to 'pwrite' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 3rd argument to 'pwrite' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -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 at -1{{The 3rd argument to 'pwrite64' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-warning at -2{{The 3rd argument to 'pwrite64' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
+ // bugpath-note at -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 at -1{{The 3rd argument to 'readlink' is 9223372036854775808 but should be a value not greater than SSIZE_MAX}}
>From 68f3d5f318d78de0550c400712f1dd8fbe00cba5 Mon Sep 17 00:00:00 2001
From: Denys Fedoryshchenko <denys.f at collabora.com>
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:
More information about the libcxx-commits
mailing list