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 <[email protected]> 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 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
