Fix possible corner cases in qarraydataops.h

Updated moveNonPod function to behave correctly under
exceptions being thrown. This is the version that was implemented
at some point but then got changed prior to merge. Added tests for
moveInGrowthDirection (which uses moveNonPod in general case) to
verify that range movements are correctly done

Updated QCommonArrayOps access modifier from private to protected
to allow testing of internal stuff by subclassing

Task-number: QTBUG-84320
Change-Id: Idb994a72ee601762e32248670cdc7819aaca0088
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
Andrei Golubev 2020-08-31 11:58:53 +02:00
parent a81859a3c8
commit 7a15d71def
2 changed files with 228 additions and 13 deletions

View File

@ -1039,7 +1039,7 @@ struct QCommonArrayOps : QArrayOpsSelector<T>::Type
using iterator = typename Base::iterator;
using const_iterator = typename Base::const_iterator;
private:
protected:
using Self = QCommonArrayOps<T>;
// Tag dispatched helper functions
@ -1116,6 +1116,11 @@ private:
--start;
}
// re-created the range. now there is an initialized memory region
// somewhere in the allocated area. if something goes wrong, we must
// clean it up, so "freeze" the position for now (cannot commit yet)
destroyer.freeze();
// step 2. move assign over existing elements in the overlapping
// region (if there's an overlap)
while (e != begin) {
@ -1124,18 +1129,15 @@ private:
*start = std::move_if_noexcept(*e);
}
// re-created the range. now there is an initialized memory region
// somewhere in the allocated area. if something goes wrong, we must
// clean it up, so "freeze" the position for now (cannot commit yet)
destroyer.freeze();
// step 3. destroy elements in the old range
const qsizetype originalSize = this_->size;
start = oldRangeEnd; // mind the possible gap, have to re-assign
while (start != begin) {
start = begin; // delete elements in reverse order to prevent any gaps
while (start != oldRangeEnd) {
// Exceptions or not, dtor called once per instance
if constexpr (std::is_same_v<std::decay_t<GrowthTag>, GrowsForwardTag>)
++this_->ptr;
--this_->size;
(--start)->~T();
(start++)->~T();
}
destroyer.commit();

View File

@ -39,6 +39,7 @@
#include <vector>
#include <stdexcept>
#include <functional>
#include <memory>
// A wrapper for a test function. Calls a function, if it fails, reports failure
#define RUN_TEST_FUNC(test, ...) \
@ -88,6 +89,8 @@ private slots:
void exceptionSafetyPrimitives_destructor();
void exceptionSafetyPrimitives_mover();
void exceptionSafetyPrimitives_displacer();
void moveNonPod_data();
void moveNonPod();
#endif
};
@ -2207,7 +2210,9 @@ ThrowingTypeWatcher& throwingTypeWatcher() { static ThrowingTypeWatcher global;
struct ThrowingType
{
static unsigned int throwOnce;
static unsigned int throwOnceInDtor;
static constexpr char throwString[] = "Requested to throw";
static constexpr char throwStringDtor[] = "Requested to throw in dtor";
void checkThrow() {
// deferred throw
if (throwOnce > 0) {
@ -2220,27 +2225,36 @@ struct ThrowingType
}
int id = 0;
ThrowingType(int val = 0) : id(val)
ThrowingType(int val = 0) noexcept(false) : id(val)
{
checkThrow();
}
ThrowingType(const ThrowingType &other) : id(other.id)
ThrowingType(const ThrowingType &other) noexcept(false) : id(other.id)
{
checkThrow();
}
ThrowingType& operator=(const ThrowingType &other)
ThrowingType& operator=(const ThrowingType &other) noexcept(false)
{
id = other.id;
checkThrow();
return *this;
}
~ThrowingType()
~ThrowingType() noexcept(false)
{
throwingTypeWatcher().destroyed(id); // notify global watcher
id = -1;
// deferred throw
if (throwOnceInDtor > 0) {
--throwOnceInDtor;
if (throwOnceInDtor == 0) {
throw std::runtime_error(throwStringDtor);
}
}
}
};
unsigned int ThrowingType::throwOnce = 0;
unsigned int ThrowingType::throwOnceInDtor = 0;
bool operator==(const ThrowingType &a, const ThrowingType &b) {
return a.id == b.id;
}
@ -2815,6 +2829,205 @@ void tst_QArrayData::exceptionSafetyPrimitives_displacer()
}
}
}
struct GenericThrowingType
{
std::shared_ptr<int> data = std::shared_ptr<int>(new int(42)); // helper for double free
ThrowingType throwingData = ThrowingType(0);
GenericThrowingType(int id = 0) : throwingData(id)
{
QVERIFY(data.use_count() > 0);
}
~GenericThrowingType()
{
// if we're in dtor but use_count is 0, it's double free
QVERIFY(data.use_count() > 0);
}
enum MoveCase {
MoveRightNoOverlap = 0,
MoveRightOverlap = 1,
MoveLeftNoOverlap = 2,
MoveLeftOverlap = 3,
};
enum ThrowCase {
ThrowInDtor = 0,
ThrowInUninitializedRegion = 1,
ThrowInOverlapRegion = 2,
};
};
struct PublicGenericMoveOps : QtPrivate::QCommonArrayOps<GenericThrowingType>
{
template<typename GrowthTag>
void public_moveInGrowthDirection(GrowthTag tag, qsizetype futureGrowth)
{
static_assert(!QTypeInfo<GenericThrowingType>::isRelocatable);
using MoveOps = QtPrivate::QCommonArrayOps<GenericThrowingType>::GenericMoveOps;
MoveOps::moveInGrowthDirection(tag, this, futureGrowth);
}
};
void tst_QArrayData::moveNonPod_data()
{
QTest::addColumn<GenericThrowingType::MoveCase>("moveCase");
QTest::addColumn<GenericThrowingType::ThrowCase>("throwCase");
// Throwing in dtor
QTest::newRow("throw-in-dtor-move-right-no-overlap")
<< GenericThrowingType::MoveRightNoOverlap << GenericThrowingType::ThrowInDtor;
QTest::newRow("throw-in-dtor-move-right-overlap")
<< GenericThrowingType::MoveRightOverlap << GenericThrowingType::ThrowInDtor;
QTest::newRow("throw-in-dtor-move-left-no-overlap")
<< GenericThrowingType::MoveLeftNoOverlap << GenericThrowingType::ThrowInDtor;
QTest::newRow("throw-in-dtor-move-left-overlap")
<< GenericThrowingType::MoveLeftOverlap << GenericThrowingType::ThrowInDtor;
// Throwing in uninitialized region
QTest::newRow("throw-in-uninit-region-move-right-no-overlap")
<< GenericThrowingType::MoveRightNoOverlap
<< GenericThrowingType::ThrowInUninitializedRegion;
QTest::newRow("throw-in-uninit-region-move-right-overlap")
<< GenericThrowingType::MoveRightOverlap << GenericThrowingType::ThrowInUninitializedRegion;
QTest::newRow("throw-in-uninit-region-move-left-no-overlap")
<< GenericThrowingType::MoveLeftNoOverlap
<< GenericThrowingType::ThrowInUninitializedRegion;
QTest::newRow("throw-in-uninit-region-move-left-overlap")
<< GenericThrowingType::MoveLeftOverlap << GenericThrowingType::ThrowInUninitializedRegion;
// Throwing in overlap region
QTest::newRow("throw-in-overlap-region-move-right-overlap")
<< GenericThrowingType::MoveRightOverlap << GenericThrowingType::ThrowInOverlapRegion;
QTest::newRow("throw-in-overlap-region-move-left-overlap")
<< GenericThrowingType::MoveLeftOverlap << GenericThrowingType::ThrowInOverlapRegion;
}
void tst_QArrayData::moveNonPod()
{
// Assume that non-throwing moves perform correctly. Otherwise, all previous
// tests would've failed. Test only what happens when exceptions are thrown.
QFETCH(GenericThrowingType::MoveCase, moveCase);
QFETCH(GenericThrowingType::ThrowCase, throwCase);
struct WatcherScope
{
WatcherScope() { throwingTypeWatcher().watch = true; }
~WatcherScope()
{
throwingTypeWatcher().watch = false;
throwingTypeWatcher().destroyedIds.clear();
}
};
const auto cast = [] (auto &dataPointer) {
return static_cast<PublicGenericMoveOps*>(std::addressof(dataPointer));
};
const auto setThrowingFlag = [throwCase] () {
switch (throwCase) {
case GenericThrowingType::ThrowInDtor: ThrowingType::throwOnceInDtor = 2; break;
case GenericThrowingType::ThrowInUninitializedRegion: ThrowingType::throwOnce = 2; break;
case GenericThrowingType::ThrowInOverlapRegion: ThrowingType::throwOnce = 3; break;
default: QFAIL("Unknown throwCase");
}
};
const auto checkExceptionText = [throwCase] (const char *what) {
if (throwCase == GenericThrowingType::ThrowInDtor) {
QCOMPARE(std::string(what), ThrowingType::throwStringDtor);
} else {
QCOMPARE(std::string(what), ThrowingType::throwString);
}
};
const auto checkNoMemoryLeaks = [throwCase] (size_t extraDestroyedElements = 0) {
const size_t destroyedElementsCount = throwingTypeWatcher().destroyedIds.size();
switch (throwCase) {
case GenericThrowingType::ThrowInDtor:
// 2 elements from uinitialized region + 2 elements from old range + extra if no overlap
QCOMPARE(destroyedElementsCount, 2u + 2u + extraDestroyedElements);
break;
case GenericThrowingType::ThrowInUninitializedRegion:
// always 1 element from uninitialized region
QCOMPARE(destroyedElementsCount, 1u);
break;
case GenericThrowingType::ThrowInOverlapRegion:
// always 2 elements from uninitialized region
QCOMPARE(destroyedElementsCount, 2u);
break;
default: QFAIL("Unknown throwCase");
}
};
switch (moveCase) {
case GenericThrowingType::MoveRightNoOverlap : { // moving right without overlap
auto storage = createDataPointer<GenericThrowingType>(20, 3);
QVERIFY(storage.freeSpaceAtEnd() > 3);
WatcherScope scope; Q_UNUSED(scope);
try {
setThrowingFlag();
cast(storage)->public_moveInGrowthDirection(QtPrivate::GrowsForwardTag{}, 4);
QFAIL("Unreachable line!");
} catch (const std::runtime_error &e) {
checkExceptionText(e.what());
}
checkNoMemoryLeaks(1);
break;
}
case GenericThrowingType::MoveRightOverlap: { // moving right with overlap
auto storage = createDataPointer<GenericThrowingType>(20, 3);
QVERIFY(storage.freeSpaceAtEnd() > 3);
WatcherScope scope; Q_UNUSED(scope);
try {
setThrowingFlag();
cast(storage)->public_moveInGrowthDirection(QtPrivate::GrowsForwardTag{}, 2);
QFAIL("Unreachable line!");
} catch (const std::runtime_error &e) {
checkExceptionText(e.what());
}
checkNoMemoryLeaks();
break;
}
case GenericThrowingType::MoveLeftNoOverlap: { // moving left without overlap
auto storage = createDataPointer<GenericThrowingType>(20, 2);
storage->insert(storage.begin(), 1, GenericThrowingType(42));
QVERIFY(storage.freeSpaceAtBegin() > 3);
WatcherScope scope; Q_UNUSED(scope);
try {
setThrowingFlag();
cast(storage)->public_moveInGrowthDirection(QtPrivate::GrowsBackwardsTag{}, 4);
QFAIL("Unreachable line!");
} catch (const std::runtime_error &e) {
checkExceptionText(e.what());
}
checkNoMemoryLeaks(1);
break;
}
case GenericThrowingType::MoveLeftOverlap: {
auto storage = createDataPointer<GenericThrowingType>(20, 2);
storage->insert(storage.begin(), 1, GenericThrowingType(42));
QVERIFY(storage.freeSpaceAtBegin() > 3);
WatcherScope scope; Q_UNUSED(scope);
try {
setThrowingFlag();
cast(storage)->public_moveInGrowthDirection(QtPrivate::GrowsBackwardsTag{}, 2);
QFAIL("Unreachable line!");
} catch (const std::runtime_error &e) {
checkExceptionText(e.what());
}
checkNoMemoryLeaks();
break;
}
default: QFAIL("Unknown moveCase");
};
}
#endif // QT_NO_EXCEPTIONS
QTEST_APPLESS_MAIN(tst_QArrayData)