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

Reply via email to