https://github.com/aaditya8979 created
https://github.com/llvm/llvm-project/pull/233722
Fixes #198344.
## Motivation
When a virtual method in a derived class adds `[[clang::lifetimebound]]`
on a parameter (or the implicit `this` object) but the overridden
base-class method does not, the annotation is silently invisible to
callers dispatching through a base-class pointer or reference. This
creates a safety gap: the caller sees no lifetime contract, but the
derived-class implementation assumes one.
```cpp
struct Base {
virtual const int &get(const int &x); // no annotation
};
struct Derived : Base {
// Adds [[lifetimebound]] — but callers via Base* never see it.
const int &get(const int &x [[clang::lifetimebound]]) override;
};
void use(Base &b) {
const int &r = b.get(42); // no warning — caller sees Base's contract
use_later(r); // potential dangling reference if b is Derived
}
```
This is an LSP violation: a virtual override must not silently *strengthen*
preconditions (like lifetime requirements) that the base class does not
declare.
## Implementation
A new check is added in `Sema::CheckOverridingFunctionAttributes`, which
already hosts the analogous `__attribute__((noescape))` check.
For each overriding method the check:
1. Calls `getDeclWithMergedLifetimeBoundAttrs()` on both the new and the
overridden method to handle attributes spread across redeclarations.
2. Iterates over parameters; if the overriding parameter has an explicit
`[[lifetimebound]]` that the overridden parameter lacks, it warns.
3. Calls `getImplicitObjectParamLifetimeBoundAttr()` to detect the same
discrepancy on the implicit `this` parameter.
4. Guards on `Attr->isImplicit()` to suppress the warning for attributes
that were added by inference (e.g. for `std::move`, `std::forward`),
consistent with the pattern in `Checker.cpp`.
5. Guards on `Diags.isIgnored()` to skip the entire loop when the warning
is disabled, preserving compile-time performance.
The warning is opt-in (`DefaultIgnore`) and is not enabled by `-Wall` or
`-Wextra`. It is reachable via:
- `-Wlifetime-safety-overridden-missing-lifetimebound` (specific flag)
- `-Wlifetime-safety-annotation-placement` (annotation-quality umbrella)
- `-Wlifetime-safety-all` (full lifetime-safety umbrella)
## Example output
```
t.cpp:5:31: warning: overriding method has 'lifetimebound' attribute on
parameter 'x', but overridden method is not marked 'lifetimebound'
[-Wlifetime-safety-overridden-missing-lifetimebound]
const int &get(const int &x [[clang::lifetimebound]]) override;
^
t.cpp:2:35: note: overridden method parameter is here
virtual const int &get(const int &x);
^
```
## Test coverage
New test file
`clang/test/Sema/LifetimeSafety/overridden-missing-lifetimebound.cpp`
with 4 `RUN` lines covering:
| Scenario | Expected result |
|---|---|
| Named parameter with `[[lifetimebound]]` on override | warning |
| Unnamed parameter with `[[lifetimebound]]` on override | warning |
| Implicit `this` with `[[lifetimebound]]` on override | warning |
| Both base and override annotated | no warning |
| Override *removes* annotation (valid under LSP) | no warning |
| Multi-level inheritance: warns against direct base | warning |
| Multiple params: only annotated param warns | warning |
| Pure virtual base | warning |
| Template class instantiation | warning |
| No warning flags (DefaultIgnore) | no diagnostics |
All 25 `clang/test/Sema/LifetimeSafety/` tests pass. No regressions in
SemaCXX virtual/override/lifetimebound test suite (23/23 pass).
cc @Xazax-hun
>From 629d23076156c5f91dca631e876814deeffa8a83 Mon Sep 17 00:00:00 2001
From: openhands <[email protected]>
Date: Tue, 20 Oct 2026 12:20:11 +0530
Subject: [PATCH] [clang][LifetimeSafety] Diagnose missing [[lifetimebound]] on
overriding methods
As per the Liskov Substitution Principle, overriding methods should not
add [[clang::lifetimebound]] annotations that are absent from the
overridden method. When a virtual call is dispatched through a base-class
pointer or reference, the caller has no reason to honor the stronger
lifetime contract implied by the annotation on the override. This creates
a silent safety gap.
This patch adds a new warning, enabled by
-Wlifetime-safety-overridden-missing-lifetimebound, that fires when:
- An overriding method's parameter carries [[clang::lifetimebound]] but
the corresponding base-class parameter does not, or
- The overriding method's implicit object parameter (this) is marked
[[clang::lifetimebound]] but the base-class method is not.
The check lives in Sema::CheckOverridingFunctionAttributes, which is
already the home for the analogous __attribute__((noescape)) check. It
uses the existing lifetime-safety helpers
getDeclWithMergedLifetimeBoundAttrs and
getImplicitObjectParamLifetimeBoundAttr to correctly handle attributes
spread across multiple redeclarations.
Only explicitly written attributes are diagnosed; attributes added by
inference (isImplicit()) are skipped, consistent with the pattern used
elsewhere in the lifetime-safety checker.
The warning is grouped under -Wlifetime-safety-annotation-placement
(reachable via -Wlifetime-safety-all) and is DefaultIgnore, so it does
not fire unless explicitly requested.
Fixes: https://github.com/llvm/llvm-project/issues/198344
---
clang/include/clang/Basic/DiagnosticGroups.td | 10 +-
.../clang/Basic/DiagnosticSemaKinds.td | 7 ++
clang/lib/Sema/SemaDeclCXX.cpp | 45 +++++++++
.../overridden-missing-lifetimebound.cpp | 91 +++++++++++++++++++
4 files changed, 152 insertions(+), 1 deletion(-)
create mode 100644
clang/test/Sema/LifetimeSafety/overridden-missing-lifetimebound.cpp
diff --git a/clang/include/clang/Basic/DiagnosticGroups.td
b/clang/include/clang/Basic/DiagnosticGroups.td
index b6c7ff7276260..bb7d676a60429 100644
--- a/clang/include/clang/Basic/DiagnosticGroups.td
+++ b/clang/include/clang/Basic/DiagnosticGroups.td
@@ -667,6 +667,13 @@ Detects function definitions whose parameters or implicit
this argument are mark
}];
}
+def LifetimeSafetyOverriddenMissingLifetimebound
+ : DiagGroup<"lifetime-safety-overridden-missing-lifetimebound"> {
+ code Documentation = [{
+Detects overriding virtual methods that add [[clang::lifetimebound]]
annotations that are missing from the overridden method.
+ }];
+}
+
def LifetimeSafetyPermissive : DiagGroup<"lifetime-safety-permissive",
[LifetimeSafetyUseAfterScope,
LifetimeSafetyReturnStackAddr,
@@ -721,7 +728,8 @@ Detects misuse of [[clang::noescape]] annotation where the
parameter escapes (fo
def LifetimeSafetyAnnotationPlacement
: DiagGroup<"lifetime-safety-annotation-placement",
[LifetimeSafetyInapplicableLifetimebound,
- LifetimeSafetyMisplacedLifetimebound]> {
+ LifetimeSafetyMisplacedLifetimebound,
+ LifetimeSafetyOverriddenMissingLifetimebound]> {
code Documentation = [{
Validates that lifetime annotations are placed on appropriate types and in
appropriate locations.
}];
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td
b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index 494d7bcd95309..cf8e294582287 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -11100,6 +11100,13 @@ def warn_lifetime_safety_inapplicable_lifetimebound
InGroup<LifetimeSafetyInapplicableLifetimebound>,
DefaultIgnore;
+def warn_overriding_method_missing_lifetimebound : Warning<
+ "overriding method has 'lifetimebound' attribute on %select{an unnamed
parameter|parameter %1|the implicit object parameter}0, but overridden method
is not marked 'lifetimebound'">,
+ InGroup<LifetimeSafetyOverriddenMissingLifetimebound>,
+ DefaultIgnore;
+def note_overridden_missing_lifetimebound : Note<
+ "overridden method %select{parameter is |is }0here">;
+
def note_lifetime_safety_used_here : Note<"later used here">;
def note_lifetime_safety_invalidated_here : Note<"%0 is invalidated here">;
def note_lifetime_safety_destroyed_here : Note<"%0 is destroyed here">;
diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp
index cd9771526bee3..444af049167c2 100644
--- a/clang/lib/Sema/SemaDeclCXX.cpp
+++ b/clang/lib/Sema/SemaDeclCXX.cpp
@@ -27,6 +27,7 @@
#include "clang/AST/StmtVisitor.h"
#include "clang/AST/TypeLoc.h"
#include "clang/AST/TypeOrdering.h"
+#include "clang/Analysis/Analyses/LifetimeSafety/LifetimeAnnotations.h"
#include "clang/Basic/AttributeCommonInfo.h"
#include "clang/Basic/PartialDiagnostic.h"
#include "clang/Basic/Specifiers.h"
@@ -19127,6 +19128,50 @@ bool
Sema::CheckOverridingFunctionAttributes(CXXMethodDecl *New,
}
}
+ // An overriding method should not add [[clang::lifetimebound]] annotations
+ // that are missing from the overridden method, as virtual calls through the
+ // base class would not enforce the required lifetime contracts.
+ if (!Diags.isIgnored(diag::warn_overriding_method_missing_lifetimebound,
+ New->getLocation())) {
+ const FunctionDecl *OldMostRecent =
+ lifetimes::getDeclWithMergedLifetimeBoundAttrs(Old);
+ const FunctionDecl *NewMostRecent =
+ lifetimes::getDeclWithMergedLifetimeBoundAttrs(New);
+
+ for (unsigned I = 0, E = std::min(NewMostRecent->getNumParams(),
+ OldMostRecent->getNumParams());
+ I != E; ++I) {
+ const ParmVarDecl *NewParam = NewMostRecent->getParamDecl(I);
+ const ParmVarDecl *OldParam = OldMostRecent->getParamDecl(I);
+ if (const auto *Attr = NewParam->getAttr<LifetimeBoundAttr>()) {
+ // Skip inferred (implicit) attributes; only diagnose explicit ones.
+ if (!Attr->isImplicit() && !OldParam->hasAttr<LifetimeBoundAttr>()) {
+ bool HasName = !NewParam->getDeclName().isEmpty();
+ Diag(Attr->getLocation(),
+ diag::warn_overriding_method_missing_lifetimebound)
+ << (HasName ? 1 : 0) << NewParam;
+ Diag(OldParam->getLocation(),
+ diag::note_overridden_missing_lifetimebound)
+ << 0;
+ }
+ }
+ }
+
+ const LifetimeBoundAttr *NewThisAttr =
+ lifetimes::getImplicitObjectParamLifetimeBoundAttr(NewMostRecent);
+ const LifetimeBoundAttr *OldThisAttr =
+ lifetimes::getImplicitObjectParamLifetimeBoundAttr(OldMostRecent);
+ // Skip inferred (implicit) attributes; only diagnose explicit ones.
+ if (NewThisAttr && !NewThisAttr->isImplicit() && !OldThisAttr) {
+ SourceLocation Loc = NewThisAttr->getLocation();
+ if (Loc.isInvalid())
+ Loc = New->getLocation();
+ Diag(Loc, diag::warn_overriding_method_missing_lifetimebound) << 2 << "";
+ Diag(Old->getLocation(), diag::note_overridden_missing_lifetimebound)
+ << 1;
+ }
+ }
+
// SME attributes must match when overriding a function declaration.
if (IsInvalidSMECallConversion(Old->getType(), New->getType())) {
Diag(New->getLocation(), diag::err_conflicting_overriding_attributes)
diff --git
a/clang/test/Sema/LifetimeSafety/overridden-missing-lifetimebound.cpp
b/clang/test/Sema/LifetimeSafety/overridden-missing-lifetimebound.cpp
new file mode 100644
index 0000000000000..542cd25e94894
--- /dev/null
+++ b/clang/test/Sema/LifetimeSafety/overridden-missing-lifetimebound.cpp
@@ -0,0 +1,91 @@
+// RUN: %clang_cc1 -fsyntax-only
-Wlifetime-safety-overridden-missing-lifetimebound -verify %s
+// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety-annotation-placement
-verify %s
+// RUN: %clang_cc1 -fsyntax-only -Wlifetime-safety-all -verify %s
+// RUN: %clang_cc1 -fsyntax-only -verify=expected-none %s
+// expected-none-no-diagnostics
+
+struct BaseNamed {
+ virtual const int &f(const int &x); // expected-note {{overridden method
parameter is here}}
+};
+
+struct DerivedNamed : BaseNamed {
+ const int &f(const int &x [[clang::lifetimebound]]) override; //
expected-warning {{overriding method has 'lifetimebound' attribute on parameter
'x', but overridden method is not marked 'lifetimebound'}}
+};
+
+struct BaseUnnamed {
+ virtual const int &f(const int &); // expected-note {{overridden method
parameter is here}}
+};
+
+struct DerivedUnnamed : BaseUnnamed {
+ const int &f([[clang::lifetimebound]] const int &) override; //
expected-warning {{overriding method has 'lifetimebound' attribute on an
unnamed parameter, but overridden method is not marked 'lifetimebound'}}
+};
+
+struct BaseThis {
+ virtual const int &f() const; // expected-note {{overridden method is here}}
+};
+
+struct DerivedThis : BaseThis {
+ const int &f() const [[clang::lifetimebound]] override; // expected-warning
{{overriding method has 'lifetimebound' attribute on the implicit object
parameter, but overridden method is not marked 'lifetimebound'}}
+};
+
+struct BaseBoth {
+ virtual const int &f(const int &x [[clang::lifetimebound]]) const
[[clang::lifetimebound]];
+};
+
+struct DerivedBoth : BaseBoth {
+ const int &f(const int &x [[clang::lifetimebound]]) const
[[clang::lifetimebound]] override; // no warning
+};
+
+struct BaseWeaker {
+ virtual const int &f(const int &x [[clang::lifetimebound]]) const
[[clang::lifetimebound]];
+};
+
+struct DerivedWeaker : BaseWeaker {
+ const int &f(const int &x) const override; // no warning under LSP
+};
+
+struct GrandBase {
+ virtual const int &f(const int &x);
+};
+
+struct Middle : GrandBase {
+ const int &f(const int &x) override; // expected-note {{overridden method
parameter is here}}
+};
+
+struct Leaf : Middle {
+ const int &f(const int &x [[clang::lifetimebound]]) override; //
expected-warning {{overriding method has 'lifetimebound' attribute on parameter
'x', but overridden method is not marked 'lifetimebound'}}
+};
+
+struct MultiParamBase {
+ virtual const int &f(const int &a, const int &b); // expected-note
{{overridden method parameter is here}}
+};
+
+struct MultiParamDerived : MultiParamBase {
+ const int &f(const int &a, const int &b [[clang::lifetimebound]]) override;
// expected-warning {{overriding method has 'lifetimebound' attribute on
parameter 'b', but overridden method is not marked 'lifetimebound'}}
+};
+
+// --- Pure virtual base ---
+
+struct PureBase {
+ virtual const int &g(const int &x) = 0; // expected-note {{overridden method
parameter is here}}
+};
+
+struct PureDerived : PureBase {
+ const int &g(const int &x [[clang::lifetimebound]]) override; //
expected-warning {{overriding method has 'lifetimebound' attribute on parameter
'x', but overridden method is not marked 'lifetimebound'}}
+};
+
+// --- Template: warn when explicit lifetimebound is added on override ---
+
+template <typename T>
+struct TemplateBase {
+ virtual const T &get(const T &x); // expected-note {{overridden method
parameter is here}}
+};
+
+template <typename T>
+struct TemplateDerived : TemplateBase<T> {
+ const T &get(const T &x [[clang::lifetimebound]]) override; //
expected-warning {{overriding method has 'lifetimebound' attribute on parameter
'x', but overridden method is not marked 'lifetimebound'}}
+};
+
+void instantiate() {
+ TemplateDerived<int> d; // expected-note {{in instantiation of template
class 'TemplateDerived<int>' requested here}}
+}
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits