Skip to content

Commit 3de8263

Browse files
committed
[silgenpattern] When emitting address only enum element dispatch, do not leak the switch's operand along the default path.
For the following discussion, let OP be the switch's operand. This is implemented by: * Modeling switch_enum_addr as not forwarding OP and instead delegate the forwarding of OP to be done by each enum case. This matches what SILGen is actually doing since the actual taking of the address in SIL is done in each enum case block by an unchecked_take_enum_data_addr. * In each enum case, I treat OP as being forwarded into an irrefutable sub-tree. I follow the pattern of other places this is done by creating a CleanupStateRestorationScope and using forwardIntoIrrefutableSubTree. This ensures that if I forward OP in the enum case, it just becomes dormant instead of being thrown away. * Inside each case, there is a bunch of code that does some final preparations to src before dispatching to the inner dispatch. This code was written using old low-level SILValue APIs where ownership is done by hand. I replaced all of that by hand ownership with higher level Managed Value APIs that automatically handle ownership for the user. This simplified the implementation and ensured correctness via SILGenBuilder API invariants. The end result of all of these together is that: 1. The cleanup on OP is still live when we emit the default case later than the cleanups. This eliminates the leak that I am fixing. 2. We have greater correctness since the SILGenBuilder APIs automatically handle ownership for us. 3. We have eliminated some brittle logic that could in the future introduce bugs. Specifically, I noticed that if we were ever given a ConsumableManagedValue that was CopyOnSuccess or BorrowAlways but its ManagedValue was a +1 value, we would leak. I could not figure out how to create a Swift test case that would go down this code path though = (. But that being said, it is one new language feature away from being broken. I added some asserts to ConsumableManagedValue that ensures this invariant, so we are safe. If it is inconvenient, we can also cause ConsumableManagedValue to create a new ManagedValue when it detects this condition without the cleanup. But lets see how difficult it is to keep this invariant. In terms of testing, I put in both a SILGen test and also an end<->end interpreter test to ensure this doesn't break again. rdar://71992652 SR-13926
1 parent f7e6aa5 commit 3de8263

File tree

3 files changed

+136
-16
lines changed

3 files changed

+136
-16
lines changed

lib/SILGen/SILGenPattern.cpp

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,25 @@ getManagedSubobject(SILGenFunction &SGF, SILValue value,
13981398
llvm_unreachable("covered switch");
13991399
}
14001400

1401+
/// Given that we've broken down a source value into this subobject,
1402+
/// and that we were supposed to use the given consumption rules on
1403+
/// it, construct an appropriate managed value.
1404+
static ConsumableManagedValue
1405+
getManagedSubobject(SILGenFunction &SGF, ManagedValue value,
1406+
CastConsumptionKind consumption) {
1407+
switch (consumption) {
1408+
case CastConsumptionKind::BorrowAlways:
1409+
case CastConsumptionKind::CopyOnSuccess:
1410+
return {value.unmanagedBorrow(), consumption};
1411+
case CastConsumptionKind::TakeAlways:
1412+
case CastConsumptionKind::TakeOnSuccess: {
1413+
auto loc = RegularLocation::getAutoGeneratedLocation();
1414+
return {value.ensurePlusOne(SGF, loc), consumption};
1415+
}
1416+
}
1417+
llvm_unreachable("covered switch");
1418+
}
1419+
14011420
static ConsumableManagedValue
14021421
emitReabstractedSubobject(SILGenFunction &SGF, SILLocation loc,
14031422
ConsumableManagedValue value,
@@ -2064,8 +2083,9 @@ void PatternMatchEmission::emitEnumElementDispatch(
20642083
}
20652084

20662085
// Emit the switch_enum_addr instruction.
2067-
SILValue srcValue = src.getFinalManagedValue().forward(SGF);
2068-
SGF.B.createSwitchEnumAddr(loc, srcValue, blocks.getDefaultBlock(),
2086+
//
2087+
// NOTE: switch_enum_addr does not actually consume the underlying value.
2088+
SGF.B.createSwitchEnumAddr(loc, src.getValue(), blocks.getDefaultBlock(),
20692089
blocks.getCaseBlocks(), blocks.getCounts(),
20702090
defaultCaseCount);
20712091

@@ -2077,12 +2097,16 @@ void PatternMatchEmission::emitEnumElementDispatch(
20772097

20782098
SGF.B.setInsertionPoint(caseBB);
20792099

2100+
// We need to make sure our cleanup stays around long enough for us to emit
2101+
// our destroy, so setup a cleanup state restoration scope for each case.
2102+
CleanupStateRestorationScope srcScope(SGF.Cleanups);
2103+
forwardIntoIrrefutableSubtree(SGF, srcScope, src);
2104+
20802105
// We're in conditionally-executed code; enter a scope.
20812106
Scope scope(SGF.Cleanups, CleanupLocation::get(loc));
2082-
2107+
20832108
// Create a BB argument or 'unchecked_take_enum_data_addr'
20842109
// instruction to receive the enum case data if it has any.
2085-
20862110
SILType eltTy;
20872111
bool hasElt = false;
20882112
if (eltDecl->hasAssociatedValues()) {
@@ -2108,6 +2132,15 @@ void PatternMatchEmission::emitEnumElementDispatch(
21082132
}
21092133
}
21102134

2135+
// Forward src along this path so we don't emit a destroy_addr on our
2136+
// subject value for this case.
2137+
//
2138+
// FIXME: Do we actually want to do this? SILGen tests today assume this
2139+
// pattern. It might be worth leaving the destroy_addr there to create
2140+
// additional liveness information. For now though, we maintain the
2141+
// current behavior.
2142+
src.getFinalManagedValue().forward(SGF);
2143+
21112144
SILValue result;
21122145
if (hasNonAny) {
21132146
result = SGF.emitEmptyTuple(loc);
@@ -2132,16 +2165,18 @@ void PatternMatchEmission::emitEnumElementDispatch(
21322165
eltConsumption = CastConsumptionKind::TakeAlways;
21332166
}
21342167

2135-
SILValue eltValue;
2168+
ManagedValue eltValue;
21362169
// We can only project destructively from an address-only enum, so
21372170
// copy the value if we can't consume it.
21382171
// TODO: Should have a more efficient way to copy payload
21392172
// nondestructively from an enum.
21402173
switch (eltConsumption) {
2141-
case CastConsumptionKind::TakeAlways:
2142-
eltValue =
2143-
SGF.B.createUncheckedTakeEnumDataAddr(loc, srcValue, eltDecl, eltTy);
2174+
case CastConsumptionKind::TakeAlways: {
2175+
auto finalValue = src.getFinalManagedValue();
2176+
eltValue = SGF.B.createUncheckedTakeEnumDataAddr(loc, finalValue,
2177+
eltDecl, eltTy);
21442178
break;
2179+
}
21452180
case CastConsumptionKind::BorrowAlways:
21462181
// If we reach this point, we know that we have a loadable
21472182
// element type from an enum with mixed address
@@ -2150,11 +2185,15 @@ void PatternMatchEmission::emitEnumElementDispatch(
21502185
// address only types do not support BorrowAlways.
21512186
llvm_unreachable("not allowed");
21522187
case CastConsumptionKind::CopyOnSuccess: {
2153-
auto copy = SGF.emitTemporaryAllocation(loc, srcValue->getType());
2154-
SGF.B.createCopyAddr(loc, srcValue, copy, IsNotTake, IsInitialization);
2188+
auto temp = SGF.emitTemporary(loc, SGF.getTypeLowering(src.getType()));
2189+
SGF.B.createCopyAddr(loc, src.getValue(), temp->getAddress(), IsNotTake,
2190+
IsInitialization);
2191+
temp->finishInitialization(SGF);
2192+
21552193
// We can always take from the copy.
21562194
eltConsumption = CastConsumptionKind::TakeAlways;
2157-
eltValue = SGF.B.createUncheckedTakeEnumDataAddr(loc, copy, eltDecl, eltTy);
2195+
eltValue = SGF.B.createUncheckedTakeEnumDataAddr(
2196+
loc, temp->getManagedAddress(), eltDecl, eltTy);
21582197
break;
21592198
}
21602199

@@ -2170,17 +2209,16 @@ void PatternMatchEmission::emitEnumElementDispatch(
21702209
if (eltTL->isLoadable()) {
21712210
// If we do not have a loadable value, just use getManagedSubObject
21722211
// Load a loadable data value.
2173-
auto managedEltValue = ManagedValue::forUnmanaged(eltValue);
21742212
if (eltConsumption == CastConsumptionKind::CopyOnSuccess) {
2175-
managedEltValue = SGF.B.createLoadBorrow(loc, managedEltValue);
2213+
eltValue = SGF.B.createLoadBorrow(loc, eltValue);
21762214
eltConsumption = CastConsumptionKind::BorrowAlways;
21772215
} else {
21782216
assert(eltConsumption == CastConsumptionKind::TakeAlways);
2179-
managedEltValue = SGF.B.createLoadTake(loc, managedEltValue);
2217+
eltValue = SGF.B.createLoadTake(loc, eltValue);
21802218
}
2181-
origCMV = {managedEltValue, eltConsumption};
2219+
origCMV = {eltValue, eltConsumption};
21822220
} else {
2183-
origCMV = getManagedSubobject(SGF, eltValue, *eltTL, eltConsumption);
2221+
origCMV = getManagedSubobject(SGF, eltValue, eltConsumption);
21842222
}
21852223

21862224
eltCMV = origCMV;

test/Interpreter/switch_default.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// RUN: %target-run-simple-swift
2+
3+
// REQUIRES: executable_test
4+
5+
import StdlibUnittest
6+
7+
var SwitchDefaultTestSuite = TestSuite("Switch.Default")
8+
defer { runAllTests() }
9+
10+
class Klass {}
11+
protocol Protocol {}
12+
13+
enum Enum {
14+
case value1(LifetimeTracked)
15+
case value2(Protocol)
16+
}
17+
18+
SwitchDefaultTestSuite.test("do not leak default case payload") {
19+
// We are passing in Enum.value1 so we go down the default and leak our
20+
// lifetime tracked.
21+
func f(_ e: Enum?) {
22+
switch (e) {
23+
case .value2: return
24+
default: return
25+
}
26+
}
27+
f(.value1(LifetimeTracked(0)))
28+
}

test/SILGen/switch_default.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// RUN: %target-swift-emit-silgen -module-name switch_default %s | %FileCheck %s
2+
3+
class Klass {}
4+
protocol Protocol {}
5+
6+
enum Enum {
7+
case value1(Klass)
8+
case value2(Protocol)
9+
}
10+
11+
// CHECK-LABEL: sil hidden [ossa] @$s14switch_default33testAddressOnlySubjectDefaultCaseyyAA4EnumOSgF : $@convention(thin) (@in_guaranteed Optional<Enum>) -> () {
12+
// CHECK: bb0([[ARG:%.*]] :
13+
// CHECK: [[STACK:%.*]] = alloc_stack $Optional<Enum>
14+
// CHECK: copy_addr [[ARG]] to [initialization] [[STACK]]
15+
// CHECK: switch_enum_addr [[STACK]] : $*Optional<Enum>, case #Optional.some!enumelt: [[SOME_BB:bb[0-9]*]], default [[DEFAULT_BB:bb[0-9]*]]
16+
//
17+
// CHECK: [[SOME_BB]]:
18+
// CHECK: [[STACK_1:%.*]] = alloc_stack $Optional<Enum>
19+
// CHECK: copy_addr [[STACK]] to [initialization] [[STACK_1]]
20+
// CHECK: [[TAKEN_ADDR:%.*]] = unchecked_take_enum_data_addr [[STACK_1]]
21+
// CHECK: switch_enum_addr [[TAKEN_ADDR]] : $*Enum, case #Enum.value2!enumelt: [[VALUE2_BB:bb[0-9]*]], default [[DEFAULT_BB_2:bb[0-9]*]]
22+
//
23+
// CHECK: [[VALUE2_BB]]:
24+
// CHECK: [[TAKEN_TAKEN_ADDR:%.*]] = unchecked_take_enum_data_addr [[TAKEN_ADDR]]
25+
// CHECK: destroy_addr [[TAKEN_TAKEN_ADDR]]
26+
// CHECK: dealloc_stack [[STACK_1]]
27+
// CHECK: destroy_addr [[STACK]]
28+
// CHECK: dealloc_stack [[STACK]]
29+
// CHECK: br [[EXIT_BB:bb[0-9]+]]
30+
//
31+
// We used to leak here!
32+
// CHECK: [[DEFAULT_BB_2]]:
33+
// CHECK: destroy_addr [[TAKEN_ADDR]]
34+
// CHECK: dealloc_stack [[STACK_1]]
35+
// CHECK: br [[CONT_BB:bb[0-9]*]]
36+
//
37+
// CHECK: [[DEFAULT_BB]]:
38+
// CHECK: br [[CONT_BB]]
39+
//
40+
// CHECK: [[CONT_BB]]:
41+
// CHECK: destroy_addr [[STACK]]
42+
// CHECK: dealloc_stack [[STACK]]
43+
// CHECK: br [[EXIT_BB]]
44+
//
45+
// CHECK: [[EXIT_BB]]:
46+
// CHECK-NEXT: tuple
47+
// CHECK-NEXT: return
48+
// } // end sil function '$s14switch_default33testAddressOnlySubjectDefaultCaseyyAA4EnumOSgF'
49+
func testAddressOnlySubjectDefaultCase(_ e: Enum?) {
50+
switch (e) {
51+
case .value2: return
52+
default: return
53+
}
54+
}

0 commit comments

Comments
 (0)