Skip to content

[Clang] Detect bit copies that should be relocation. #139104

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

cor3ntin
Copy link
Contributor

@cor3ntin cor3ntin commented May 8, 2025

We already warn when memcpy/memmove involves a non-trivially-copyable type. Although we only warn when the target (not the source) is not trivially copyable.

As we moved to deprecate the old is __is_trivially_relocatable trait, it's useful to help users migrating away from memcpy.

Note that currently __builtin_trivially_relocate and memmove have the same effect, but that will change as ptrauth learn to handle relocatable types, we add preconditions (ubsan), etc.

We already warn when memcpy/memmove involves a non-trivially-copyable
type. Although we only warn when the target (not the source) is
not trivially copyable.

As we moved to deprecate the old is __is_trivially_relocatable
trait, it's useful to help users migrating away from memcpy.

Note that currently __builtin_trivially_relocate and memmove
have the same effect, but that will change as ptrauth learn
to handle relocatable types, we add preconditions (ubsan), etc.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels May 8, 2025
@llvmbot
Copy link
Member

llvmbot commented May 8, 2025

@llvm/pr-subscribers-clang

Author: cor3ntin (cor3ntin)

Changes

We already warn when memcpy/memmove involves a non-trivially-copyable type. Although we only warn when the target (not the source) is not trivially copyable.

As we moved to deprecate the old is __is_trivially_relocatable trait, it's useful to help users migrating away from memcpy.

Note that currently __builtin_trivially_relocate and memmove have the same effect, but that will change as ptrauth learn to handle relocatable types, we add preconditions (ubsan), etc.


Full diff: https://github.com/llvm/llvm-project/pull/139104.diff

3 Files Affected:

  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+7)
  • (modified) clang/lib/Sema/SemaChecking.cpp (+36)
  • (modified) clang/test/SemaCXX/warn-memaccess.cpp (+36-1)
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index e1b9ed0647bb9..65315fe0d38d7 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -853,6 +853,13 @@ def warn_cxxstruct_memaccess : Warning<
   "first argument in call to "
   "%0 is a pointer to non-trivially copyable type %1">,
   InGroup<NonTrivialMemcall>;
+
+def warn_cxxstruct_memaccess_relocatable
+    : Warning<"calling %0 with a pointer to a type %1 which is trivially "
+              "relocatable "
+              "but not trivially copyable; did you mean to call %2?">,
+      InGroup<NonTrivialMemcall>;
+
 def note_nontrivial_field : Note<
   "field is non-trivial to %select{copy|default-initialize}0">;
 def err_non_trivial_c_union_in_invalid_context : Error<
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 5a0cec3d112db..98aaa51dd3462 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -9659,6 +9659,42 @@ void Sema::CheckMemaccessArguments(const CallExpr *Call,
   if (BId == Builtin::BIbzero && !FirstArgTy->getAs<PointerType>())
     return;
 
+  // Try to detect a relocation operation
+  if (getLangOpts().CPlusPlus &&
+      (BId == Builtin::BImemmove || BId == Builtin::BImemcpy)) {
+    const Expr *Dest = Call->getArg(0)->IgnoreParenImpCasts();
+    const Expr *Src = Call->getArg(1)->IgnoreParenImpCasts();
+    QualType DestTy = Dest->getType();
+    QualType SrcTy = Src->getType();
+
+    QualType DestPointeeTy = DestTy->getPointeeType();
+    QualType SrcPointeeTy = SrcTy->getPointeeType();
+    bool HasSameTargetAndSource =
+        !DestPointeeTy.isNull() && !SrcPointeeTy.isNull() &&
+        Context.hasSameUnqualifiedType(DestPointeeTy, SrcPointeeTy);
+
+    if (HasSameTargetAndSource &&
+        !DestPointeeTy.getUnqualifiedType()->isIncompleteType() &&
+        !DestPointeeTy.isConstQualified() && !SrcPointeeTy.isConstQualified() &&
+        !DestPointeeTy.isTriviallyCopyableType(getASTContext()) &&
+        SemaRef.IsCXXTriviallyRelocatableType(DestPointeeTy)) {
+
+      bool SuggestStd = getLangOpts().CPlusPlus26 && getStdNamespace();
+      if (const Decl *D = Call->getReferencedDeclOfCallee();
+          D && !D->isInStdNamespace())
+        SuggestStd = false;
+
+      StringRef Replacement = SuggestStd ? "std::trivially_relocate"
+                                         : "__builtin_trivially_relocate";
+
+      DiagRuntimeBehavior(Dest->getExprLoc(), Dest,
+                          PDiag(diag::warn_cxxstruct_memaccess_relocatable)
+                              << Call->getCallee()->getSourceRange() << FnName
+                              << DestPointeeTy << Replacement);
+      return;
+    }
+  }
+
   for (unsigned ArgIdx = 0; ArgIdx != LastArg; ++ArgIdx) {
     const Expr *Dest = Call->getArg(ArgIdx)->IgnoreParenImpCasts();
     SourceRange ArgRange = Call->getArg(ArgIdx)->getSourceRange();
diff --git a/clang/test/SemaCXX/warn-memaccess.cpp b/clang/test/SemaCXX/warn-memaccess.cpp
index 2e60539b3e48e..20113170b9e93 100644
--- a/clang/test/SemaCXX/warn-memaccess.cpp
+++ b/clang/test/SemaCXX/warn-memaccess.cpp
@@ -1,4 +1,5 @@
-// RUN: %clang_cc1 -fsyntax-only -verify -std=c++11 -Wnontrivial-memcall %s
+// RUN: %clang_cc1 -fsyntax-only -std=c++11 -verify=expected,cxx11 -Wnontrivial-memcall %s
+// RUN: %clang_cc1 -fsyntax-only -std=c++2c -verify=expected,cxx26 -Wnontrivial-memcall %s
 
 extern "C" void *bzero(void *, unsigned);
 extern "C" void *memset(void *, int, unsigned);
@@ -8,6 +9,9 @@ extern "C" void *memcpy(void *s1, const void *s2, unsigned n);
 class TriviallyCopyable {};
 class NonTriviallyCopyable { NonTriviallyCopyable(const NonTriviallyCopyable&);};
 struct Incomplete;
+class Relocatable {
+  virtual void f();
+};
 
 void test_bzero(TriviallyCopyable* tc,
                 NonTriviallyCopyable *ntc,
@@ -83,3 +87,34 @@ void test_memmove(TriviallyCopyable* tc0, TriviallyCopyable* tc1,
   // OK
   memmove((void*)ntc0, (void*)ntc1, sizeof(*ntc0));
 }
+
+
+void test_possible_relocation(Relocatable* r, const Relocatable* r2) {
+     // expected-warning@+1 {{calling 'memcpy' with a pointer to a type 'Relocatable' which is trivially relocatable but not trivially copyable; did you mean to call __builtin_trivially_relocate?}}
+     memcpy(r, r, sizeof(*r));
+     // expected-warning@+1 {{calling 'memmove' with a pointer to a type 'Relocatable' which is trivially relocatable but not trivially copyable; did you mean to call __builtin_trivially_relocate?}}
+     memmove(r, r, sizeof(*r));
+
+     // expected-warning@+2{{destination for this 'memcpy' call is a pointer to dynamic class 'Relocatable'}}
+     // expected-note@+1{{explicitly cast the pointer to silence this warning}}
+     memcpy(r, r2, sizeof(*r));
+
+     // expected-warning@+2{{destination for this 'memmove' call is a pointer to dynamic class 'Relocatable'}}
+     // expected-note@+1{{explicitly cast the pointer to silence this warning}}
+     memmove(r, r2, sizeof(*r));
+}
+
+namespace std {
+  using ::memcpy;
+  using ::memmove;
+};
+
+
+void test_possible_relocation_std(Relocatable* r, const Relocatable* r2) {
+     // cxx11-warning@+2 {{calling 'memcpy' with a pointer to a type 'Relocatable' which is trivially relocatable but not trivially copyable; did you mean to call __builtin_trivially_relocate?}}
+     // cxx26-warning@+1 {{calling 'memcpy' with a pointer to a type 'Relocatable' which is trivially relocatable but not trivially copyable; did you mean to call std::trivially_relocate?}}
+     std::memcpy(r, r, sizeof(*r));
+     // cxx11-warning@+2 {{calling 'memmove' with a pointer to a type 'Relocatable' which is trivially relocatable but not trivially copyable; did you mean to call __builtin_trivially_relocate?}}
+     // cxx26-warning@+1 {{calling 'memmove' with a pointer to a type 'Relocatable' which is trivially relocatable but not trivially copyable; did you mean to call std::trivially_relocate?}}
+     std::memmove(r, r, sizeof(*r));
+}

Comment on lines 9683 to 9685
if (const Decl *D = Call->getReferencedDeclOfCallee();
D && !D->isInStdNamespace())
SuggestStd = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if I’m reading this correctly this will cause us to suggest std::trivially_relocate if the user called std::memcpy, and __builtin_trivially_relocate if they called some other memcpy that isn’t defined in the std namespace. Is there a reason why we wouldn’t just always suggest std::trivially_relocate if C++26 is enabled (or is this because the header providing that function might not be included which could potentially be confusing if someone then uses std::trivially_relocate but then we start complaining about it not being found?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the header might not be included.
I don't think we would have a way to generally detect a function exists in the STL

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the header might not be included. I don't think we would have a way to generally detect a function exists in the STL

Yeah, I don’t think we need to care about that because there is no good way of doing that (and presumably we’d end up suggesting ‘include this header to get it’ anyway like we do w/ other functions which is fine imo). But in that case, imo we can just always suggest std::trivially_relocate in C++26 mode even if the memcpy we’re calling isn’t in the std namespace.

def warn_cxxstruct_memaccess_relocatable
: Warning<"calling %0 with a pointer to a type %1 which is trivially "
"relocatable "
"but not trivially copyable; did you mean to call %2?">,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"but not trivially copyable; did you mean to call %2?">,
"but not trivially copyable; did you mean to call '%2'?">,

This one needs to be quoted manually because it’s just a StringRef.

@Sirraide
Copy link
Member

Sirraide commented May 9, 2025

Also, should this come w/ a release note? Or does this just count as part of adding support for trivial relocation?

@cor3ntin
Copy link
Contributor Author

cor3ntin commented May 9, 2025

Also, should this come w/ a release note? Or does this just count as part of adding support for trivial relocation?

Yup, this is "implement trivial relocation". There are more PRs coming!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants