Skip to content

Commit 2040b55

Browse files
authored
[IR] Fix User use-after-destroy by zapping in ~User (llvm#170575)
First, this moves the removal of operands from use lists from `User::operator delete` to `User::~User`. This is straightforward, and nothing blocks that. Second, this makes LLVM more compatible with bug finding tools like MSan, GCC `-flifetime-dse`, and forthcoming enhancements to Clang itself through `dead_on_return` annotations. However, the complication is that `User::operator delete` needs to recover the start of the allocation, and it needs to recover that information somehow without examining the fields of the `User` object. The natural way to handle this is for the destructor to return an adjusted `this` pointer, and that's in fact how deleting destructors are often implemented, but it requires making assumptions about the C++ ABI. Another solution to this problem in C++20 would be to use [destroying delete](https://en.cppreference.com/w/cpp/memory/new/destroying_delete_t), which should be on our roadmap, since it would allow us to eliminate `deleteValue`, and move that polymorphic switch into the destroying delete operator, instead of having to use this funky method. Since we don't have C++20 yet, it seems practical to store the information into the operand memory, to the left of `this`, and to reload the start of the allocation from `((void**)this)[-1]` after the destructor runs. The downside is that zero-operand Users such as `ret void`, `unreachable`, `fence`, and `ConstantInt` must allocate one more pointer worth of memory to the left of the main allocation, just to thread this information through to `User::operator delete`. This change avoids increasing the effective size of all `ConstantData` instances by specializing `ConstantData` new and delete, and adding a type check to `~User`. When we have C++20, we should definitely replace all of this with the destroying delete solution, which is much clearer, but for now, this is a low-cost fix to long-standing UB and it unblocks other work, so it deserves to land. Fixes llvm#24952
1 parent c907d7d commit 2040b55

3 files changed

Lines changed: 47 additions & 29 deletions

File tree

llvm/include/llvm/IR/Constants.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,10 @@ class ConstantData : public Constant {
6565
protected:
6666
explicit ConstantData(Type *Ty, ValueTy VT) : Constant(Ty, VT, AllocMarker) {}
6767

68-
void *operator new(size_t S) { return User::operator new(S, AllocMarker); }
68+
void *operator new(size_t S) { return ::operator new(S); }
6969

7070
public:
71-
void operator delete(void *Ptr) { User::operator delete(Ptr); }
71+
void operator delete(void *Ptr) { ::operator delete(Ptr); }
7272

7373
ConstantData(const ConstantData &) = delete;
7474

llvm/include/llvm/IR/User.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ class User : public Value {
141141
LLVM_ABI void growHungoffUses(unsigned N, bool WithExtraValues = false);
142142

143143
protected:
144-
~User() = default; // Use deleteValue() to delete a generic Instruction.
144+
// Use deleteValue() to delete a generic User.
145+
~User();
145146

146147
public:
147148
User(const User &) = delete;

llvm/lib/IR/User.cpp

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "llvm/IR/User.h"
1010
#include "llvm/IR/Constant.h"
11+
#include "llvm/IR/Constants.h"
1112
#include "llvm/IR/GlobalValue.h"
1213
#include "llvm/IR/IntrinsicInst.h"
1314

@@ -144,19 +145,24 @@ void *User::allocateFixedOperandUser(size_t Size, unsigned Us,
144145
assert(DescBytesToAllocate % sizeof(void *) == 0 &&
145146
"We need this to satisfy alignment constraints for Uses");
146147

147-
uint8_t *Storage = static_cast<uint8_t *>(
148-
::operator new(Size + sizeof(Use) * Us + DescBytesToAllocate));
149-
Use *Start = reinterpret_cast<Use *>(Storage + DescBytesToAllocate);
150-
Use *End = Start + Us;
151-
User *Obj = reinterpret_cast<User *>(End);
148+
size_t LeadingSize = DescBytesToAllocate + sizeof(Use) * Us;
149+
150+
// Ensure we allocate at least one pointer's worth of space before the main
151+
// user allocation. We use this memory to pass information from the destructor
152+
// to the deletion operator, so it can recover the true allocation start.
153+
LeadingSize = std::max(LeadingSize, sizeof(void *));
154+
155+
uint8_t *Storage = static_cast<uint8_t *>(::operator new(LeadingSize + Size));
156+
User *Obj = reinterpret_cast<User *>(Storage + LeadingSize);
157+
Use *Operands = reinterpret_cast<Use *>(Obj) - Us;
152158
Obj->NumUserOperands = Us;
153159
Obj->HasHungOffUses = false;
154160
Obj->HasDescriptor = DescBytes != 0;
155-
for (; Start != End; Start++)
156-
new (Start) Use(Obj);
161+
for (unsigned I = 0; I < Us; ++I)
162+
new (&Operands[I]) Use(Obj);
157163

158164
if (DescBytes != 0) {
159-
auto *DescInfo = reinterpret_cast<DescriptorInfo *>(Storage + DescBytes);
165+
auto *DescInfo = reinterpret_cast<DescriptorInfo *>(Operands) - 1;
160166
DescInfo->SizeInBytes = DescBytes;
161167
}
162168

@@ -189,31 +195,42 @@ void *User::operator new(size_t Size, HungOffOperandsAllocMarker) {
189195
// User operator delete Implementation
190196
//===----------------------------------------------------------------------===//
191197

192-
// Repress memory sanitization, due to use-after-destroy by operator
193-
// delete. Bug report 24578 identifies this issue.
194-
LLVM_NO_SANITIZE_MEMORY_ATTRIBUTE void User::operator delete(void *Usr) {
198+
User::~User() {
195199
// Hung off uses use a single Use* before the User, while other subclasses
196200
// use a Use[] allocated prior to the user.
197-
User *Obj = static_cast<User *>(Usr);
198-
if (Obj->HasHungOffUses) {
199-
assert(!Obj->HasDescriptor && "not supported!");
201+
void *AllocStart = nullptr;
202+
if (HasHungOffUses) {
203+
assert(!HasDescriptor && "not supported!");
200204

201-
Use **HungOffOperandList = static_cast<Use **>(Usr) - 1;
205+
Use **HungOffOperandList = reinterpret_cast<Use **>(this) - 1;
202206
// drop the hung off uses.
203-
Use::zap(*HungOffOperandList, *HungOffOperandList + Obj->NumUserOperands,
207+
Use::zap(*HungOffOperandList, *HungOffOperandList + NumUserOperands,
204208
/* Delete */ true);
205-
::operator delete(HungOffOperandList);
206-
} else if (Obj->HasDescriptor) {
207-
Use *UseBegin = static_cast<Use *>(Usr) - Obj->NumUserOperands;
208-
Use::zap(UseBegin, UseBegin + Obj->NumUserOperands, /* Delete */ false);
209+
AllocStart = HungOffOperandList;
210+
} else if (HasDescriptor) {
211+
Use *UseBegin = reinterpret_cast<Use *>(this) - NumUserOperands;
212+
Use::zap(UseBegin, UseBegin + NumUserOperands, /* Delete */ false);
209213

210214
auto *DI = reinterpret_cast<DescriptorInfo *>(UseBegin) - 1;
211-
uint8_t *Storage = reinterpret_cast<uint8_t *>(DI) - DI->SizeInBytes;
212-
::operator delete(Storage);
213-
} else {
214-
Use *Storage = static_cast<Use *>(Usr) - Obj->NumUserOperands;
215-
Use::zap(Storage, Storage + Obj->NumUserOperands,
215+
AllocStart = reinterpret_cast<uint8_t *>(DI) - DI->SizeInBytes;
216+
} else if (NumUserOperands > 0) {
217+
Use *Storage = reinterpret_cast<Use *>(this) - NumUserOperands;
218+
Use::zap(Storage, Storage + NumUserOperands,
216219
/* Delete */ false);
217-
::operator delete(Storage);
220+
AllocStart = Storage;
221+
} else {
222+
// Handle the edge case where there are no operands and no descriptor.
223+
AllocStart = (void **)(this) - 1;
218224
}
225+
226+
// Operator delete needs to know where the allocation started. To avoid
227+
// use-after-destroy, we have to store the allocation start outside the User
228+
// object memory. The `User` new operator always allocates least one pointer
229+
// before the User, so we can use that to store the allocation start. As a
230+
// special case, we avoid this extra prefix allocation for ConstantData
231+
// instances, since those are extremely common.
232+
if (!isa<ConstantData>(this))
233+
((void **)this)[-1] = AllocStart;
219234
}
235+
236+
void User::operator delete(void *Usr) { ::operator delete(((void **)Usr)[-1]); }

0 commit comments

Comments
 (0)