Skip to content

Commit 9d40198

Browse files
committed
SIL: fix miscompiles of non-Copyable struct/enum with deinits
The SIL optimizer has fundamental bugs that result in dropping non-Copyable struct & enum the deinitializers. Fix this by 1. correctly representing the ownership of struct & enum values that are initialized from trivial values. 2. checking move-only types before deleting forwarding instructions. These bugs block other bug fixes. They are exposed by other unrelated SIL optimizations to SIL. I'm sure its possible to expose the bugs with source-level tests, but the current order of inlining and deinit devirtualization has been hiding the bugs and complicates reproduction.
1 parent 2f2051d commit 9d40198

File tree

5 files changed

+96
-5
lines changed

5 files changed

+96
-5
lines changed

include/swift/SIL/SILInstruction.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,9 @@ class ForwardingInstruction {
12931293
/// The resulting forwarded value's ownership, returned by getOwnershipKind(),
12941294
/// is not identical to the forwarding ownership. It differs when the result
12951295
/// is trivial type. e.g. an owned or guaranteed value can be cast to a
1296-
/// trivial type using owned or guaranteed forwarding.
1296+
/// trivial type using owned or guaranteed forwarding. Similarly, if a trivial
1297+
/// value is forwarded into an owned non-Copyable struct or enum, forwarding
1298+
/// ownership is 'none' while value ownerhip is 'owned'.
12971299
ValueOwnershipKind getForwardingOwnershipKind() const {
12981300
return ownershipKind;
12991301
}
@@ -7009,8 +7011,9 @@ class EnumInst
70097011
EnumInst(SILDebugLocation DebugLoc, SILValue Operand,
70107012
EnumElementDecl *Element, SILType ResultTy,
70117013
ValueOwnershipKind forwardingOwnershipKind)
7012-
: InstructionBase(DebugLoc, ResultTy, forwardingOwnershipKind),
7013-
Element(Element) {
7014+
: InstructionBase(DebugLoc, ResultTy,
7015+
forwardingOwnershipKind.forwardToInit(ResultTy)),
7016+
Element(Element) {
70147017
sharedUInt32().EnumInst.caseIndex = InvalidCaseIndex;
70157018

70167019
if (Operand) {

include/swift/SIL/SILValue.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,30 @@ struct ValueOwnershipKind {
353353
});
354354
}
355355

356+
// An initialized value whose nominal type has a deinit() must be 'owned'. For
357+
// example, an owned struct/enum-with-deinit may be initialized by
358+
// "forwarding" a trivial value. A struct/enum-with-deinit must be prevented
359+
// from forwarding a guaranteed value.
360+
//
361+
// Simply consider all non-Copyable types to be 'owned'. This could instead be
362+
// limited to isValueTypeWithDeinit(). However, forcing non-Copyable types to
363+
// be owned is consistent with the fact that their type is non-Trivial and
364+
// simplifies reasoning about non-Copyable ownership.
365+
ValueOwnershipKind forwardToInit(SILType nominalType) {
366+
if (nominalType.isMoveOnly()) {
367+
switch (value) {
368+
case OwnershipKind::Any:
369+
case OwnershipKind::None:
370+
case OwnershipKind::Owned:
371+
return OwnershipKind::Owned;
372+
case OwnershipKind::Guaranteed:
373+
case OwnershipKind::Unowned:
374+
ABORT("Cannot initialize a nonCopyable type with a guaranteed value");
375+
}
376+
}
377+
return *this;
378+
}
379+
356380
StringRef asString() const;
357381
};
358382

lib/SIL/IR/SILInstructions.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,8 +1509,9 @@ StructInst *StructInst::create(SILDebugLocation Loc, SILType Ty,
15091509
StructInst::StructInst(SILDebugLocation Loc, SILType Ty,
15101510
ArrayRef<SILValue> Elems,
15111511
ValueOwnershipKind forwardingOwnershipKind)
1512-
: InstructionBaseWithTrailingOperands(Elems, Loc, Ty,
1513-
forwardingOwnershipKind) {
1512+
: InstructionBaseWithTrailingOperands(
1513+
Elems, Loc, Ty, forwardingOwnershipKind.forwardToInit(Ty))
1514+
{
15141515
assert(!Ty.getStructOrBoundGenericStruct()->hasUnreferenceableStorage());
15151516
}
15161517

lib/SILOptimizer/Utils/CanonicalizeInstruction.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,10 @@ static SILBasicBlock::iterator
548548
eliminateUnneededForwardingUnarySingleValueInst(SingleValueInstruction *inst,
549549
CanonicalizeInstruction &pass) {
550550
auto next = std::next(inst->getIterator());
551+
if (inst->getType().isValueTypeWithDeinit()) {
552+
// Avoid bypassing non-Copyable struct/enum deinitializers.
553+
return next;
554+
}
551555
if (isa<DropDeinitInst>(inst))
552556
return next;
553557
if (auto *uedi = dyn_cast<UncheckedEnumDataInst>(inst)) {

test/SILOptimizer/sil_combine_moveonly.sil

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ struct Wrapper<T>: ~Copyable {
3131
var t: T
3232
}
3333

34+
struct NonCopyableForwardingStruct : ~Copyable {
35+
var some: Int
36+
deinit
37+
}
38+
39+
enum NonCopyableForwardingEnum : ~Copyable {
40+
case some(Int)
41+
case nothing
42+
deinit
43+
}
44+
3445
sil @getWrappedValue : $@convention(thin) <T> (@in_guaranteed Wrapper<T>) -> @out T
3546

3647
// Test that a release_value is not removed for a struct-with-deinit.
@@ -110,3 +121,51 @@ bb0(%0 : @owned $S):
110121
%64 = tuple ()
111122
return %64 : $()
112123
}
124+
125+
// Test a "dead" non-Copyable struct-with-deinit that forwards a
126+
// single trivial value. The destroy_value cannot be eliminated
127+
// without first devirtualizing the deinit.
128+
//
129+
// CHECK-LABEL: sil [ossa] @testNonCopyableForwardingStructDeinit : $@convention(thin) (Int) -> () {
130+
// CHECK: [[STRUCT:%[0-9]+]] = struct $NonCopyableForwardingStruct (%0 : $Int)
131+
// CHECK: destroy_value [[STRUCT]]
132+
// CHECK-LABEL: } // end sil function 'testNonCopyableForwardingStructDeinit'
133+
sil [ossa] @testNonCopyableForwardingStructDeinit : $@convention(thin) (Int) -> () {
134+
bb0(%0: $Int):
135+
%1 = struct $NonCopyableForwardingStruct(%0 : $Int)
136+
destroy_value %1
137+
%13 = tuple ()
138+
return %13
139+
}
140+
141+
// Test a "dead" non-Copyable enum-with-deinit that forwards a single
142+
// trivial value. The destroy_value cannot be eliminated without first
143+
// devirtualizing the deinit.
144+
//
145+
// CHECK-LABEL: sil [ossa] @testNonCopyableForwardingEnumDeinit : $@convention(thin) (Int) -> () {
146+
// CHECK: [[ENUM:%[0-9]+]] = enum $NonCopyableForwardingEnum, #NonCopyableForwardingEnum.some!enumelt, %0
147+
// CHECK: destroy_value [[ENUM]]
148+
// CHECK-LABEL: } // end sil function 'testNonCopyableForwardingEnumDeinit'
149+
sil [ossa] @testNonCopyableForwardingEnumDeinit : $@convention(thin) (Int) -> () {
150+
bb0(%0 : $Int):
151+
%1 = enum $NonCopyableForwardingEnum, #NonCopyableForwardingEnum.some!enumelt, %0
152+
destroy_value %1
153+
%13 = tuple ()
154+
return %13
155+
}
156+
157+
// Test a "dead" non-Copyable enum-with-deinit and an empty case. The
158+
// destroy_value cannot be eliminated without first devirtualizing the
159+
// deinit.
160+
//
161+
// CHECK-LABEL: sil [ossa] @testNonCopyableEmptyEnumDeinit : $@convention(thin) () -> () {
162+
// CHECK: [[ENUM:%[0-9]+]] = enum $NonCopyableForwardingEnum, #NonCopyableForwardingEnum.nothing
163+
// CHECK: destroy_value [[ENUM]]
164+
// CHECK-LABEL: } // end sil function 'testNonCopyableEmptyEnumDeinit'
165+
sil [ossa] @testNonCopyableEmptyEnumDeinit : $@convention(thin) () -> () {
166+
bb0:
167+
%0 = enum $NonCopyableForwardingEnum, #NonCopyableForwardingEnum.nothing
168+
destroy_value %0
169+
%13 = tuple ()
170+
return %13
171+
}

0 commit comments

Comments
 (0)