[clang] [CIR] Emit lifetime markers for automatic variables (PR #206695)
Jiahao Guo via cfe-commits
cfe-commits at lists.llvm.org
Tue Jun 30 03:00:17 PDT 2026
https://github.com/E00N777 created https://github.com/llvm/llvm-project/pull/206695
### summary
This is a follow up of : https://github.com/llvm/llvm-project/pull/199599
Per the consensus from the PR[https://github.com/llvm/llvm-project/pull/199599], this implements lifetime-marker emission in ClangIR that closely follows classic CodeGen.
Emit lifetime markers (cir.lifetime.start/end) for automatic variables in ClangIR.
- Gated by shouldEmitLifetimeMarkers (optimized builds or ASan-use-after-scope/
HWASan/MSan/MemtagStack), like classic CodeGen.
- start at the decl; end as a NormalEHLifetimeMarker cleanup (after the destructor);
both target the underlying alloca. Wired through the EH stack so they don't force
a landing pad.
- Bypass analysis (PR28267) not ported; conservatively suppress markers for functions
with a label/switch/indirect goto.
- Test: scalar, destructor ordering, no markers at -O0.
>From bcd87ec04965353797c79ef88a4f3b5e373e74b3 Mon Sep 17 00:00:00 2001
From: E00N777 <E0N_gjh at 163.com>
Date: Tue, 30 Jun 2026 17:40:25 +0800
Subject: [PATCH] [CIR] Emit lifetime markers for automatic variables
---
clang/include/clang/CIR/MissingFeatures.h | 1 +
clang/lib/CIR/CodeGen/CIRGenCleanup.cpp | 5 +-
clang/lib/CIR/CodeGen/CIRGenCleanup.h | 1 +
clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 62 ++++++++++++++++++++++
clang/lib/CIR/CodeGen/CIRGenFunction.cpp | 20 +++++++
clang/lib/CIR/CodeGen/CIRGenFunction.h | 12 +++++
clang/test/CIR/CodeGen/lifetime-marker.cpp | 54 +++++++++++++++++++
7 files changed, 152 insertions(+), 3 deletions(-)
create mode 100644 clang/test/CIR/CodeGen/lifetime-marker.cpp
diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h
index 9a1546fe14e65..c1df47a5de637 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -227,6 +227,7 @@ struct MissingFeatures {
static bool emitCondLikelihoodViaExpectIntrinsic() { return false; }
static bool emitConstrainedFPCall() { return false; }
static bool emitLifetimeMarkers() { return false; }
+ static bool lifetimeMarkersBypass() { return false; }
static bool emitLValueAlignmentAssumption() { return false; }
static bool emitNullCheckForDeleteCalls() { return false; }
static bool emitNullabilityCheck() { return false; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
index c1d2def6909d7..1bf4317347817 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
@@ -392,7 +392,7 @@ void *EHScopeStack::pushCleanup(CleanupKind kind, size_t size) {
innermostEHScope = stable_begin();
if (isLifetimeMarker)
- cgf->cgm.errorNYI("push lifetime marker cleanup");
+ scope->setLifetimeMarker();
// With Windows -EHa, Invoke llvm.seh.scope.begin() for EHCleanup
if (cgf->getLangOpts().EHAsynch && isEHCleanup && !isLifetimeMarker &&
@@ -436,8 +436,7 @@ bool EHScopeStack::requiresCatchOrCleanup() const {
for (stable_iterator si = getInnermostEHScope(); si != stable_end();) {
if (auto *cleanup = dyn_cast<EHCleanupScope>(&*find(si))) {
if (cleanup->isLifetimeMarker()) {
- // Skip lifetime markers and continue from the enclosing EH scope
- assert(!cir::MissingFeatures::emitLifetimeMarkers());
+ si = cleanup->getEnclosingEHScope();
continue;
}
}
diff --git a/clang/lib/CIR/CodeGen/CIRGenCleanup.h b/clang/lib/CIR/CodeGen/CIRGenCleanup.h
index bae04a2452006..46f1382bced7d 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCleanup.h
+++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.h
@@ -157,6 +157,7 @@ class alignas(EHScopeStack::ScopeStackAlignment) EHCleanupScope
void setActive(bool isActive) { cleanupBits.isActive = isActive; }
bool isLifetimeMarker() const { return cleanupBits.isLifetimeMarker; }
+ void setLifetimeMarker() { cleanupBits.isLifetimeMarker = true; }
bool hasActiveFlag() const { return activeFlag.isValid(); }
Address getActiveFlag() const { return activeFlag; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index ad572f23b3667..34ae84252c21c 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -10,9 +10,11 @@
//
//===----------------------------------------------------------------------===//
+#include "Address.h"
#include "CIRGenCleanup.h"
#include "CIRGenConstantEmitter.h"
#include "CIRGenFunction.h"
+#include "EHScopeStack.h"
#include "mlir/IR/Location.h"
#include "clang/AST/Attr.h"
#include "clang/AST/Attrs.inc"
@@ -28,6 +30,20 @@
using namespace clang;
using namespace clang::CIRGen;
+/// Does the statement tree rooted at \p s contain a label, switch, or indirect
+/// goto that could bypass a local's initialization? A coarse stand-in for
+/// classic CodeGen's per-decl bypass analysis (PR28267).
+static bool functionMightHaveBypass(const Stmt *s) {
+ if (!s)
+ return false;
+ if (isa<LabelStmt, SwitchStmt, IndirectGotoStmt>(s))
+ return true;
+ for (const Stmt *child : s->children())
+ if (functionMightHaveBypass(child))
+ return true;
+ return false;
+}
+
CIRGenFunction::AutoVarEmission
CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
mlir::OpBuilder::InsertPoint ip) {
@@ -129,6 +145,21 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
/*arraySize=*/nullptr, /*alloca=*/nullptr, ip);
declare(address.getPointer(), &d, ty, getLoc(d.getSourceRange()),
alignment);
+ // A goto/switch that bypasses the init splits the lifetime across IR
+ // regions and miscompiles under stack coloring (PR28267). Lacking
+ // classic's per-decl bypass analysis, drop markers for the whole
+ // function if any such statement is present.
+ assert(!cir::MissingFeatures::lifetimeMarkersBypass());
+ if (shouldEmitLifetimeOp && haveInsertPoint()) {
+ if (!fnHasBypassStmt.has_value())
+ fnHasBypassStmt = functionMightHaveBypass(
+ curFuncDecl ? curFuncDecl->getBody() : nullptr);
+ // Peel address-space casts to the alloca so the op verifier sees a
+ // value produced by cir.alloca.
+ if (!*fnHasBypassStmt)
+ emission.useLifetimeOp = emitLifetimeStartOp(
+ loc, address.getUnderlyingAllocaOp().getResult());
+ }
}
} else {
// Non-constant size type
@@ -165,6 +196,9 @@ CIRGenFunction::emitAutoVarAlloca(const VarDecl &d,
assert(!cir::MissingFeatures::generateDebugInfo());
}
+ if (emission.useLifetimeOp)
+ pushLifetimeEnd(address);
+
emission.addr = address;
setAddrOfLocalVar(&d, address);
@@ -1005,6 +1039,15 @@ struct CallStackRestore final : EHScopeStack::Cleanup {
}
};
+struct CallLifetimeEnd final : EHScopeStack::Cleanup {
+ Address addr;
+ CallLifetimeEnd(Address addr) : addr(addr) {}
+ void emit(CIRGenFunction &cgf, Flags flags) override {
+ mlir::Value allocaPtr = addr.getUnderlyingAllocaOp().getResult();
+ cgf.emitLifetimeEndOp(allocaPtr.getLoc(), allocaPtr);
+ }
+};
+
/// A cleanup which performs a partial array destroy where the end pointer is
/// irregularly determined and must be loaded from a local.
struct IrregularPartialArrayDestroy final : EHScopeStack::Cleanup {
@@ -1261,6 +1304,10 @@ void CIRGenFunction::pushStackRestore(CleanupKind kind, Address spMem) {
ehStack.pushCleanup<CallStackRestore>(kind, spMem);
}
+void CIRGenFunction::pushLifetimeEnd(Address addr) {
+ ehStack.pushCleanup<CallLifetimeEnd>(NormalEHLifetimeMarker, addr);
+}
+
/// Enter a destroy cleanup for the given local variable.
void CIRGenFunction::emitAutoVarTypeCleanup(
const CIRGenFunction::AutoVarEmission &emission,
@@ -1317,3 +1364,18 @@ void CIRGenFunction::maybeEmitDeferredVarDeclInit(const VarDecl *vd) {
emitVarDecl(*hd);
}
}
+
+bool CIRGenFunction::emitLifetimeStartOp(mlir::Location loc, mlir::Value addr) {
+ if (!shouldEmitLifetimeOp)
+ return false;
+
+ cir::LifetimeStartOp::create(builder, loc, addr);
+ return true;
+}
+
+void CIRGenFunction::emitLifetimeEndOp(mlir::Location loc, mlir::Value addr) {
+ if (!shouldEmitLifetimeOp)
+ return;
+
+ cir::LifetimeEndOp::create(builder, loc, addr);
+}
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
index 6606cf74c7dea..be1f01c606a02 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
@@ -28,10 +28,30 @@
namespace clang::CIRGen {
+/// shouldEmitLifetimeMarkers - Decide whether we need emit the life-time
+/// markers. Mirror of CodeGenFunction::shouldEmitLifetimeMarkers.
+static bool shouldEmitLifetimeMarkers(const CodeGenOptions &cgOpts,
+ const LangOptions &langOpts) {
+
+ if (cgOpts.DisableLifetimeMarkers)
+ return false;
+
+ // Sanitizers may use markers.
+ if (cgOpts.SanitizeAddressUseAfterScope ||
+ langOpts.Sanitize.has(SanitizerKind::HWAddress) ||
+ langOpts.Sanitize.has(SanitizerKind::Memory) ||
+ langOpts.Sanitize.has(SanitizerKind::MemtagStack))
+ return true;
+
+ return cgOpts.OptimizationLevel != 0;
+}
+
CIRGenFunction::CIRGenFunction(CIRGenModule &cgm, CIRGenBuilderTy &builder,
bool suppressNewContext)
: CIRGenTypeCache(cgm), cgm{cgm}, builder(builder) {
ehStack.setCGF(this);
+ shouldEmitLifetimeOp = shouldEmitLifetimeMarkers(cgm.getCodeGenOpts(),
+ getContext().getLangOpts());
}
CIRGenFunction::~CIRGenFunction() {}
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index b6a4a277fab92..5e3768a332042 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -688,6 +688,9 @@ class CIRGenFunction : public CIRGenTypeCache {
/// have the same sort of alloca initialization.
bool emittedAsOffload = false;
+ /// True if lifetime op should be used.
+ bool useLifetimeOp = false;
+
mlir::Value nrvoFlag{};
struct Invalid {};
@@ -769,6 +772,7 @@ class CIRGenFunction : public CIRGenTypeCache {
}
void pushStackRestore(CleanupKind kind, Address spMem);
+ void pushLifetimeEnd(Address addr);
/// Set the address of a local variable.
void setAddrOfLocalVar(const clang::VarDecl *vd, Address addr) {
@@ -1553,6 +1557,9 @@ class CIRGenFunction : public CIRGenTypeCache {
int64_t alignment,
mlir::Value offsetValue = nullptr);
+ bool emitLifetimeStartOp(mlir::Location loc, mlir::Value addr);
+ void emitLifetimeEndOp(mlir::Location loc, mlir::Value addr);
+
private:
void emitAndUpdateRetAlloca(clang::QualType type, mlir::Location loc,
clang::CharUnits alignment);
@@ -2699,6 +2706,11 @@ class CIRGenFunction : public CIRGenTypeCache {
private:
QualType getVarArgType(const Expr *arg);
+ bool shouldEmitLifetimeOp = false;
+ /// Set when the current function has a goto/switch that may bypass a local's
+ /// init; lifetime markers are then suppressed. See functionMightHaveBypass.
+ std::optional<bool> fnHasBypassStmt;
+
class InlinedInheritingConstructorScope {
public:
InlinedInheritingConstructorScope(CIRGenFunction &cgf, GlobalDecl gd)
diff --git a/clang/test/CIR/CodeGen/lifetime-marker.cpp b/clang/test/CIR/CodeGen/lifetime-marker.cpp
new file mode 100644
index 0000000000000..3e8e54dbce096
--- /dev/null
+++ b/clang/test/CIR/CodeGen/lifetime-marker.cpp
@@ -0,0 +1,54 @@
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s --check-prefix=CIR
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -O2 -fclangir -emit-llvm -disable-llvm-passes %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s --check-prefix=LLVM
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t-o0.cir
+// RUN: FileCheck --input-file=%t-o0.cir %s --check-prefix=O0
+
+void use(int);
+
+// A scalar automatic variable gets a lifetime.start at its declaration and a
+// matching lifetime.end when its scope is left.
+void f() {
+ int x;
+ use(x);
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z1fv()
+// CIR: %[[X:.*]] = cir.alloca "x" {{.*}} : !cir.ptr<!s32i>
+// CIR: cir.lifetime.start %[[X]] : !cir.ptr<!s32i>
+// CIR: cir.cleanup.scope {
+// CIR: } cleanup normal {
+// CIR: cir.lifetime.end %[[X]] : !cir.ptr<!s32i>
+// CIR: }
+
+// LLVM-LABEL: define{{.*}} void @_Z1fv()
+// LLVM: %[[X:.*]] = alloca i32
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[X]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[X]])
+
+struct S {
+ ~S();
+};
+
+// The destructor runs before lifetime.end: the end marker is the outermost
+// cleanup, so it is emitted after the destructor call. FileCheck matches in
+// order, which pins the relative ordering.
+void g() {
+ S s;
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z1gv()
+// CIR: %[[S:.*]] = cir.alloca "s" {{.*}} : !cir.ptr<!rec_S>
+// CIR: cir.lifetime.start %[[S]] : !cir.ptr<!rec_S>
+// CIR: cir.call @_ZN1SD1Ev(%[[S]])
+// CIR: cir.lifetime.end %[[S]] : !cir.ptr<!rec_S>
+
+// LLVM-LABEL: define{{.*}} void @_Z1gv()
+// LLVM: %[[S:.*]] = alloca %struct.S
+// LLVM: call void @llvm.lifetime.start.p0(ptr %[[S]])
+// LLVM: call void @_ZN1SD1Ev(ptr {{.*}} %[[S]])
+// LLVM: call void @llvm.lifetime.end.p0(ptr %[[S]])
+
+// Without optimization no lifetime markers are emitted at all.
+// O0-NOT: cir.lifetime
More information about the cfe-commits
mailing list