Skip to content

Commit 3b03448

Browse files
jameseh96daverigby
authored andcommitted
MB-42688: Do not check vbucket state on each comparator call
The vbucket comparator returned by PagingVisitor::getVBucketComparator() must meet the requirements of a comparator for std::sort ( https://en.cppreference.com/w/cpp/named_req/Compare ) The comparator must not be affected by changes to the vbucket while sorting, otherwise mid-way through std::sort it may begin giving inconsistent results. The comparator previously cached the pageable mem usage to avoid this, but _did not_ read the vbucket state ahead of time. As a result, a vbucket state change part way through the call to std::sort made by VBCBAdaptor could lead to a crash on some platforms. Fixed by checking the vbucket state once ahead of time. Change-Id: I633f7e002c8f2b79d79a24d907e44b3569da8a21 Reviewed-on: http://review.couchbase.org/c/kv_engine/+/140126 Tested-by: Build Bot <[email protected]> Well-Formed: Build Bot <[email protected]> Reviewed-by: Dave Rigby <[email protected]>
1 parent 69730e6 commit 3b03448

File tree

2 files changed

+125
-11
lines changed

2 files changed

+125
-11
lines changed

engines/ep/src/item_pager.cc

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -314,28 +314,28 @@ PagingVisitor::PagingVisitor(KVBucket& s,
314314

315315
std::function<bool(const VBucket::id_type&, const VBucket::id_type&)>
316316
PagingVisitor::getVBucketComparator() const {
317-
// Get the pageable mem used of each vb _once_ and cache it.
317+
// Get the pageable mem used and state of each vb _once_ and cache it.
318318
// Fetching these values repeatedly in the comparator could cause issues
319319
// as the values can change _during_ a given sort call.
320320

321-
std::map<VBucket::id_type, size_t> pageableMemUsed;
321+
std::map<VBucket::id_type, std::pair<bool, size_t>>
322+
stateAndPageableMemUsed;
322323

323324
for (const auto& vbid : store.getVBuckets().getBuckets()) {
324325
auto vb = store.getVBucket(vbid);
325-
pageableMemUsed[vbid] = vb ? vb->getPageableMemUsage() : 0;
326+
if (vb) {
327+
stateAndPageableMemUsed[vbid] = {
328+
vb->getState() == vbucket_state_replica,
329+
vb->getPageableMemUsage()};
330+
}
326331
}
327332

328333
// Capture initializers are C++14
329-
return [pageableMemUsed, this](const VBucket::id_type& a,
330-
const VBucket::id_type& b) mutable {
331-
auto vbA = store.getVBucket(a);
332-
auto vbB = store.getVBucket(b);
333-
bool aReplica = vbA && vbA->getState() == vbucket_state_replica;
334-
bool bReplica = vbB && vbB->getState() == vbucket_state_replica;
334+
return [stateAndPageableMemUsed](const VBucket::id_type& a,
335+
const VBucket::id_type& b) mutable {
335336
// sort replicas before all other vbucket states, then sort by
336337
// pageableMemUsed
337-
return std::make_pair(aReplica, pageableMemUsed[a]) >
338-
std::make_pair(bReplica, pageableMemUsed[b]);
338+
return stateAndPageableMemUsed[a] > stateAndPageableMemUsed[b];
339339
};
340340
}
341341

engines/ep/tests/module_tests/item_pager_test.cc

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
#include <xattr/blob.h>
3737
#include <xattr/utils.h>
3838

39+
#include <numeric>
40+
3941
/**
4042
* Test fixture for bucket quota tests. Sets quota (max_size) to 200KB and
4143
* enables the MemoryTracker.
@@ -1106,6 +1108,118 @@ TEST_P(STItemPagerTest, ItemPagerEvictionOrder) {
11061108
;
11071109
}
11081110

1111+
TEST_P(STItemPagerTest, ItemPagerEvictionOrderIsSafe) {
1112+
// MB-42688: Test that the ordering of the paging visitor comparator is
1113+
// fixed even if the amount of pageable memory or the vbucket state changes.
1114+
// This is required to meet the strict weak ordering requirement of
1115+
// std::sort
1116+
1117+
using Vbid = VBucket::id_type;
1118+
1119+
// 17 is the minimum number of vbs found to demonstrate a segfault
1120+
// with an unsuitable comparator on linux. This is likely impl dependent.
1121+
// 18 gives 6 vbs per active/replica/pending
1122+
std::vector<Vbid> allVBs(18);
1123+
1124+
std::iota(allVBs.begin(), allVBs.end(), 0);
1125+
1126+
for (int i = 0; i < 6; i++) {
1127+
setVBucketStateAndRunPersistTask(i, vbucket_state_active);
1128+
setVBucketStateAndRunPersistTask(i + 6, vbucket_state_pending);
1129+
setVBucketStateAndRunPersistTask(i + 12, vbucket_state_replica);
1130+
}
1131+
1132+
if (engine->getConfiguration().getBucketType() == "persistent") {
1133+
// flush all vbs
1134+
for (const auto& vbid : allVBs) {
1135+
getEPBucket().flushVBucket(vbid);
1136+
}
1137+
}
1138+
1139+
auto& stats = engine->getEpStats();
1140+
auto available = std::make_shared<std::atomic<bool>>();
1141+
auto& config = engine->getConfiguration();
1142+
1143+
std::atomic<item_pager_phase> phase{ACTIVE_AND_PENDING_ONLY};
1144+
bool isEphemeral = std::get<0>(GetParam()) == "ephemeral";
1145+
1146+
auto pv = std::make_unique<MockPagingVisitor>(
1147+
*store,
1148+
stats,
1149+
1.0,
1150+
available,
1151+
ITEM_PAGER,
1152+
false,
1153+
1.0,
1154+
VBucketFilter(allVBs),
1155+
&phase,
1156+
isEphemeral,
1157+
config.getItemEvictionAgePercentage(),
1158+
config.getItemEvictionFreqCounterAgeThreshold(),
1159+
PagingVisitor::EvictionPolicy::hifi_mfu);
1160+
1161+
// now test that even with state changes, the comparator sorts the vbuckets
1162+
// acceptably
1163+
auto innerComparator = pv->getVBucketComparator();
1164+
1165+
// wrap the comparator to allow insertion of state changes mid-sort
1166+
auto comparator = [&innerComparator, this](const Vbid& a, const Vbid& b) {
1167+
// run the actual comparator
1168+
auto res = innerComparator(a, b);
1169+
// now, to _intentionally_ try to break the strict weak ordering, change
1170+
// the state of one of the vbuckets. If the vbucket comparator is
1171+
// checking the state each time, this can cause a crash in std::sort
1172+
for (const auto& vbid : {a, b}) {
1173+
auto state = store->getVBucket(vbid)->getState();
1174+
EXPECT_EQ(ENGINE_SUCCESS,
1175+
store->setVBucketState(vbid,
1176+
state == vbucket_state_replica
1177+
? vbucket_state_active
1178+
: vbucket_state_replica,
1179+
/*transfer*/ false));
1180+
}
1181+
1182+
return res;
1183+
};
1184+
1185+
// if the vbucket comparator is "safe" and checks the state once, this
1186+
// sort will work as expected - otherwise it _may_ segfault.
1187+
// Note: this is not a robust test, variations in the impl of sort
1188+
// may mean this does _not_ crash even with an unacceptable comparator
1189+
std::sort(allVBs.begin(), allVBs.end(), comparator);
1190+
1191+
// as a secondary check, directly test the requirements of a strict
1192+
// weak ordering on the comparator while actively changing
1193+
// the state of vbuckets. This will fail if the comparator checks
1194+
// the bucket state on every call.
1195+
1196+
// irreflexivity
1197+
for (const auto& x : allVBs) {
1198+
EXPECT_FALSE(comparator(x, x));
1199+
}
1200+
1201+
// asymmetry
1202+
for (const auto& x : allVBs) {
1203+
for (const auto& y : allVBs) {
1204+
EXPECT_FALSE(comparator(x, y) && comparator(y, x));
1205+
}
1206+
}
1207+
1208+
// transitivity
1209+
for (const auto& x : allVBs) {
1210+
for (const auto& y : allVBs) {
1211+
for (const auto& z : allVBs) {
1212+
// x < y && y < z => x < z
1213+
// equivalent to
1214+
// !(x < y && y < z) || x < z
1215+
// if x < y and y < z, it must be true that x < z
1216+
EXPECT_TRUE(!(comparator(x, y) && comparator(y, z)) ||
1217+
comparator(x, z));
1218+
}
1219+
}
1220+
}
1221+
}
1222+
11091223
/// Subclass for expiry tests only applicable to persistent buckets.
11101224
class STPersistentExpiryPagerTest : public STExpiryPagerTest {};
11111225

0 commit comments

Comments
 (0)