Skip to content

[IR] Only allow lifetime.start/end on allocas #149310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 7 additions & 25 deletions llvm/docs/LangRef.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26634,19 +26634,14 @@ Arguments:

The first argument is a constant integer representing the size of the
object, or -1 if it is variable sized. The second argument is a pointer
to the object.
to an ``alloca`` instruction.

Semantics:
""""""""""

If ``ptr`` is a stack-allocated object and it points to the first byte of
the object, the object is initially marked as dead.
``ptr`` is conservatively considered as a non-stack-allocated object if
the stack coloring algorithm that is used in the optimization pipeline cannot
conclude that ``ptr`` is a stack-allocated object.

After '``llvm.lifetime.start``', the stack object that ``ptr`` points is marked
as alive and has an uninitialized value.
The stack-allocated object that ``ptr`` points to is initially marked as dead.
After '``llvm.lifetime.start``', the stack object is marked as alive and has an
uninitialized value.
The stack object is marked as dead when either
:ref:`llvm.lifetime.end <int_lifeend>` to the alloca is executed or the
function returns.
Expand All @@ -26656,11 +26651,6 @@ After :ref:`llvm.lifetime.end <int_lifeend>` is called,
The second '``llvm.lifetime.start``' call marks the object as alive, but it
does not change the address of the object.

If ``ptr`` is a non-stack-allocated object, it does not point to the first
byte of the object or it is a stack object that is already alive, it simply
fills all bytes of the object with ``poison``.


.. _int_lifeend:

'``llvm.lifetime.end``' Intrinsic
Expand All @@ -26684,24 +26674,16 @@ Arguments:

The first argument is a constant integer representing the size of the
object, or -1 if it is variable sized. The second argument is a pointer
to the object.
to an ``alloca`` instruction.

Semantics:
""""""""""

If ``ptr`` is a stack-allocated object and it points to the first byte of the
object, the object is dead.
``ptr`` is conservatively considered as a non-stack-allocated object if
the stack coloring algorithm that is used in the optimization pipeline cannot
conclude that ``ptr`` is a stack-allocated object.
The stack-allocated object that ``ptr`` points to becomes dead after the call
to this intrinsic.

Calling ``llvm.lifetime.end`` on an already dead alloca is no-op.

If ``ptr`` is a non-stack-allocated object or it does not point to the first
byte of the object, it is equivalent to simply filling all bytes of the object
with ``poison``.


'``llvm.invariant.start``' Intrinsic
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
8 changes: 5 additions & 3 deletions llvm/lib/Bitcode/Reader/BitcodeReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7116,9 +7116,11 @@ Error BitcodeReader::materializeModule() {
if (CallInst *CI = dyn_cast<CallInst>(U))
UpgradeIntrinsicCall(CI, I.second);
}
if (!I.first->use_empty())
I.first->replaceAllUsesWith(I.second);
I.first->eraseFromParent();
if (I.first != I.second) {
if (!I.first->use_empty())
I.first->replaceAllUsesWith(I.second);
I.first->eraseFromParent();
}
}
UpgradedIntrinsics.clear();

Expand Down
7 changes: 7 additions & 0 deletions llvm/lib/CodeGen/SafeStack.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,13 @@ Value *SafeStack::moveStaticAllocasToUnsafeStack(
Use &U = *AI->use_begin();
Instruction *User = cast<Instruction>(U.getUser());

// Drop lifetime markers now that this is no longer an alloca.
// SafeStack has already performed its own stack coloring.
if (User->isLifetimeStartOrEnd()) {
User->eraseFromParent();
continue;
}

Instruction *InsertBefore;
if (auto *PHI = dyn_cast<PHINode>(User))
InsertBefore = PHI->getIncomingBlock(U)->getTerminator();
Expand Down
44 changes: 42 additions & 2 deletions llvm/lib/IR/AutoUpgrade.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,18 @@ static bool upgradeIntrinsicFunction1(Function *F, Function *&NewFn,
return true;
}
break;
case 'l':
if (Name.starts_with("lifetime.start") ||
Name.starts_with("lifetime.end")) {
// Unless remangling is required, do not upgrade the function declaration,
// but do upgrade the calls.
if (auto Result = llvm::Intrinsic::remangleIntrinsicFunction(F))
NewFn = *Result;
else
NewFn = F;
return true;
}
break;
case 'm': {
// Updating the memory intrinsics (memcpy/memmove/memset) that have an
// alignment parameter to embedding the alignment as an attribute of
Expand Down Expand Up @@ -1629,7 +1641,6 @@ bool llvm::UpgradeIntrinsicFunction(Function *F, Function *&NewFn,
NewFn = nullptr;
bool Upgraded =
upgradeIntrinsicFunction1(F, NewFn, CanUpgradeDebugIntrinsicsToRecords);
assert(F != NewFn && "Intrinsic function upgraded to the same function");

// Upgrade intrinsic attributes. This does not change the function.
if (NewFn)
Expand Down Expand Up @@ -4570,6 +4581,9 @@ void llvm::UpgradeIntrinsicCall(CallBase *CI, Function *NewFn) {
}

const auto &DefaultCase = [&]() -> void {
if (F == NewFn)
return;

if (CI->getFunctionType() == NewFn->getFunctionType()) {
// Handle generic mangling change.
assert(
Expand Down Expand Up @@ -5109,6 +5123,31 @@ void llvm::UpgradeIntrinsicCall(CallBase *CI, Function *NewFn) {
MTI->setSourceAlignment(Align->getMaybeAlignValue());
break;
}

case Intrinsic::lifetime_start:
case Intrinsic::lifetime_end: {
Value *Size = CI->getArgOperand(0);
Value *Ptr = CI->getArgOperand(1);
if (isa<AllocaInst>(Ptr)) {
DefaultCase();
return;
}

// Try to strip pointer casts, such that the lifetime works on an alloca.
Ptr = Ptr->stripPointerCasts();
if (isa<AllocaInst>(Ptr)) {
// Don't use NewFn, as we might have looked through an addrspacecast.
if (NewFn->getIntrinsicID() == Intrinsic::lifetime_start)
NewCall = Builder.CreateLifetimeStart(Ptr, cast<ConstantInt>(Size));
else
NewCall = Builder.CreateLifetimeEnd(Ptr, cast<ConstantInt>(Size));
break;
}

// Otherwise remove the lifetime marker.
CI->eraseFromParent();
return;
}
}
assert(NewCall && "Should have either set this variable or returned through "
"the default case");
Expand All @@ -5131,7 +5170,8 @@ void llvm::UpgradeCallsToIntrinsic(Function *F) {
UpgradeIntrinsicCall(CB, NewFn);

// Remove old function, no longer used, from the module.
F->eraseFromParent();
if (F != NewFn)
F->eraseFromParent();
}
}

Expand Down
5 changes: 5 additions & 0 deletions llvm/lib/IR/Verifier.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6679,6 +6679,11 @@ void Verifier::visitIntrinsicCall(Intrinsic::ID ID, CallBase &Call) {
"llvm.threadlocal.address operand isThreadLocal() must be true");
break;
}
case Intrinsic::lifetime_start:
case Intrinsic::lifetime_end:
Check(isa<AllocaInst>(Call.getArgOperand(1)),
"llvm.lifetime.start/end can only be used on alloca", &Call);
break;
};

// Verify that there aren't any unmediated control transfers between funclets.
Expand Down
6 changes: 5 additions & 1 deletion llvm/lib/Target/SPIRV/SPIRVPrepareFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ bool SPIRVPrepareFunctions::substituteIntrinsicCalls(Function *F) {
bool Changed = false;
const SPIRVSubtarget &STI = TM.getSubtarget<SPIRVSubtarget>(*F);
for (BasicBlock &BB : *F) {
for (Instruction &I : BB) {
for (Instruction &I : make_early_inc_range(BB)) {
auto Call = dyn_cast<CallInst>(&I);
if (!Call)
continue;
Expand Down Expand Up @@ -408,12 +408,16 @@ bool SPIRVPrepareFunctions::substituteIntrinsicCalls(Function *F) {
if (!STI.isShader()) {
Changed |= toSpvOverloadedIntrinsic(
II, Intrinsic::SPVIntrinsics::spv_lifetime_start, {1});
} else {
II->eraseFromParent();
}
break;
case Intrinsic::lifetime_end:
if (!STI.isShader()) {
Changed |= toSpvOverloadedIntrinsic(
II, Intrinsic::SPVIntrinsics::spv_lifetime_end, {1});
} else {
II->eraseFromParent();
}
break;
case Intrinsic::ptr_annotation:
Expand Down
6 changes: 6 additions & 0 deletions llvm/lib/Transforms/Coroutines/CoroEarly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ void Lowerer::hidePromiseAlloca(CoroIdInst *CoroId, CoroBeginInst *CoroBegin) {
auto *PI = Builder.CreateIntrinsic(
Builder.getPtrTy(), Intrinsic::coro_promise, Arg, {}, "promise.addr");
PI->setCannotDuplicate();
// Remove lifetime markers, as these are only allowed on allocas.
for (User *U : make_early_inc_range(PA->users())) {
auto *I = cast<Instruction>(U);
if (I->isLifetimeStartOrEnd())
I->eraseFromParent();
}
PA->replaceUsesWithIf(PI, [CoroId](Use &U) {
bool IsBitcast = U == U.getUser()->stripPointerCasts();
bool IsCoroId = U.getUser() == CoroId;
Expand Down
18 changes: 18 additions & 0 deletions llvm/lib/Transforms/Instrumentation/AddressSanitizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3637,6 +3637,7 @@ void FunctionStackPoisoner::processStaticAllocas() {
"Variable descriptions relative to ASan stack base will be dropped");

// Replace Alloca instructions with base+offset.
SmallVector<Value *> NewAllocaPtrs;
for (const auto &Desc : SVD) {
AllocaInst *AI = Desc.AI;
replaceDbgDeclare(AI, LocalStackBaseAllocaPtr, DIB, DIExprFlags,
Expand All @@ -3645,6 +3646,7 @@ void FunctionStackPoisoner::processStaticAllocas() {
IRB.CreateAdd(LocalStackBase, ConstantInt::get(IntptrTy, Desc.Offset)),
AI->getType());
AI->replaceAllUsesWith(NewAllocaPtr);
NewAllocaPtrs.push_back(NewAllocaPtr);
}

// The left-most redzone has enough space for at least 4 pointers.
Expand Down Expand Up @@ -3694,6 +3696,15 @@ void FunctionStackPoisoner::processStaticAllocas() {
}
}

// Remove lifetime markers now that these are no longer allocas.
for (Value *NewAllocaPtr : NewAllocaPtrs) {
for (User *U : make_early_inc_range(NewAllocaPtr->users())) {
auto *I = cast<Instruction>(U);
if (I->isLifetimeStartOrEnd())
I->eraseFromParent();
}
}

SmallVector<uint8_t, 64> ShadowClean(ShadowAfterScope.size(), 0);
SmallVector<uint8_t, 64> ShadowAfterReturn;

Expand Down Expand Up @@ -3829,6 +3840,13 @@ void FunctionStackPoisoner::handleDynamicAllocaCall(AllocaInst *AI) {

Value *NewAddressPtr = IRB.CreateIntToPtr(NewAddress, AI->getType());

// Remove lifetime markers now that this is no longer an alloca.
for (User *U : make_early_inc_range(AI->users())) {
auto *I = cast<Instruction>(U);
if (I->isLifetimeStartOrEnd())
I->eraseFromParent();
}

// Replace all uses of AddessReturnedByAlloca with NewAddressPtr.
AI->replaceAllUsesWith(NewAddressPtr);

Expand Down
2 changes: 2 additions & 0 deletions llvm/lib/Transforms/Scalar/InferAddressSpaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,8 @@ bool InferAddressSpacesImpl::rewriteIntrinsicOperands(IntrinsicInst *II,
}
case Intrinsic::lifetime_start:
case Intrinsic::lifetime_end: {
// Always force lifetime markers to work directly on the alloca.
NewV = NewV->stripPointerCasts();
Function *NewDecl = Intrinsic::getOrInsertDeclaration(
M, II->getIntrinsicID(), {NewV->getType()});
II->setArgOperand(1, NewV);
Expand Down
14 changes: 10 additions & 4 deletions llvm/test/Analysis/BasicAA/modref.ll
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,33 @@ define i8 @test2a(ptr %P) {
ret i8 %A
}

define void @test3(ptr %P, i8 %X) {
define void @test3(i8 %X) {
; CHECK-LABEL: @test3(
; CHECK-NEXT: [[P2:%.*]] = getelementptr i8, ptr [[P:%.*]], i32 2
; CHECK-NEXT: [[P:%.*]] = alloca i64, align 8
; CHECK-NEXT: [[P2:%.*]] = getelementptr i8, ptr [[P]], i32 2
; CHECK-NEXT: call void @llvm.lifetime.end.p0(i64 1, ptr [[P]])
; CHECK-NEXT: store i8 2, ptr [[P2]], align 1
; CHECK-NEXT: call void @external(ptr [[P]])
; CHECK-NEXT: ret void
;
%P = alloca i64
%Y = add i8 %X, 1 ;; Dead, because the only use (the store) is dead.

%P2 = getelementptr i8, ptr %P, i32 2
store i8 %Y, ptr %P2 ;; Not read by lifetime.end, should be removed.
call void @llvm.lifetime.end.p0(i64 1, ptr %P)
store i8 2, ptr %P2
call void @external(ptr %P)
ret void
}

define void @test3a(ptr %P, i8 %X) {
define void @test3a(i8 %X) {
; CHECK-LABEL: @test3a(
; CHECK-NEXT: call void @llvm.lifetime.end.p0(i64 10, ptr [[P:%.*]])
; CHECK-NEXT: [[P:%.*]] = alloca i64, align 8
; CHECK-NEXT: call void @llvm.lifetime.end.p0(i64 10, ptr [[P]])
; CHECK-NEXT: ret void
;
%P = alloca i64
%Y = add i8 %X, 1 ;; Dead, because the only use (the store) is dead.

%P2 = getelementptr i8, ptr %P, i32 2
Expand Down
13 changes: 3 additions & 10 deletions llvm/test/Analysis/CallGraph/ignore-assumelike-calls.ll
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
; CHECK-EMPTY:
; CHECK-NEXT: Call graph node for function: 'bitcast_only'<<{{.*}}>> #uses=0
; CHECK-EMPTY:
; CHECK-NEXT: Call graph node for function: 'llvm.lifetime.start.p0'<<{{.*}}>> #uses=3
; CHECK-NEXT: Call graph node for function: 'llvm.lifetime.start.p0'<<{{.*}}>> #uses=2
; CHECK-EMPTY:
; CHECK-NEXT: Call graph node for function: 'llvm.memset.p0.i64'<<{{.*}}>> #uses=2
; CHECK-EMPTY:
Expand All @@ -25,18 +25,11 @@
; CHECK-NEXT: Call graph node for function: 'used_by_lifetime'<<{{.*}}>> #uses=0
; CHECK-NEXT: CS<{{.*}}> calls function 'llvm.lifetime.start.p0'
; CHECK-EMPTY:
; CHECK-NEXT: Call graph node for function: 'used_by_lifetime_cast'<<{{.*}}>> #uses=0
; CHECK-NEXT: CS<{{.*}}> calls function 'llvm.lifetime.start.p0'
; CHECK-EMPTY:

define internal void @used_by_lifetime() {
entry:
call void @llvm.lifetime.start.p0(i64 4, ptr @used_by_lifetime)
ret void
}

define internal void @used_by_lifetime_cast() addrspace(1) {
call void @llvm.lifetime.start.p0(i64 4, ptr addrspacecast (ptr addrspace(1) @used_by_lifetime_cast to ptr))
%a = alloca i8
call void @llvm.lifetime.start.p0(i64 4, ptr %a)
ret void
}

Expand Down
15 changes: 9 additions & 6 deletions llvm/test/Analysis/CostModel/X86/free-intrinsics.ll
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

define i32 @trivially_free() {
; CHECK-SIZE-LABEL: 'trivially_free'
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %alloca = alloca i8, align 1
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a0 = call i32 @llvm.annotation.i32.p0(i32 undef, ptr undef, ptr undef, i32 undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.assume(i1 undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.experimental.noalias.scope.decl(metadata !3)
Expand All @@ -13,14 +14,15 @@ define i32 @trivially_free() {
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a2 = call ptr @llvm.launder.invariant.group.p0(ptr undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a3 = call ptr @llvm.strip.invariant.group.p0(ptr undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a4 = call i1 @llvm.is.constant.i32(i32 undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.start.p0(i64 1, ptr undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.end.p0(i64 1, ptr undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.start.p0(i64 1, ptr %alloca)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.end.p0(i64 1, ptr %alloca)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a5 = call i64 @llvm.objectsize.i64.p0(ptr undef, i1 true, i1 true, i1 true)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a6 = call ptr @llvm.ptr.annotation.p0.p0(ptr undef, ptr undef, ptr undef, i32 undef, ptr undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.var.annotation.p0.p0(ptr undef, ptr undef, ptr undef, i32 undef, ptr undef)
; CHECK-SIZE-NEXT: Cost Model: Found an estimated cost of 1 for instruction: ret i32 undef
;
; CHECK-THROUGHPUT-LABEL: 'trivially_free'
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %alloca = alloca i8, align 1
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a0 = call i32 @llvm.annotation.i32.p0(i32 undef, ptr undef, ptr undef, i32 undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.assume(i1 undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.experimental.noalias.scope.decl(metadata !3)
Expand All @@ -30,13 +32,14 @@ define i32 @trivially_free() {
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a2 = call ptr @llvm.launder.invariant.group.p0(ptr undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a3 = call ptr @llvm.strip.invariant.group.p0(ptr undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a4 = call i1 @llvm.is.constant.i32(i32 undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.start.p0(i64 1, ptr undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.end.p0(i64 1, ptr undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.start.p0(i64 1, ptr %alloca)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.lifetime.end.p0(i64 1, ptr %alloca)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a5 = call i64 @llvm.objectsize.i64.p0(ptr undef, i1 true, i1 true, i1 true)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: %a6 = call ptr @llvm.ptr.annotation.p0.p0(ptr undef, ptr undef, ptr undef, i32 undef, ptr undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: call void @llvm.var.annotation.p0.p0(ptr undef, ptr undef, ptr undef, i32 undef, ptr undef)
; CHECK-THROUGHPUT-NEXT: Cost Model: Found an estimated cost of 0 for instruction: ret i32 undef
;
%alloca = alloca i8
%a0 = call i32 @llvm.annotation.i32(i32 undef, ptr undef, ptr undef, i32 undef)
call void @llvm.assume(i1 undef)
call void @llvm.experimental.noalias.scope.decl(metadata !4)
Expand All @@ -46,8 +49,8 @@ define i32 @trivially_free() {
%a2 = call ptr @llvm.launder.invariant.group.p0(ptr undef)
%a3 = call ptr @llvm.strip.invariant.group.p0(ptr undef)
%a4 = call i1 @llvm.is.constant.i32(i32 undef)
call void @llvm.lifetime.start.p0(i64 1, ptr undef)
call void @llvm.lifetime.end.p0(i64 1, ptr undef)
call void @llvm.lifetime.start.p0(i64 1, ptr %alloca)
call void @llvm.lifetime.end.p0(i64 1, ptr %alloca)
%a5 = call i64 @llvm.objectsize.i64.p0(ptr undef, i1 1, i1 1, i1 1)
%a6 = call ptr @llvm.ptr.annotation.p0(ptr undef, ptr undef, ptr undef, i32 undef, ptr undef)
call void @llvm.var.annotation(ptr undef, ptr undef, ptr undef, i32 undef, ptr undef)
Expand Down
Loading
Loading