[AllocBoxToStack] Don't destroy in dead-ends.

It is valid to leak a value on paths into dead-end regions.
Specifically, it is valid to leak an `alloc_box`.  Thus, "final
releases" in dead-end regions may not destroy the box and consequently
may not release its contents.  Therefore it's invalid to lower such final
releases to `dealloc_stack`s, let alone `destroy_addr`s.  The in-general
invalidity of that transformation results in miscompiling whenever a box
is leaked and its projected address is used after such final releases.

Fix this by not treating final releases as boundary markers of the
`alloc_box` and not lowering them to `destroy_addr`s and
`dealloc_stack`s.

rdar://158149082
This commit is contained in:
Nate Chandler
2025-08-25 16:45:46 -07:00
parent 55f82ea3a0
commit eb9f5b2a92
3 changed files with 473 additions and 10 deletions

View File

@@ -25,6 +25,8 @@
#include "swift/SIL/SILArgument.h"
#include "swift/SIL/SILBuilder.h"
#include "swift/SIL/SILCloner.h"
#include "swift/SILOptimizer/Analysis/DeadEndBlocksAnalysis.h"
#include "swift/SILOptimizer/Analysis/LoopAnalysis.h"
#include "swift/SILOptimizer/PassManager/Passes.h"
#include "swift/SILOptimizer/PassManager/Transforms.h"
#include "swift/SILOptimizer/Utils/InstOptUtils.h"
@@ -601,7 +603,9 @@ static void hoistMarkUnresolvedNonCopyableValueInsts(
/// rewriteAllocBoxAsAllocStack - Replace uses of the alloc_box with a
/// new alloc_stack, but do not delete the alloc_box yet.
static bool rewriteAllocBoxAsAllocStack(AllocBoxInst *ABI) {
static bool rewriteAllocBoxAsAllocStack(AllocBoxInst *ABI,
DeadEndBlocksAnalysis &deba,
SILLoopAnalysis &la) {
LLVM_DEBUG(llvm::dbgs() << "*** Promoting alloc_box to stack: " << *ABI);
SILValue HeapBox = ABI;
@@ -693,9 +697,31 @@ static bool rewriteAllocBoxAsAllocStack(AllocBoxInst *ABI) {
ABI->getBoxType(), ABI->getModule().Types, 0));
auto Loc = CleanupLocation(ABI->getLoc());
auto *deb = deba.get(ABI->getFunction());
for (auto LastRelease : FinalReleases) {
auto *dbi = dyn_cast<DeallocBoxInst>(LastRelease);
if (!dbi && deb->isDeadEnd(LastRelease->getParent()) &&
!la.get(ABI->getFunction())->getLoopFor(LastRelease->getParent())) {
// "Last" releases in dead-end regions may not actually destroy the box
// and consequently may not actually release the stored value. That's
// because values (including boxes) may be leaked along paths into
// dead-end regions. Thus it is invalid to lower such final releases of
// the box to destroy_addr's/dealloc_box's of the stack-promoted storage.
//
// There is one exception: if the alloc_box is in a dead-end loop. In
// that case SIL invariants require that the final releases actually
// destroy the box; otherwise, a box would leak once per loop. To check
// for this, it is sufficient check that the LastRelease is in a dead-end
// loop: if the alloc_box is not in that loop, then the entire loop is in
// the live range, so no release within the loop would be a "final
// release".
//
// None of this applies to dealloc_box instructions which always destroy
// the box.
continue;
}
SILBuilderWithScope Builder(LastRelease);
if (!isa<DeallocBoxInst>(LastRelease)&& !Lowering.isTrivial()) {
if (!dbi && !Lowering.isTrivial()) {
// If we have a mark_unresolved_non_copyable_value use of our stack box,
// we want to destroy that.
SILValue valueToDestroy = StackBox;
@@ -709,7 +735,6 @@ static bool rewriteAllocBoxAsAllocStack(AllocBoxInst *ABI) {
// instruction we found that isn't an explicit dealloc_box.
Builder.emitDestroyAddrAndFold(Loc, valueToDestroy);
}
auto *dbi = dyn_cast<DeallocBoxInst>(LastRelease);
if (dbi && dbi->isDeadEnd()) {
// Don't bother to create dealloc_stack instructions in dead-ends.
continue;
@@ -1265,7 +1290,9 @@ static void rewriteApplySites(AllocBoxToStackState &pass) {
/// Clone closure bodies and rewrite partial applies. Returns the number of
/// alloc_box allocations promoted.
static unsigned rewritePromotedBoxes(AllocBoxToStackState &pass) {
static unsigned rewritePromotedBoxes(AllocBoxToStackState &pass,
DeadEndBlocksAnalysis &deba,
SILLoopAnalysis &la) {
// First we'll rewrite any ApplySite that we can to remove
// the box container pointer from the operands.
rewriteApplySites(pass);
@@ -1274,7 +1301,7 @@ static unsigned rewritePromotedBoxes(AllocBoxToStackState &pass) {
auto rend = pass.Promotable.rend();
for (auto I = pass.Promotable.rbegin(); I != rend; ++I) {
auto *ABI = *I;
if (rewriteAllocBoxAsAllocStack(ABI)) {
if (rewriteAllocBoxAsAllocStack(ABI, deba, la)) {
++Count;
ABI->eraseFromParent();
}
@@ -1299,7 +1326,9 @@ class AllocBoxToStack : public SILFunctionTransform {
}
if (!pass.Promotable.empty()) {
auto Count = rewritePromotedBoxes(pass);
auto *deba = getAnalysis<DeadEndBlocksAnalysis>();
auto *la = getAnalysis<SILLoopAnalysis>();
auto Count = rewritePromotedBoxes(pass, *deba, *la);
NumStackPromoted += Count;
if (Count) {
if (StackNesting::fixNesting(getFunction()) == StackNesting::Changes::CFG)

View File

@@ -14,6 +14,8 @@ struct Bool {
protocol Error {}
class C {}
// CHECK-LABEL: sil @simple_promotion
sil @simple_promotion : $(Int) -> Int {
bb0(%0 : $Int):
@@ -901,7 +903,6 @@ bb2:
// CHECK: bb1:
// CHECK-NEXT: [[STACK2:%[0-9]+]] = alloc_stack $Bool
// CHECK-NEXT: dealloc_stack [[STACK2]]
// CHECK-NEXT: dealloc_stack [[BOX]]
// CHECK-NEXT: unreachable
// CHECK: bb2:
// CHECK: store {{%[0-9]+}}
@@ -1059,10 +1060,8 @@ bb3:
// CHECK-NEXT: [[AI:%[0-9]+]] = alloc_stack $Int
// CHECK-NEXT: cond_br
// CHECK: bb1:
// CHECK-NEXT: dealloc_stack [[AI]]
// CHECK-NEXT: br bb3
// CHECK: bb2:
// CHECK-NEXT: dealloc_stack [[AI]]
// CHECK-NEXT: br bb3
// CHECK: bb3:
// CHECK-NEXT: store {{%[0-9]+}}
@@ -1226,3 +1225,201 @@ bb0:
destroy_value %box : ${ var Builtin.Int32 }
return %value : $Builtin.Int32
}
sil @getC : $@convention(thin) () -> (@owned C)
// CHECK-LABEL: sil @leak_to_inf_loop_1 : {{.*}} {
// CHECK-NOT: destroy_addr
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'leak_to_inf_loop_1'
sil @leak_to_inf_loop_1 : $@convention(thin) (@owned C) -> () {
entry(%c : $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to %c_addr
strong_retain %box
strong_release %box
br loop
loop:
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c2 = apply %getC() : $@convention(thin) () -> (@owned C)
%c_old = load %c_addr
store %c2 to %c_addr
release_value %c_old
br loop
}
// CHECK-LABEL: sil @leak_to_inf_loop_2 : {{.*}} {
// CHECK: cond_br undef, [[EXIT:bb[0-9]+]], [[TO_LOOP:bb[0-9]+]]
// CHECK: [[EXIT]]
// CHECK: destroy_addr
// CHECK: dealloc_stack
// CHECK: [[TO_LOOP]]
// CHECK-NOT: destroy_addr
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'leak_to_inf_loop_2'
sil @leak_to_inf_loop_2 : $@convention(thin) (@owned C) -> () {
entry(%c : $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to %c_addr
strong_retain %box
strong_release %box
cond_br undef, exit, to_loop
exit:
strong_release %box
return undef : $()
to_loop:
strong_retain %box
strong_release %box
br loop
loop:
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c2 = apply %getC() : $@convention(thin) () -> (@owned C)
%c_old = load %c_addr
store %c2 to %c_addr
release_value %c_old
br loop
}
// CHECK-LABEL: sil @leak_to_inf_loop_3 : {{.*}} {
// CHECK-NOT: destroy_addr
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'leak_to_inf_loop_3'
sil @leak_to_inf_loop_3 : $@convention(thin) (@owned C) -> () {
entry(%c : $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to %c_addr
br to_loop
to_loop:
strong_retain %box
strong_release %box
br loop
loop:
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c2 = apply %getC() : $@convention(thin) () -> (@owned C)
%c_old = load %c_addr
store %c2 to %c_addr
release_value %c_old
br loop
}
// CHECK-LABEL: sil @dealloc_box_before_loop
// CHECK: dealloc_stack
// CHECK-LABEL: } // end sil function 'dealloc_box_before_loop'
sil @dealloc_box_before_loop : $@convention(thin) (@owned C) -> () {
entry(%c : $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to %c_addr
cond_br undef, exit, to_loop
exit:
strong_release %box
return undef : $()
to_loop:
dealloc_box %box
br loop
loop:
br loop
}
// CHECK-LABEL: sil @alloc_box_in_deadend_loop
// CHECK: alloc_stack
// CHECK: dealloc_stack
// CHECK-LABEL: } // end sil function 'alloc_box_in_deadend_loop'
sil @alloc_box_in_deadend_loop : $@convention(thin) () -> () {
entry:
br loop
loop:
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c = apply %getC() : $@convention(thin) () -> (@owned C)
store %c to %c_addr
strong_release %box
br loop
}
// CHECK-LABEL: sil @alloc_box_in_exiting_loop
// CHECK: br [[EXITING_LOOP:bb[0-9]+]]
// CHECK: [[EXITING_LOOP]]:
// CHECK: alloc_stack
// CHECK: cond_br undef, [[EXIT:bb[0-9]+]], [[LATCH:bb[0-9]+]]
// CHECK: [[LATCH]]:
// CHECK: cond_br undef, [[BACKEDGE:bb[0-9]+]], [[INFINITE_LOOP:bb[0-9]+]]
// CHECK: [[BACKEDGE]]:
// CHECK: dealloc_stack
// CHECK: br [[EXITING_LOOP]]
// CHECK: [[EXIT]]:
// CHECK: dealloc_stack
// CHECK: [[INFINITE_LOOP]]:
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'alloc_box_in_exiting_loop'
sil @alloc_box_in_exiting_loop : $@convention(thin) () -> () {
entry:
br exiting_loop
exiting_loop:
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
cond_br undef, exit, latch
latch:
cond_br undef, backedge, infinite_loop
backedge:
strong_release %box
br exiting_loop
exit:
strong_release %box
return undef : $()
infinite_loop:
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c = apply %getC() : $@convention(thin) () -> (@owned C)
store %c to %c_addr
strong_retain %box
strong_release %box
br infinite_loop
}
// CHECK-LABEL: sil @alloc_box_in_deadend_loop_with_another_deadend
// CHECK: br [[LOOP:bb[0-9]+]]
// CHECK: [[LOOP]]:
// CHECK: alloc_stack
// CHECK: cond_br undef, [[BACKEDGE:bb[0-9]+]], [[DIE:bb[0-9]+]]
// CHECK: [[BACKEDGE]]:
// CHECK: dealloc_stack
// CHECK: br [[LOOP]]
// CHECK: [[DIE]]:
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'alloc_box_in_deadend_loop_with_another_deadend'
sil @alloc_box_in_deadend_loop_with_another_deadend : $@convention(thin) () -> () {
entry:
br loop
loop:
%box = alloc_box ${ var C }
cond_br undef, backedge, die
backedge:
strong_release %box
br loop
die:
strong_retain %box
strong_release %box
unreachable
}

View File

@@ -14,6 +14,8 @@ struct Bool {
protocol Error {}
class C {}
// CHECK-LABEL: sil [ossa] @simple_promotion
sil [ossa] @simple_promotion : $@convention(thin) (Int) -> Int {
bb0(%0 : $Int):
@@ -1006,7 +1008,6 @@ bb2:
// CHECK: bb1:
// CHECK-NEXT: [[STACK2:%[0-9]+]] = alloc_stack $Bool
// CHECK-NEXT: dealloc_stack [[STACK2]]
// CHECK-NEXT: dealloc_stack [[BOX]]
// CHECK-NEXT: unreachable
// CHECK: bb2:
// CHECK: store
@@ -1203,3 +1204,239 @@ bb0(%0 : $*T, %1 : $*T, %2 : $*T):
%15 = tuple ()
return %15 : $()
}
// CHECK-LABEL: sil [ossa] @alloc_box_in_specialized_callee :
// CHECK-NOT: alloc_box
// CHECK-LABEL: } // end sil function 'alloc_box_in_specialized_callee'
sil [ossa] @alloc_box_in_specialized_callee : $@convention(thin) (Int) -> () {
bb0(%0 : $Int):
%1 = alloc_box ${ var Int }
%2 = project_box %1, 0
store %0 to [trivial] %2
%4 = function_ref @callee_with_allocbox : $@convention(thin) (@guaranteed { var Int }) -> ()
%5 = apply %4(%1) : $@convention(thin) (@guaranteed { var Int }) -> ()
destroy_value %1
%r = tuple ()
return %r
}
// CHECK-LABEL: sil shared [ossa] @$s20callee_with_allocboxTf0s_n :
// CHECK-NOT: alloc_box
// CHECK-LABEL: } // end sil function '$s20callee_with_allocboxTf0s_n'
// CHECK-LABEL: sil [ossa] @callee_with_allocbox :
// CHECK-NOT: alloc_box
// CHECK-LABEL: } // end sil function 'callee_with_allocbox'
sil [ossa] @callee_with_allocbox : $@convention(thin) (@guaranteed { var Int }) -> () {
bb0(%0 : @guaranteed ${ var Int }):
%1 = alloc_box ${ var Int }
%2 = project_box %1 : ${ var Int }, 0
%3 = project_box %0 : ${ var Int }, 0
copy_addr %3 to %2
destroy_value %1
%r = tuple ()
return %r
}
sil @getC : $@convention(thin) () -> (@owned C)
// CHECK-LABEL: sil [ossa] @leak_to_inf_loop_1 : {{.*}} {
// CHECK-NOT: destroy_addr
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'leak_to_inf_loop_1'
sil [ossa] @leak_to_inf_loop_1 : $@convention(thin) (@owned C) -> () {
entry(%c : @owned $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to [init] %c_addr
%copy = copy_value %box
destroy_value %box
br loop
loop:
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c2 = apply %getC() : $@convention(thin) () -> (@owned C)
%c_old = load [take] %c_addr
store %c2 to [init] %c_addr
destroy_value %c_old
br loop
}
// CHECK-LABEL: sil [ossa] @leak_to_inf_loop_2 : {{.*}} {
// CHECK: cond_br undef, [[EXIT:bb[0-9]+]], [[TO_LOOP:bb[0-9]+]]
// CHECK: [[EXIT]]
// CHECK: destroy_addr
// CHECK: dealloc_stack
// CHECK: [[TO_LOOP]]
// CHECK-NOT: destroy_addr
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'leak_to_inf_loop_2'
sil [ossa] @leak_to_inf_loop_2 : $@convention(thin) (@owned C) -> () {
entry(%c : @owned $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to [init] %c_addr
%copy = copy_value %box
destroy_value %box
cond_br undef, exit, to_loop
exit:
destroy_value %copy
return undef : $()
to_loop:
%copy2 = copy_value %copy
destroy_value %copy
br loop
loop:
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c2 = apply %getC() : $@convention(thin) () -> (@owned C)
%c_old = load [take] %c_addr
store %c2 to [init] %c_addr
destroy_value %c_old
br loop
}
// CHECK-LABEL: sil [ossa] @leak_to_inf_loop_3 : {{.*}} {
// CHECK-NOT: destroy_addr
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'leak_to_inf_loop_3'
sil [ossa] @leak_to_inf_loop_3 : $@convention(thin) (@owned C) -> () {
entry(%c : @owned $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to [init] %c_addr
br to_loop
to_loop:
%copy = copy_value %box
destroy_value %box
br loop
loop:
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c2 = apply %getC() : $@convention(thin) () -> (@owned C)
%c_old = load [take] %c_addr
store %c2 to [init] %c_addr
destroy_value %c_old
br loop
}
// CHECK-LABEL: sil [ossa] @dealloc_box_before_loop
// CHECK: dealloc_stack
// CHECK-LABEL: } // end sil function 'dealloc_box_before_loop'
sil [ossa] @dealloc_box_before_loop : $@convention(thin) (@owned C) -> () {
entry(%c : @owned $C):
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
store %c to [init] %c_addr
cond_br undef, exit, to_loop
exit:
destroy_value %box
return undef : $()
to_loop:
dealloc_box %box
br loop
loop:
br loop
}
// CHECK-LABEL: sil [ossa] @alloc_box_in_deadend_loop
// CHECK: alloc_stack
// CHECK: dealloc_stack
// CHECK-LABEL: } // end sil function 'alloc_box_in_deadend_loop'
sil [ossa] @alloc_box_in_deadend_loop : $@convention(thin) () -> () {
entry:
br loop
loop:
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c = apply %getC() : $@convention(thin) () -> (@owned C)
store %c to [init] %c_addr
destroy_value %box
br loop
}
// CHECK-LABEL: sil [ossa] @alloc_box_in_exiting_loop
// CHECK: br [[EXITING_LOOP:bb[0-9]+]]
// CHECK: [[EXITING_LOOP]]:
// CHECK: alloc_stack
// CHECK: cond_br undef, [[EXIT:bb[0-9]+]], [[LATCH:bb[0-9]+]]
// CHECK: [[LATCH]]:
// CHECK: cond_br undef, [[BACKEDGE:bb[0-9]+]], [[INFINITE_LOOP:bb[0-9]+]]
// CHECK: [[BACKEDGE]]:
// CHECK: dealloc_stack
// CHECK: br [[EXITING_LOOP]]
// CHECK: [[EXIT]]:
// CHECK: dealloc_stack
// CHECK: [[INFINITE_LOOP]]:
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'alloc_box_in_exiting_loop'
sil [ossa] @alloc_box_in_exiting_loop : $@convention(thin) () -> () {
entry:
br exiting_loop
exiting_loop:
%box = alloc_box ${ var C }
%c_addr = project_box %box, 0
%getC = function_ref @getC : $@convention(thin) () -> (@owned C)
%c = apply %getC() : $@convention(thin) () -> (@owned C)
store %c to [init] %c_addr
cond_br undef, exit, latch
latch:
cond_br undef, backedge, to_infinite_loop
backedge:
destroy_value %box
br exiting_loop
exit:
destroy_value %box
return undef : $()
to_infinite_loop:
br infinite_loop
infinite_loop:
%c2 = apply %getC() : $@convention(thin) () -> (@owned C)
store %c2 to [assign] %c_addr
%copy = copy_value %box
destroy_value %copy
br infinite_loop
}
// CHECK-LABEL: sil [ossa] @alloc_box_in_deadend_loop_with_another_deadend
// CHECK: br [[LOOP:bb[0-9]+]]
// CHECK: [[LOOP]]:
// CHECK: alloc_stack
// CHECK: cond_br undef, [[BACKEDGE:bb[0-9]+]], [[DIE:bb[0-9]+]]
// CHECK: [[BACKEDGE]]:
// CHECK: dealloc_stack
// CHECK: br [[LOOP]]
// CHECK: [[DIE]]:
// CHECK-NOT: dealloc_stack
// CHECK-LABEL: } // end sil function 'alloc_box_in_deadend_loop_with_another_deadend'
sil [ossa] @alloc_box_in_deadend_loop_with_another_deadend : $@convention(thin) () -> () {
entry:
br loop
loop:
%box = alloc_box ${ var C }
cond_br undef, backedge, die
backedge:
destroy_value %box
br loop
die:
%copy = copy_value %box
destroy_value %copy
unreachable
}