Skip to content

Commit 9dc59d2

Browse files
committed
Fix TempRValue: Add checkTempObjectDestroy bail out logic.
This avoids use-after-free bugs that can be introduced by removing a copy without replacing all corresponding destroys. Add more descriptive comments since multiple bugs have been introduced in this pass. None of this will be relevant once the pass is converted to OSSA.
1 parent 2e9b6b8 commit 9dc59d2

File tree

2 files changed

+144
-2
lines changed

2 files changed

+144
-2
lines changed

Diff for: lib/SILOptimizer/Transforms/CopyForwarding.cpp

+87
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
#include "swift/SILOptimizer/PassManager/Passes.h"
7070
#include "swift/SILOptimizer/PassManager/Transforms.h"
7171
#include "swift/SILOptimizer/Utils/CFGOptUtils.h"
72+
#include "swift/SILOptimizer/Utils/ValueLifetime.h"
7273
#include "llvm/ADT/SetVector.h"
7374
#include "llvm/ADT/Statistic.h"
7475
#include "llvm/Support/CommandLine.h"
@@ -1569,6 +1570,8 @@ class TempRValueOptPass : public SILFunctionTransform {
15691570
bool checkNoSourceModification(CopyAddrInst *copyInst,
15701571
const llvm::SmallPtrSetImpl<SILInstruction *> &useInsts);
15711572

1573+
bool checkTempObjectDestroy(AllocStackInst *tempObj, CopyAddrInst *copyInst);
1574+
15721575
bool tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst);
15731576

15741577
void run() override;
@@ -1620,6 +1623,17 @@ void TempRValueOptPass::run() {
16201623

16211624
/// Transitively explore all data flow uses of the given \p address until
16221625
/// reaching a load or returning false.
1626+
///
1627+
/// Any user opcode recognized by collectLoads must be replaced correctly later
1628+
/// during tryOptimizeCopyIntoTemp. If it is possible for any use to destroy the
1629+
/// value in \p address, then that use must be removed or made non-destructive
1630+
/// after the copy is removed and its operand is replaced.
1631+
///
1632+
/// Warning: To preserve the original object lifetime, tryOptimizeCopyIntoTemp
1633+
/// must assume that there are no holes in lifetime of the temporary stack
1634+
/// location at \address. The temporary must be initialized by the original copy
1635+
/// and never written to again. Therefore, collectLoads disallows any operation
1636+
/// that may write to memory at \p address.
16231637
bool TempRValueOptPass::collectLoads(
16241638
Operand *userOp, SILInstruction *user, SingleValueInstruction *address,
16251639
SILValue srcObject,
@@ -1768,6 +1782,73 @@ bool TempRValueOptPass::checkNoSourceModification(CopyAddrInst *copyInst,
17681782
return false;
17691783
}
17701784

1785+
/// Return true if the \p tempObj, which is initialized by \p copyInst, is
1786+
/// destroyed in an orthodox way.
1787+
///
1788+
/// When tryOptimizeCopyIntoTemp replaces all of tempObj's uses, it assumes that
1789+
/// the object is initialized by the original copy and directly destroyed on all
1790+
/// paths by one of the recognized 'destroy_addr' or 'copy_addr [take]'
1791+
/// operations. This assumption must be checked. For example, in non-OSSA,
1792+
/// it is legal to destroy an in-memory object by loading the value and
1793+
/// releasing it. Rather than detecting unbalanced load releases, simply check
1794+
/// that tempObj is destroyed directly on all paths.
1795+
bool TempRValueOptPass::checkTempObjectDestroy(AllocStackInst *tempObj,
1796+
CopyAddrInst *copyInst) {
1797+
// If the original copy was a take, then replacing all uses cannot affect
1798+
// the lifetime.
1799+
if (copyInst->isTakeOfSrc())
1800+
return true;
1801+
1802+
// ValueLifetimeAnalysis is not normally used for address types. It does not
1803+
// reason about the lifetime of the in-memory object. However the utility can
1804+
// be abused here to check that the address is directly destroyed on all
1805+
// paths. collectLoads has already guaranteed that tempObj's lifetime has no
1806+
// holes/reinitializations.
1807+
SmallVector<SILInstruction *, 8> users;
1808+
for (auto result : tempObj->getResults()) {
1809+
for (Operand *operand : result->getUses()) {
1810+
SILInstruction *user = operand->getUser();
1811+
if (user == copyInst)
1812+
continue;
1813+
if (isa<DeallocStackInst>(user))
1814+
continue;
1815+
users.push_back(user);
1816+
}
1817+
}
1818+
// Find the boundary of tempObj's address lifetime, starting at copyInst.
1819+
ValueLifetimeAnalysis vla(copyInst, users);
1820+
ValueLifetimeAnalysis::Frontier tempAddressFrontier;
1821+
if (!vla.computeFrontier(tempAddressFrontier,
1822+
ValueLifetimeAnalysis::DontModifyCFG)) {
1823+
return false;
1824+
}
1825+
// Check that the lifetime boundary ends at direct destroy points.
1826+
for (SILInstruction *frontierInst : tempAddressFrontier) {
1827+
auto pos = frontierInst->getIterator();
1828+
// If the frontier is at the head of a block, then either it is an
1829+
// unexpected lifetime exit, or the lifetime ended at a
1830+
// terminator. TempRValueOptPass does not handle either case.
1831+
if (pos == frontierInst->getParent()->begin())
1832+
return false;
1833+
1834+
// Look for a known destroy point as described in the funciton level
1835+
// comment. This whitelist can be expanded as more cases are handled in
1836+
// tryOptimizeCopyIntoTemp during copy replacement.
1837+
SILInstruction *lastUser = &*std::prev(pos);
1838+
if (isa<DestroyAddrInst>(lastUser))
1839+
continue;
1840+
1841+
if (auto *cai = dyn_cast<CopyAddrInst>(lastUser)) {
1842+
assert(cai->getSrc() == tempObj && "collectLoads checks for writes");
1843+
assert(!copyInst->isTakeOfSrc() && "checked above");
1844+
if (cai->isTakeOfSrc())
1845+
continue;
1846+
}
1847+
return false;
1848+
}
1849+
return true;
1850+
}
1851+
17711852
/// Tries to perform the temporary rvalue copy elimination for \p copyInst
17721853
bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
17731854
if (!copyInst->isInitializationOfDest())
@@ -1803,6 +1884,9 @@ bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
18031884
if (!checkNoSourceModification(copyInst, loadInsts))
18041885
return false;
18051886

1887+
if (!checkTempObjectDestroy(tempObj, copyInst))
1888+
return false;
1889+
18061890
LLVM_DEBUG(llvm::dbgs() << " Success: replace temp" << *tempObj);
18071891

18081892
// Do a "replaceAllUses" by either deleting the users or replacing them with
@@ -1833,6 +1917,9 @@ bool TempRValueOptPass::tryOptimizeCopyIntoTemp(CopyAddrInst *copyInst) {
18331917
use->set(copyInst->getSrc());
18341918
break;
18351919
}
1920+
// ASSUMPTION: no operations that may be handled by this default clause can
1921+
// destroy tempObj. This includes operations that load the value from memory
1922+
// and release it.
18361923
default:
18371924
use->set(copyInst->getSrc());
18381925
break;

Diff for: test/SILOptimizer/temp_rvalue_opt.sil

+57-2
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,11 @@ bb0(%0 : $*GS<B>, %1 : $*GS<B>):
106106

107107
// CHECK-LABEL: sil @take_from_temp
108108
// CHECK: bb0(%0 : $*B, %1 : $*GS<B>):
109-
// CHECK-NEXT: struct_element_addr
110-
// CHECK-NEXT: copy_addr [take]
109+
// CHECK-NEXT: [[STK:%.*]] = alloc_stack
110+
// CHECK-NEXT: copy_addr %1 to [initialization] [[STK]]
111+
// CHECK-NEXT: [[INNER:%.*]] = struct_element_addr
112+
// CHECK-NEXT: copy_addr [take] [[INNER]]
113+
// CHECK-NEXT: dealloc_stack
111114
// CHECK-NEXT: tuple
112115
// CHECK-NEXT: return
113116
sil @take_from_temp : $@convention(thin) <B> (@inout B, @inout GS<B>) -> () {
@@ -600,3 +603,55 @@ bb3:
600603
dealloc_stack %2 : $*Optional<P>
601604
br bb2
602605
}
606+
607+
///////////////////////////////////////////////////////////////////////////////
608+
// Test checkTempObjectDestroy
609+
// <rdar://problem/56393491> Use-after free crashing an XCTest.
610+
611+
sil @takeGuaranteedObj : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
612+
613+
// Do not remove a copy that is released via a load (because
614+
// TempRValueOpt is an address-based optimization does not know how to
615+
// remove releases, and trying to do that would reduce to ARC
616+
// optimization).
617+
// CHECK-LABEL: sil @copyWithLoadRelease : $@convention(thin) (@in_guaranteed Builtin.NativeObject) -> () {
618+
// CHECK: bb0(%0 : $*Builtin.NativeObject):
619+
// CHECK: [[STK:%.*]] = alloc_stack $Builtin.NativeObject
620+
// CHECK: copy_addr %0 to [initialization] [[STK]] : $*Builtin.NativeObject
621+
// CHECK: [[VAL:%.*]] = load [[STK]] : $*Builtin.NativeObject
622+
// CHECK: apply %{{.*}}([[VAL]]) : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
623+
// CHECK: release_value [[VAL]] : $Builtin.NativeObject
624+
// CHECK: dealloc_stack [[STK]] : $*Builtin.NativeObject
625+
// CHECK-LABEL: } // end sil function 'copyWithLoadRelease'
626+
sil @copyWithLoadRelease : $@convention(thin) (@in_guaranteed Builtin.NativeObject) -> () {
627+
bb0(%0 : $*Builtin.NativeObject):
628+
%stk = alloc_stack $Builtin.NativeObject
629+
copy_addr %0 to [initialization] %stk : $*Builtin.NativeObject
630+
%obj = load %stk : $*Builtin.NativeObject
631+
%f = function_ref @takeGuaranteedObj : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
632+
%call = apply %f(%obj) : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
633+
release_value %obj : $Builtin.NativeObject
634+
dealloc_stack %stk : $*Builtin.NativeObject
635+
%v = tuple ()
636+
return %v : $()
637+
}
638+
639+
// Remove a copy that is released via a load as long as it was a copy [take].
640+
// CHECK-LABEL: sil @takeWithLoadRelease : $@convention(thin) (@in Builtin.NativeObject) -> () {
641+
// CHECK: bb0(%0 : $*Builtin.NativeObject):
642+
// CHECK: [[V:%.*]] = load %0 : $*Builtin.NativeObject
643+
// CHECK: apply %{{.*}}([[V]]) : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
644+
// CHECK: release_value [[V]] : $Builtin.NativeObject
645+
// CHECK-LABEL: } // end sil function 'takeWithLoadRelease'
646+
sil @takeWithLoadRelease : $@convention(thin) (@in Builtin.NativeObject) -> () {
647+
bb0(%0 : $*Builtin.NativeObject):
648+
%stk = alloc_stack $Builtin.NativeObject
649+
copy_addr [take] %0 to [initialization] %stk : $*Builtin.NativeObject
650+
%obj = load %stk : $*Builtin.NativeObject
651+
%f = function_ref @takeGuaranteedObj : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
652+
%call = apply %f(%obj) : $@convention(thin) (@guaranteed Builtin.NativeObject) -> ()
653+
release_value %obj : $Builtin.NativeObject
654+
dealloc_stack %stk : $*Builtin.NativeObject
655+
%v = tuple ()
656+
return %v : $()
657+
}

0 commit comments

Comments
 (0)