Skip to content

Conversation

@ostannard
Copy link
Collaborator

The lifetimes of local variables and function parameters must end before the call to a [[clang::musttail]] function, instead of before the return, because we will not have a stack frame to hold them when doing the call.

This documents this limitation, and adds diagnostics to warn about some code which is invalid because of it.

The lifetimes of local variables and function parameters must end before
the call to a [[clang::musttail]] function, instead of before the
return, because we will not have a stack frame to hold them when doing
the call.

This documents this limitation, and adds diagnostics to warn about some
code which is invalid because of it.
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Sep 19, 2024
@llvmbot
Copy link
Member

llvmbot commented Sep 19, 2024

@llvm/pr-subscribers-clang

Author: Oliver Stannard (ostannard)

Changes

The lifetimes of local variables and function parameters must end before the call to a [[clang::musttail]] function, instead of before the return, because we will not have a stack frame to hold them when doing the call.

This documents this limitation, and adds diagnostics to warn about some code which is invalid because of it.


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

6 Files Affected:

  • (modified) clang/include/clang/Basic/AttrDocs.td (+5)
  • (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+2-1)
  • (modified) clang/lib/Sema/CheckExprLifetime.cpp (+14-2)
  • (modified) clang/lib/Sema/CheckExprLifetime.h (+6)
  • (modified) clang/lib/Sema/SemaStmt.cpp (+10)
  • (modified) clang/test/SemaCXX/attr-musttail.cpp (+27)
diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td
index 8ef151b3f2fddb..7226871074ee7e 100644
--- a/clang/include/clang/Basic/AttrDocs.td
+++ b/clang/include/clang/Basic/AttrDocs.td
@@ -637,6 +637,11 @@ return value must be trivially destructible. The calling convention of the
 caller and callee must match, and they must not be variadic functions or have
 old style K&R C function declarations.
 
+The lifetimes of all local variables and function parameters end immediately
+before the call to the function. This means that it is undefined behaviour to
+pass a pointer or reference to a local variable to the called function, which
+is not the case without the attribute.
+
 ``clang::musttail`` provides assurances that the tail call can be optimized on
 all targets, not just one.
   }];
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index ba813af960af6f..75c7f9e0eb7de0 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -10101,7 +10101,8 @@ def err_lifetimebound_ctor_dtor : Error<
 // CHECK: returning address/reference of stack memory
 def warn_ret_stack_addr_ref : Warning<
   "%select{address of|reference to}0 stack memory associated with "
-  "%select{local variable|parameter|compound literal}2 %1 returned">,
+  "%select{local variable|parameter|compound literal}2 %1 "
+  "%select{returned|passed to musttail function}3">,
   InGroup<ReturnStackAddress>;
 def warn_ret_local_temp_addr_ref : Warning<
   "returning %select{address of|reference to}0 local temporary object">,
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index c98fbca849faba..211c1cc7bc81f9 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -33,6 +33,10 @@ enum LifetimeKind {
   /// the entity is a return object.
   LK_Return,
 
+  /// The lifetime of a temporary bound to this entity ends too soon, because
+  /// the entity passed to a musttail function call.
+  LK_MustTail,
+
   /// The lifetime of a temporary bound to this entity ends too soon, because
   /// the entity is the result of a statement expression.
   LK_StmtExprResult,
@@ -1150,6 +1154,7 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
       break;
 
     case LK_Return:
+    case LK_MustTail:
     case LK_StmtExprResult:
       if (auto *DRE = dyn_cast<DeclRefExpr>(L)) {
         // We can't determine if the local variable outlives the statement
@@ -1158,7 +1163,8 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
           return false;
         SemaRef.Diag(DiagLoc, diag::warn_ret_stack_addr_ref)
             << InitEntity->getType()->isReferenceType() << DRE->getDecl()
-            << isa<ParmVarDecl>(DRE->getDecl()) << DiagRange;
+            << isa<ParmVarDecl>(DRE->getDecl()) << (LK == LK_MustTail)
+            << DiagRange;
       } else if (isa<BlockExpr>(L)) {
         SemaRef.Diag(DiagLoc, diag::err_ret_local_block) << DiagRange;
       } else if (isa<AddrLabelExpr>(L)) {
@@ -1170,7 +1176,7 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
       } else if (auto *CLE = dyn_cast<CompoundLiteralExpr>(L)) {
         SemaRef.Diag(DiagLoc, diag::warn_ret_stack_addr_ref)
             << InitEntity->getType()->isReferenceType() << CLE->getInitializer()
-            << 2 << DiagRange;
+            << 2 << (LK == LK_MustTail) << DiagRange;
       } else {
         // P2748R5: Disallow Binding a Returned Glvalue to a Temporary.
         // [stmt.return]/p6: In a function whose return type is a reference,
@@ -1265,6 +1271,12 @@ void checkExprLifetime(Sema &SemaRef, const InitializedEntity &Entity,
                         /*AEntity*/ nullptr, Init);
 }
 
+void checkExprLifetimeMustTailArg(Sema &SemaRef, const InitializedEntity &Entity,
+                       Expr *Init) {
+  checkExprLifetimeImpl(SemaRef, &Entity, nullptr, LK_MustTail,
+                        /*AEntity*/ nullptr, Init);
+}
+
 void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity,
                        Expr *Init) {
   bool EnableDanglingPointerAssignment = !SemaRef.getDiagnostics().isIgnored(
diff --git a/clang/lib/Sema/CheckExprLifetime.h b/clang/lib/Sema/CheckExprLifetime.h
index 8c8d0806dee0a3..903f312f3533e5 100644
--- a/clang/lib/Sema/CheckExprLifetime.h
+++ b/clang/lib/Sema/CheckExprLifetime.h
@@ -35,6 +35,12 @@ void checkExprLifetime(Sema &SemaRef, const InitializedEntity &Entity,
 /// sufficient for assigning to the entity.
 void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity, Expr *Init);
 
+/// Check that the lifetime of the given expr (and its subobjects) is
+/// sufficient, assuming that it is passed as an argument to a musttail
+/// function.
+void checkExprLifetimeMustTailArg(Sema &SemaRef,
+                                  const InitializedEntity &Entity, Expr *Init);
+
 } // namespace clang::sema
 
 #endif // LLVM_CLANG_SEMA_CHECK_EXPR_LIFETIME_H
diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp
index 9664287b9a3fe9..9e235a46707cd4 100644
--- a/clang/lib/Sema/SemaStmt.cpp
+++ b/clang/lib/Sema/SemaStmt.cpp
@@ -10,6 +10,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "CheckExprLifetime.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/ASTDiagnostic.h"
 #include "clang/AST/ASTLambda.h"
@@ -889,6 +890,15 @@ bool Sema::checkMustTailAttr(const Stmt *St, const Attr &MTA) {
     return false;
   }
 
+  // The lifetimes of locals and incoming function parameters must end before
+  // the call, because we can't have a stack frame to store them, so diagnose
+  // any pointers or references to them passed into the musttail call.
+  for (auto ArgExpr : CE->arguments()) {
+    InitializedEntity Entity = InitializedEntity::InitializeParameter(
+        Context, ArgExpr->getType(), false);
+    checkExprLifetimeMustTailArg(*this, Entity, const_cast<Expr *>(ArgExpr));
+  }
+
   return true;
 }
 
diff --git a/clang/test/SemaCXX/attr-musttail.cpp b/clang/test/SemaCXX/attr-musttail.cpp
index 561184e7a24f94..d84b97a4f04d86 100644
--- a/clang/test/SemaCXX/attr-musttail.cpp
+++ b/clang/test/SemaCXX/attr-musttail.cpp
@@ -267,3 +267,30 @@ namespace ns {}
 void TestCallNonValue() {
   [[clang::musttail]] return ns; // expected-error {{unexpected namespace name 'ns': expected expression}}
 }
+
+// Test diagnostics for lifetimes of local variables, which end earlier for a
+// musttail call than for a nowmal one.
+
+void TakesIntAndPtr(int, int *);
+void PassAddressOfLocal(int a, int *b) {
+  int c;
+  [[clang::musttail]] return TakesIntAndPtr(0, &c); // expected-warning {{address of stack memory associated with local variable 'c' passed to musttail function}}
+}
+void PassAddressOfParam(int a, int *b) {
+  [[clang::musttail]] return TakesIntAndPtr(0, &a); // expected-warning {{address of stack memory associated with parameter 'a' passed to musttail function}}
+}
+void PassValues(int a, int *b) {
+  [[clang::musttail]] return TakesIntAndPtr(a, b);
+}
+
+void TakesIntAndRef(int, int &);
+void PassRefOfLocal(int a, int &b) {
+  int c;
+  [[clang::musttail]] return TakesIntAndRef(0, c); // expected-warning {{address of stack memory associated with local variable 'c' passed to musttail function}}
+}
+void PassRefOfParam(int a, int &b) {
+  [[clang::musttail]] return TakesIntAndRef(0, a); // expected-warning {{address of stack memory associated with parameter 'a' passed to musttail function}}
+}
+void PassValuesRef(int a, int &b) {
+  [[clang::musttail]] return TakesIntAndRef(a, b);
+}

@github-actions
Copy link

github-actions bot commented Sep 19, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

Copy link
Collaborator

@hokein hokein left a comment

Choose a reason for hiding this comment

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

I think it warrants a mention in the release notes.

Copy link
Collaborator

@hokein hokein left a comment

Choose a reason for hiding this comment

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

Thanks, it looks good, just a few nits.

void TakesIntAndPtr(int, int *);
void PassAddressOfLocal(int a, int *b) {
int c;
[[clang::musttail]] return TakesIntAndPtr(0, &c); // expected-warning {{address of stack memory associated with local variable 'c' passed to musttail function}}
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you add a test where a function call is used in the argument? something like

int foo();
void test(int a, const int &b) {
  int c;
  [[clang::musttail]] return test(0, foo()); 
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done, and there was a different diagnostic message used for temporaries, so added a better wording for that.

The lifetimes of all local variables and function parameters end immediately
before the call to the function. This means that it is undefined behaviour to
pass a pointer or reference to a local variable to the called function, which
is not the case without the attribute.
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: maybe mention that clang will give a diagnostic on this case.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

Copy link
Collaborator

@hokein hokein left a comment

Choose a reason for hiding this comment

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

Thanks, still looks good.

@ostannard ostannard merged commit 0b0a37e into llvm:main Sep 23, 2024
hubot pushed a commit to gcc-mirror/gcc that referenced this pull request Apr 2, 2025
…ents, instead warn [PR119376]

As discussed here and in bugzilla, [[clang::musttail]] attribute in clang
not just strongly asks for tail call or error, but changes behavior.
To quote:
https://clang.llvm.org/docs/AttributeReference.html#musttail
"The lifetimes of all local variables and function parameters end immediately
before the call to the function.  This means that it is undefined behaviour
to pass a pointer or reference to a local variable to the called function,
which is not the case without the attribute.  Clang will emit a warning in
common cases where this happens."

The GCC behavior was just to error if we can't prove the musttail callee
could not have dereferenced escaped pointers to local vars or parameters
of the caller.  That is still the case for variables with non-trivial
destruction (even in clang), like vars with C++ non-trivial destructors or
variables with cleanup attribute.

The following patch changes the behavior to match that of clang, for all of
[[clang::musttail]], [[gnu::musttail]] and __attribute__((musttail)).

clang 20 actually added warning for some cases of it in
llvm/llvm-project#109255
but it is under -Wreturn-stack-address warning.

Now, gcc doesn't have that warning, but -Wreturn-local-addr instead, and
IMHO it is better to have this under new warnings, because this isn't about
returning local address, but about passing it to a musttail call, or maybe
escaping to a musttail call.  And perhaps users will appreciate they can
control it separately as well.

The patch introduces 2 new warnings.
-Wmusttail-local-addr
which is turn on by default and warns for the always dumb cases of passing
an address of a local variable or parameter to musttail call's argument.
And then
-Wmaybe-musttail-local-addr
which is only diagnosed if -Wmusttail-local-addr was not diagnosed and
diagnoses at most one (so that we don't emit 100s of warnings for one call
if 100s of vars can escape) case where an address of a local var could have
escaped to the musttail call.  This is less severe, the code doesn't have
to be obviously wrong, so the warning is only enabled in -Wextra.

And I've adjusted also the documentation for this change and addition of
new warnings.

2025-04-02  Jakub Jelinek  <[email protected]>

	PR ipa/119376
	* common.opt (Wmusttail-local-addr, Wmaybe-musttail-local-addr): New.
	* tree-tailcall.cc (suitable_for_tail_call_opt_p): Don't fail for
	TREE_ADDRESSABLE PARM_DECLs for musttail calls if diag_musttail.
	Emit -Wmusttail-local-addr warnings.
	(maybe_error_musttail): Use gimple_location instead of directly
	accessing location member.
	(find_tail_calls): For musttail calls if diag_musttail, don't fail
	if address of local could escape to the call, instead emit
	-Wmaybe-musttail-local-addr warnings.  Emit
	-Wmaybe-musttail-local-addr warnings also for address taken
	parameters.
	* common.opt.urls: Regenerate.
	* doc/extend.texi (musttail statement attribute): Clarify local
	variables without non-trivial destruction are considered out of scope
	before the tail call instruction.
	* doc/invoke.texi (-Wno-musttail-local-addr,
	-Wmaybe-musttail-local-addr): Document.

	* c-c++-common/musttail8.c: Expect a warning rather than error in one
	case.
	(f4): Add int * argument.
	* c-c++-common/musttail15.c: Don't disallow for C++98.
	* c-c++-common/musttail16.c: Likewise.
	* c-c++-common/musttail17.c: Likewise.
	* c-c++-common/musttail18.c: Likewise.
	* c-c++-common/musttail19.c: Likewise.  Expect a warning rather than
	error in one case.
	(f4): Add int * argument.
	* c-c++-common/musttail20.c: Don't disallow for C++98.
	* c-c++-common/musttail21.c: Likewise.
	* c-c++-common/musttail28.c: New test.
	* c-c++-common/musttail29.c: New test.
	* c-c++-common/musttail30.c: New test.
	* c-c++-common/musttail31.c: New test.
	* g++.dg/ext/musttail1.C: New test.
	* g++.dg/ext/musttail2.C: New test.
	* g++.dg/ext/musttail3.C: New test.
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.

3 participants