Skip to content

Commit ad47f53

Browse files
committed
MB-51373: Inspect and correct Item objects created by KVStore
MB-52793 exposed a bug in the handing of deletes which have a body (for System XATTRS). The root cause of that bug has been addressed under that bug, however the problem remains that /previous/ versions of KV-Engine could have written invalid deleted documents to disk, which could be encountered after an (offline) upgrade. Create a function that Couch/Magma-KVStore should call when they have created an Item from the underlying stored data. The function inspects the Item for datatype anomalies and if found logs and corrects the Item preventing exceptions occurring further up the stack. In this case we check for an Item with no value, but a datatype, which in the case of datatype=xattr can cause faults in xattr inspection code. Also adds a regression test which verifies that the sanitiztion of such items is correctly triggered when reading documents from disk in the various ways we access them. Change-Id: I235af07a1973c4af35301e17223e624a2cb5acf0 Reviewed-on: https://review.couchbase.org/c/kv_engine/+/177217 Reviewed-by: Trond Norbye <[email protected]> Reviewed-by: Jim Walker <[email protected]> Well-Formed: Restriction Checker Tested-by: Build Bot <[email protected]>
1 parent 8855aeb commit ad47f53

File tree

6 files changed

+202
-9
lines changed

6 files changed

+202
-9
lines changed

engines/ep/src/couch-kvstore/couch-kvstore.cc

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -138,18 +138,27 @@ static sized_buf to_sized_buf(const DiskDocKey& key) {
138138

139139
/**
140140
* Helper function to create an Item from couchstore DocInfo & related types.
141+
* @param vbid The vbucket the Item belongs to
142+
* @param docinfo The id, db_seq and rev_seq are used in the Item construction
143+
* @param metadata The metadata to use to create the Item
144+
* @param value an optional value, this allows "key-only" paths to be
145+
* distinguishable from a value of 0 length.
141146
*/
142147
static std::unique_ptr<Item> makeItemFromDocInfo(Vbid vbid,
143148
const DocInfo& docinfo,
144149
const MetaData& metadata,
145-
sized_buf value) {
150+
boost::optional<sized_buf> value) {
151+
152+
value_t body;
153+
if (value) {
154+
body.reset(Blob::New(value->buf, value->size));
155+
}
146156
// Strip off the DurabilityPrepare namespace (if present) from the persisted
147157
// dockey before we create the in-memory item.
148158
auto item = std::make_unique<Item>(makeDiskDocKey(docinfo.id).getDocKey(),
149159
metadata.getFlags(),
150160
metadata.getExptime(),
151-
value.buf,
152-
value.size,
161+
body,
153162
metadata.getDataType(),
154163
metadata.getCas(),
155164
docinfo.db_seq,
@@ -159,6 +168,8 @@ static std::unique_ptr<Item> makeItemFromDocInfo(Vbid vbid,
159168
item->setDeleted(metadata.getDeleteSource());
160169
}
161170

171+
KVStore::checkAndFixKVStoreCreatedItem(*item);
172+
162173
if (metadata.getVersionInitialisedFrom() == MetaData::Version::V3) {
163174
// Metadata is from a SyncWrite - update the Item appropriately.
164175
switch (metadata.getDurabilityOp()) {
@@ -843,7 +854,7 @@ static int notify_expired_item(DocInfo& info,
843854
sized_buf item,
844855
compaction_ctx& ctx,
845856
time_t currtime) {
846-
sized_buf data{nullptr, 0};
857+
boost::optional<sized_buf> data;
847858
cb::compression::Buffer inflated;
848859

849860
if (mcbp::datatype::is_xattr(metadata.getDataType())) {
@@ -870,7 +881,7 @@ static int notify_expired_item(DocInfo& info,
870881
// Now remove snappy bit
871882
metadata.setDataType(metadata.getDataType() &
872883
~PROTOCOL_BINARY_DATATYPE_SNAPPY);
873-
data = {inflated.data(), inflated.size()};
884+
data = sized_buf{inflated.data(), inflated.size()};
874885
}
875886
}
876887

@@ -1952,9 +1963,12 @@ couchstore_error_t CouchKVStore::fetchDoc(Db* db,
19521963
return COUCHSTORE_ERROR_DB_NO_LONGER_VALID;
19531964
}
19541965

1955-
if (metaOnly == GetMetaOnly::Yes) {
1966+
const bool forceValueFetch = isDocumentPotentiallyCorruptedByMB52793(
1967+
docinfo->deleted, *metadata);
1968+
if (metaOnly == GetMetaOnly::Yes && !forceValueFetch) {
1969+
// Can skip reading document value.
19561970
auto it = makeItemFromDocInfo(
1957-
vbId, *docinfo, *metadata, {nullptr, docinfo->size});
1971+
vbId, *docinfo, *metadata, boost::none);
19581972

19591973
docValue = GetValue(std::move(it));
19601974
// update ep-engine IO stats
@@ -2015,7 +2029,6 @@ int CouchKVStore::recordDbDump(Db *db, DocInfo *docinfo, void *ctx) {
20152029
auto* cl = sctx->lookup.get();
20162030

20172031
Doc *doc = nullptr;
2018-
sized_buf value{nullptr, 0};
20192032
uint64_t byseqno = docinfo->db_seq;
20202033
Vbid vbucketId = sctx->vbid;
20212034

@@ -2053,7 +2066,10 @@ int CouchKVStore::recordDbDump(Db *db, DocInfo *docinfo, void *ctx) {
20532066

20542067
auto metadata = MetaDataFactory::createMetaData(docinfo->rev_meta);
20552068

2056-
if (sctx->valFilter != ValueFilter::KEYS_ONLY) {
2069+
boost::optional<sized_buf> value;
2070+
const bool forceValueFetch = isDocumentPotentiallyCorruptedByMB52793(
2071+
docinfo->deleted, *metadata);
2072+
if (sctx->valFilter != ValueFilter::KEYS_ONLY || forceValueFetch) {
20572073
couchstore_open_options openOptions = 0;
20582074

20592075
/**

engines/ep/src/kvstore.cc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include <platform/dirutils.h>
4444
#include <sys/types.h>
4545
#include <sys/stat.h>
46+
#include <utilities/logtags.h>
4647

4748
ScanContext::ScanContext(
4849
std::shared_ptr<StatusCallback<GetValue>> cb,
@@ -713,3 +714,45 @@ IORequest::~IORequest() = default;
713714
bool IORequest::isDelete() const {
714715
return item->isDeleted() && !item->isPending();
715716
}
717+
718+
bool KVStore::isDocumentPotentiallyCorruptedByMB52793(
719+
bool deleted, const MetaData& metadata) {
720+
// As per MB-52793, Deleted documents with a zero length value can
721+
// incorrectly end up with the datatype set to XATTR (when it should be
722+
// RAW_BYTES), if it was previously a deleted document /with/ a value of
723+
// system XATTRs.
724+
// To be able to fixup such documents we need to know more than just the
725+
// metadata, as the value size is stored as part of the value. As such;
726+
// return true if it meets all the criteria which metadata informs us of.
727+
return deleted && metadata.getDataType() == PROTOCOL_BINARY_DATATYPE_XATTR;
728+
}
729+
730+
// MB-51373: Fix the datatype of invalid documents. Currently checks for
731+
// datatype ! raw but no value, this invalid document has been seen in
732+
// production deployments and reading them can lead to a restart
733+
bool KVStore::checkAndFixKVStoreCreatedItem(Item& item) {
734+
#if CB_DEVELOPMENT_ASSERTS
735+
if (item.isDeleted() &&
736+
item.getDataType() == PROTOCOL_BINARY_DATATYPE_XATTR) {
737+
// If we encounter a potential invalid doc (MB-51373) - Delete with
738+
// datatype XATTR, we should have its value to be able to verify it
739+
// is correct, or otherwise sanitise it.
740+
Expects(item.getValue());
741+
}
742+
#endif
743+
if (item.isDeleted() &&
744+
item.getValue() &&
745+
item.getNBytes() == 0 &&
746+
item.getDataType() != PROTOCOL_BINARY_RAW_BYTES) {
747+
std::stringstream ss;
748+
ss << item;
749+
EP_LOG_WARN(
750+
"KVStore::checkAndFixKVStoreCreatedItem: {} correcting invalid "
751+
"datatype {}",
752+
item.getVBucketId(),
753+
cb::UserDataView(ss.str()).getSanitizedValue());
754+
item.setDataType(PROTOCOL_BINARY_RAW_BYTES);
755+
return true;
756+
}
757+
return false;
758+
}

engines/ep/src/kvstore.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class DiskDocKey;
4141
class Item;
4242
class KVStore;
4343
class KVStoreConfig;
44+
class MetaData;
4445
class PersistenceCallback;
4546
class RollbackCB;
4647
class RollbackResult;
@@ -926,6 +927,28 @@ class KVStore {
926927
postFlushHook = hook;
927928
}
928929

930+
/**
931+
* Check if the specified document metadata is /potentially/ affected
932+
* by a datatype corruption issue (MB-52793) - a deleted document with
933+
* zero length value has an incorrect datatype.
934+
*
935+
* @return True if the document is /potentially/ affected and hence further
936+
* analysis is needed (such as fetching the document body for
937+
* additional checks).
938+
*/
939+
static bool isDocumentPotentiallyCorruptedByMB52793(
940+
bool deleted, const MetaData& metadata);
941+
942+
/**
943+
* Function inspects the Item for some known issues that may exist in
944+
* persisted data (possibly from older releases and now present due to
945+
* upgrade). If an inconsistency is found it will log a fix the Item.
946+
*
947+
* @param item [in/out] the Item to check and if needed, fix.
948+
* @return true if the Item was changed by the function because of an issue
949+
*/
950+
static bool checkAndFixKVStoreCreatedItem(Item& item);
951+
929952
protected:
930953
/**
931954
* Prepare for delete of the vbucket file - Implementation specific method

engines/ep/src/magma-kvstore/magma-kvstore.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1267,6 +1267,10 @@ std::unique_ptr<Item> MagmaKVStore::makeItem(Vbid vb,
12671267
vb,
12681268
meta.revSeqno);
12691269

1270+
if (filter != ValueFilter::KEYS_ONLY) {
1271+
checkAndFixKVStoreCreatedItem(*item);
1272+
}
1273+
12701274
if (meta.deleted) {
12711275
item->setDeleted(static_cast<DeleteSource>(meta.deleteSource));
12721276
}

engines/ep/tests/module_tests/evp_store_single_threaded_test.cc

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5996,3 +5996,100 @@ TEST_P(STParamPersistentBucketTest,
59965996
EXPECT_EQ(PROTOCOL_BINARY_RAW_BYTES, v->getDatatype());
59975997
}
59985998
}
5999+
6000+
TEST_P(STParamPersistentBucketTest,
6001+
SanitizeOnDiskDeletedDocWithIncorrectXATTRFull) {
6002+
testSanitizeOnDiskDeletedDocWithIncorrectXATTR(false);
6003+
}
6004+
6005+
TEST_P(STParamPersistentBucketTest,
6006+
SanitizeOnDiskDeletedDocWithIncorrectXATTRMetaOnly) {
6007+
testSanitizeOnDiskDeletedDocWithIncorrectXATTR(true);
6008+
}
6009+
6010+
void STParamPersistentBucketTest::
6011+
testSanitizeOnDiskDeletedDocWithIncorrectXATTR(bool fetchMetaOnly) {
6012+
setVBucketStateAndRunPersistTask(vbid, vbucket_state_active);
6013+
6014+
const auto key = makeStoredDocKey("key");
6015+
6016+
// Store an initial item for us to rollback to (see end of test).
6017+
const auto initalItem = store_item(vbid, key, "value");
6018+
flushVBucketToDiskIfPersistent(vbid, 1);
6019+
6020+
// Construct a document on-disk which is Deleted, zero value,
6021+
// with datatype=XATTR.
6022+
setVBucketStateAndRunPersistTask(vbid, vbucket_state_replica);
6023+
auto item = make_item(vbid, key, {}, 0, PROTOCOL_BINARY_DATATYPE_XATTR);
6024+
item.setCas(1234);
6025+
item.setDeleted(DeleteSource::Explicit);
6026+
6027+
uint64_t seqno;
6028+
ASSERT_EQ(ENGINE_SUCCESS,
6029+
store->setWithMeta(item,
6030+
0 /* cas */,
6031+
&seqno,
6032+
cookie,
6033+
{vbucket_state_replica},
6034+
CheckConflicts::No,
6035+
/*allowExisting*/ true));
6036+
flushVBucketToDiskIfPersistent(vbid, 1);
6037+
6038+
// Re-fetch from disk to confirm it is correctly sanitized.
6039+
// Need to be active to be able to fetch from store APIs.
6040+
setVBucketStateAndRunPersistTask(vbid, vbucket_state_active);
6041+
6042+
auto fetchDocAndCheck = [&]() {
6043+
if (fetchMetaOnly) {
6044+
ItemMetaData metadata;
6045+
uint32_t deleted;
6046+
uint8_t datatype;
6047+
ASSERT_EQ(ENGINE_EWOULDBLOCK,
6048+
store->getMetaData(key,
6049+
vbid,
6050+
cookie,
6051+
metadata,
6052+
deleted,
6053+
datatype));
6054+
runBGFetcherTask();
6055+
ASSERT_EQ(ENGINE_SUCCESS,
6056+
store->getMetaData(key,
6057+
vbid,
6058+
cookie,
6059+
metadata,
6060+
deleted,
6061+
datatype));
6062+
EXPECT_TRUE(deleted);
6063+
EXPECT_EQ(PROTOCOL_BINARY_RAW_BYTES, datatype);
6064+
} else {
6065+
// Fetch entire document and check sanitized.
6066+
get_options_t options = static_cast<get_options_t>(
6067+
QUEUE_BG_FETCH | HONOR_STATES | TRACK_REFERENCE |
6068+
DELETE_TEMP | HIDE_LOCKED_CAS | TRACK_STATISTICS |
6069+
GET_DELETED_VALUE);
6070+
auto gv = store->get(key, vbid, cookie, options);
6071+
ASSERT_EQ(ENGINE_EWOULDBLOCK, gv.getStatus());
6072+
6073+
runBGFetcherTask();
6074+
gv = store->get(key, vbid, cookie, GET_DELETED_VALUE);
6075+
EXPECT_EQ(ENGINE_SUCCESS, gv.getStatus());
6076+
EXPECT_EQ(PROTOCOL_BINARY_RAW_BYTES,
6077+
gv.item->getDataType());
6078+
}
6079+
};
6080+
fetchDocAndCheck();
6081+
6082+
// Restart and warmup, checking that the invalid document does not cause
6083+
// issues at warmup.
6084+
resetEngineAndWarmup();
6085+
fetchDocAndCheck();
6086+
6087+
// Finally trigger a rollback of the problematic document, and
6088+
// confirm the rollback is successful (it must perform a KVStore scan
6089+
// which covers the affected document.
6090+
setVBucketStateAndRunPersistTask(vbid, vbucket_state_replica);
6091+
ASSERT_EQ(TaskStatus::Complete,
6092+
store->rollback(vbid, initalItem.getBySeqno()));
6093+
6094+
6095+
}

engines/ep/tests/module_tests/evp_store_single_threaded_test.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,14 @@ class STParamPersistentBucketTest : public STParameterizedBucketTest {
399399
void testCompactionPersistedDeletes(bool dropDeletes);
400400

401401
void testFailoverTableEntryPersistedAtWarmup(std::function<void()>);
402+
403+
/**
404+
* Test for MB-51373 - if we end up with a deleted document on-disk with
405+
* an empty value but datatype=XATTR (when it should be RAW_BYTES - bug
406+
* MB-52793), then we should sanitize that value when it is loaded from
407+
* disk.
408+
* @param fetchMetaOnly If true then when fetching corrupted doc from disk,
409+
* only fetch metadata, else fetch entire document.
410+
*/
411+
void testSanitizeOnDiskDeletedDocWithIncorrectXATTR(bool fetchMetaOnly);
402412
};

0 commit comments

Comments
 (0)