Skip to content

Commit 08d4dd7

Browse files
committed
Merge pull request godotengine#100694 from Ivorforce/cowdata-destruct-graciously
Destruct `CowData` more graciously by avoiding accidentally exposing a half-destructed buffer.
2 parents b97c8b3 + 25cd923 commit 08d4dd7

File tree

2 files changed

+47
-19
lines changed

2 files changed

+47
-19
lines changed

core/templates/cowdata.h

+22-19
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ class CowData {
162162
return *out;
163163
}
164164

165+
// Decrements the reference count. Deallocates the backing buffer if needed.
166+
// After this function, _ptr is guaranteed to be NULL.
165167
void _unref();
166168
void _ref(const CowData *p_from);
167169
void _ref(const CowData &p_from);
@@ -252,7 +254,7 @@ class CowData {
252254
Size count(const T &p_val) const;
253255

254256
_FORCE_INLINE_ CowData() {}
255-
_FORCE_INLINE_ ~CowData();
257+
_FORCE_INLINE_ ~CowData() { _unref(); }
256258
_FORCE_INLINE_ CowData(std::initializer_list<T> p_init);
257259
_FORCE_INLINE_ CowData(const CowData<T> &p_from) { _ref(p_from); }
258260
_FORCE_INLINE_ CowData(CowData<T> &&p_from) {
@@ -269,22 +271,30 @@ void CowData<T>::_unref() {
269271

270272
SafeNumeric<USize> *refc = _get_refcount();
271273
if (refc->decrement() > 0) {
272-
return; // still in use
274+
// Data is still in use elsewhere.
275+
_ptr = nullptr;
276+
return;
273277
}
274-
// clean up
278+
// Clean up.
279+
// First, invalidate our own reference.
280+
// NOTE: It is required to do so immediately because it must not be observable outside of this
281+
// function after refcount has already been reduced to 0.
282+
// WARNING: It must be done before calling the destructors, because one of them may otherwise
283+
// observe it through a reference to us. In this case, it may try to access the buffer,
284+
// which is illegal after some of the elements in it have already been destructed, and
285+
// may lead to a segmentation fault.
286+
USize current_size = *_get_size();
287+
T *prev_ptr = _ptr;
288+
_ptr = nullptr;
275289

276290
if constexpr (!std::is_trivially_destructible_v<T>) {
277-
USize current_size = *_get_size();
278-
279291
for (USize i = 0; i < current_size; ++i) {
280-
// call destructors
281-
T *t = &_ptr[i];
282-
t->~T();
292+
prev_ptr[i].~T();
283293
}
284294
}
285295

286296
// free mem
287-
Memory::free_static(((uint8_t *)_ptr) - DATA_OFFSET, false);
297+
Memory::free_static((uint8_t *)prev_ptr - DATA_OFFSET, false);
288298
}
289299

290300
template <typename T>
@@ -339,9 +349,8 @@ Error CowData<T>::resize(Size p_size) {
339349
}
340350

341351
if (p_size == 0) {
342-
// wants to clean up
343-
_unref();
344-
_ptr = nullptr;
352+
// Wants to clean up.
353+
_unref(); // Resets _ptr to nullptr.
345354
return OK;
346355
}
347356

@@ -484,8 +493,7 @@ void CowData<T>::_ref(const CowData &p_from) {
484493
return; // self assign, do nothing.
485494
}
486495

487-
_unref();
488-
_ptr = nullptr;
496+
_unref(); // Resets _ptr to nullptr.
489497

490498
if (!p_from._ptr) {
491499
return; //nothing to do
@@ -496,11 +504,6 @@ void CowData<T>::_ref(const CowData &p_from) {
496504
}
497505
}
498506

499-
template <typename T>
500-
CowData<T>::~CowData() {
501-
_unref();
502-
}
503-
504507
template <typename T>
505508
CowData<T>::CowData(std::initializer_list<T> p_init) {
506509
Error err = resize(p_init.size());

tests/core/templates/test_vector.h

+25
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,31 @@ TEST_CASE("[Vector] Operators") {
532532
CHECK(vector != vector_other);
533533
}
534534

535+
struct CyclicVectorHolder {
536+
Vector<CyclicVectorHolder> *vector = nullptr;
537+
bool is_destructing = false;
538+
539+
~CyclicVectorHolder() {
540+
if (is_destructing) {
541+
// The vector must exist and not expose its backing array at this point.
542+
CHECK_NE(vector, nullptr);
543+
CHECK_EQ(vector->ptr(), nullptr);
544+
}
545+
}
546+
};
547+
548+
TEST_CASE("[Vector] Cyclic Reference") {
549+
// Create a stack-space vector.
550+
Vector<CyclicVectorHolder> vector;
551+
// Add a new (empty) element.
552+
vector.resize(1);
553+
// Expose the vector to its element through CyclicVectorHolder.
554+
// This is questionable behavior, but should still behave graciously.
555+
vector.ptrw()[0] = CyclicVectorHolder{ &vector };
556+
vector.ptrw()[0].is_destructing = true;
557+
// The vector goes out of scope and destructs, calling CyclicVectorHolder's destructor.
558+
}
559+
535560
} // namespace TestVector
536561

537562
#endif // TEST_VECTOR_H

0 commit comments

Comments
 (0)