Skip to content

Commit aa2f43e

Browse files
committed
MB-34346: Allow pruning of compressed xattrs
The API which prunes documents of non-system xattrs can trigger an exception when the incoming value is compressed. The expects that the final (pruned) value will fit into the input buffer, if not an exception occurs. This exception can be made to trigger when the incoming buffer contains a snappy compressed value. The code decompresses the value and prunes the xattrs, then fails because in some cases the decompressed and pruned value is larger than the buffer it arrived in. This is made safe by changing the API so that we don't re-use the input buffer for output data, instead a new buffer is returned, which is empty except in the case when a modified/pruned value is to be returned. Change-Id: Icd18e632aba8178aac70843d41010e76ef659908 Reviewed-on: http://review.couchbase.org/109721 Tested-by: Build Bot <[email protected]> Reviewed-by: Dave Rigby <[email protected]> Well-Formed: Build Bot <[email protected]>
1 parent 5e9fca9 commit aa2f43e

File tree

7 files changed

+80
-63
lines changed

7 files changed

+80
-63
lines changed

daemon/doc_pre_expiry.cc

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@
2323

2424
#include <gsl/gsl>
2525

26-
bool document_pre_expiry(item_info& itm_info) {
26+
std::string document_pre_expiry(const item_info& itm_info) {
2727
if (!mcbp::datatype::is_xattr(itm_info.datatype)) {
2828
// The object does not contain any XATTRs so we should remove
2929
// the entire content
30-
return false;
30+
return {};
3131
}
3232

3333
cb::char_buffer payload{static_cast<char*>(itm_info.value[0].iov_base),
@@ -36,32 +36,11 @@ bool document_pre_expiry(item_info& itm_info) {
3636
cb::xattr::Blob blob(payload, mcbp::datatype::is_snappy(itm_info.datatype));
3737
blob.prune_user_keys();
3838
auto pruned = blob.finalize();
39-
if (pruned.len == 0) {
39+
if (pruned.size() == 0) {
4040
// The old payload only contained user xattrs and
4141
// we removed everything
42-
return false;
42+
return {};
4343
}
4444

45-
// Pruning the user keys should just repack the data internally
46-
// without any allocations, but to be on the safe side we should
47-
// just check to verify, and if it happened to have done any
48-
// reallocations we could copy the data into our buffer if it fits.
49-
// (or we could throw an exception here, but I'd hate to get that
50-
// 2AM call if we can avoid that with a simple fallback check)
51-
if (pruned.buf != payload.buf) {
52-
if (pruned.len > payload.len) {
53-
throw std::logic_error("pre_expiry_document: the pruned object "
54-
"won't fit!");
55-
}
56-
57-
std::copy(pruned.buf, pruned.buf + pruned.len, payload.buf);
58-
}
59-
60-
// Update the length field of the item
61-
itm_info.nbytes = gsl::narrow<uint32_t>(pruned.len);
62-
itm_info.value[0].iov_len = pruned.len;
63-
// Clear all other datatype flags (we've stripped off everything)
64-
itm_info.datatype = PROTOCOL_BINARY_DATATYPE_XATTR;
65-
66-
return true;
45+
return {pruned.data(), pruned.size()};
6746
}

daemon/doc_pre_expiry.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@
2323
* Implementation of the pre_expiry hook in the `SERVER_DOCUMENT_API`. Check
2424
* the server API documentation for more information.
2525
*/
26-
bool document_pre_expiry(item_info& itm_info);
26+
std::string document_pre_expiry(const item_info& itm_info);

engines/ep/src/kv_bucket.cc

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -542,11 +542,12 @@ void KVBucket::runPreExpiryHook(VBucket& vb, Item& it) {
542542
it.decompressValue(); // A no-op for already decompressed items
543543
auto info =
544544
it.toItemInfo(vb.failovers->getLatestUUID(), vb.getHLCEpochSeqno());
545-
if (engine.getServerApi()->document->pre_expiry(info)) {
546-
// The payload is modified and contains data we should use
547-
it.replaceValue(Blob::New(static_cast<char*>(info.value[0].iov_base),
548-
info.value[0].iov_len));
549-
it.setDataType(info.datatype);
545+
auto result = engine.getServerApi()->document->pre_expiry(info);
546+
if (!result.empty()) {
547+
// A modified value was returned, use it
548+
it.replaceValue(Blob::New(result.data(), result.size()));
549+
// The API states only uncompressed xattr values are returned
550+
it.setDataType(PROTOCOL_BINARY_DATATYPE_XATTR);
550551
} else {
551552
// Make the document empty and raw
552553
it.replaceValue(Blob::New(0));

engines/ep/src/vbucket.cc

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -480,21 +480,19 @@ void VBucket::handlePreExpiry(const std::unique_lock<std::mutex>& hbl,
480480
EventuallyPersistentEngine* engine = ObjectRegistry::getCurrentEngine();
481481
itm_info =
482482
itm->toItemInfo(failovers->getLatestUUID(), getHLCEpochSeqno());
483-
value_t new_val(Blob::Copy(*value));
484-
itm->replaceValue(new_val.get());
485-
itm->setDataType(v.getDatatype());
486483

487484
SERVER_HANDLE_V1* sapi = engine->getServerApi();
488485
/* TODO: In order to minimize allocations, the callback needs to
489486
* allocate an item whose value size will be exactly the size of the
490487
* value after pre-expiry is performed.
491488
*/
492-
if (sapi->document->pre_expiry(itm_info)) {
489+
auto result = sapi->document->pre_expiry(itm_info);
490+
if (!result.empty()) {
493491
Item new_item(v.getKey(),
494492
v.getFlags(),
495493
v.getExptime(),
496-
itm_info.value[0].iov_base,
497-
itm_info.value[0].iov_len,
494+
result.data(),
495+
result.size(),
498496
itm_info.datatype,
499497
v.getCas(),
500498
v.getBySeqno(),

engines/ep/tests/module_tests/kv_bucket_test.cc

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,50 @@ TEST_P(KVBucketParamTest, MB31495_GetRandomKey) {
12521252
EXPECT_EQ(ENGINE_SUCCESS, gv.getStatus());
12531253
}
12541254

1255+
// Test that expiring a compressed xattr doesn't trigger any errors
1256+
TEST_P(KVBucketParamTest, MB_34346) {
1257+
// Create an XTTR value with only a large system xattr, and compress the lot
1258+
// Note the large xattr should be highly compressible to make it easier to
1259+
// trigger the MB.
1260+
cb::xattr::Blob blob;
1261+
blob.set("_sync",
1262+
R"({"fffff":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})");
1263+
auto xattr = blob.finalize();
1264+
cb::compression::Buffer output;
1265+
cb::compression::deflate(cb::compression::Algorithm::Snappy,
1266+
{xattr.data(), xattr.size()},
1267+
output);
1268+
EXPECT_LT(output.size(), xattr.size())
1269+
<< "Expected the compressed buffer to be smaller than the input";
1270+
1271+
auto key = makeStoredDocKey("key_1");
1272+
store_item(
1273+
vbid,
1274+
key,
1275+
{output.data(), output.size()},
1276+
ep_abs_time(ep_current_time() + 10),
1277+
{cb::engine_errc::success},
1278+
PROTOCOL_BINARY_DATATYPE_XATTR | PROTOCOL_BINARY_DATATYPE_SNAPPY);
1279+
1280+
flushVBucketToDiskIfPersistent(vbid, 1);
1281+
1282+
EXPECT_EQ(1, engine->getVBucket(vbid)->getNumItems())
1283+
<< "Should have 1 item after calling store()";
1284+
1285+
TimeTraveller docBrown(15);
1286+
1287+
get_options_t options = static_cast<get_options_t>(
1288+
QUEUE_BG_FETCH | HONOR_STATES | TRACK_REFERENCE | DELETE_TEMP |
1289+
HIDE_LOCKED_CAS | TRACK_STATISTICS | GET_DELETED_VALUE);
1290+
GetValue gv2 = store->get(key, vbid, cookie, options);
1291+
EXPECT_TRUE(gv2.item->isDeleted());
1292+
1293+
flushVBucketToDiskIfPersistent(vbid, 1);
1294+
1295+
EXPECT_EQ(0, engine->getVBucket(vbid)->getNumItems())
1296+
<< "Should still have 0 items after time-travelling/expiry";
1297+
}
1298+
12551299
class StoreIfTest : public KVBucketTest {
12561300
public:
12571301
void SetUp() override {

include/memcached/server_api.h

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -317,17 +317,17 @@ struct SERVER_DOCUMENT_API {
317317

318318
/**
319319
* This callback is called from the underlying engine right before
320-
* a particular document expires. The callback is responsible for
321-
* modifying the contents of the itm_info passed in. The updated
322-
* total size is available in itm_info.nbytes.
320+
* a particular document expires. The callback is responsible examining
321+
* the value and possibly returning a new and modified value.
323322
*
324323
* @param itm_info info pertaining to the item that is to be expired.
325-
* @return true indicating that the info has been modified in itm_info.
326-
* false indicating that there is no data available in itm_info.
324+
* @return std::string empty if the value required no modification, not
325+
* empty then the string contains the modified value. When not empty
326+
* the datatype of the new value is datatype xattr only.
327+
*
327328
* @throws std::bad_alloc in case of memory allocation failure
328-
* @throws std::logic_error if the data has grown
329329
*/
330-
bool (*pre_expiry)(item_info& itm_info);
330+
std::string (*pre_expiry)(const item_info& itm_info);
331331
};
332332

333333
#ifdef WIN32

tests/doc_server_api/doc_pre_expiry_test.cc

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
TEST(PreExpiry, EmptyDocument) {
2626
item_info info{};
27-
EXPECT_FALSE(document_pre_expiry(info));
27+
EXPECT_TRUE(document_pre_expiry(info).empty());
2828
}
2929

3030
TEST(PreExpiry, DocumentWithoutXattr) {
@@ -33,8 +33,8 @@ TEST(PreExpiry, DocumentWithoutXattr) {
3333
info.value[0].iov_base = static_cast<void*>(blob);
3434
info.value[0].iov_len = sizeof(blob);
3535
info.nbytes = sizeof(blob);
36-
EXPECT_FALSE(document_pre_expiry(info));
37-
EXPECT_EQ(PROTOCOL_BINARY_RAW_BYTES, info.datatype);
36+
auto rv = document_pre_expiry(info);
37+
EXPECT_TRUE(rv.empty());
3838
}
3939

4040
TEST(PreExpiry, DocumentWithUserXAttrOnly) {
@@ -46,8 +46,8 @@ TEST(PreExpiry, DocumentWithUserXAttrOnly) {
4646
info.value[0].iov_len = body.size();
4747
info.nbytes = gsl::narrow<uint32_t>(body.size());
4848
info.datatype = PROTOCOL_BINARY_DATATYPE_XATTR;
49-
EXPECT_FALSE(document_pre_expiry(info));
50-
EXPECT_EQ(PROTOCOL_BINARY_DATATYPE_XATTR, info.datatype);
49+
auto rv = document_pre_expiry(info);
50+
EXPECT_TRUE(rv.empty());
5151
}
5252

5353
TEST(PreExpiry, DocumentWithSystemXattrOnly) {
@@ -59,8 +59,8 @@ TEST(PreExpiry, DocumentWithSystemXattrOnly) {
5959
info.value[0].iov_len = body.size();
6060
info.nbytes = gsl::narrow<uint32_t>(body.size());
6161
info.datatype = PROTOCOL_BINARY_DATATYPE_XATTR;
62-
EXPECT_TRUE(document_pre_expiry(info));
63-
EXPECT_EQ(PROTOCOL_BINARY_DATATYPE_XATTR, info.datatype);
62+
auto rv = document_pre_expiry(info);
63+
EXPECT_FALSE(rv.empty());
6464
}
6565

6666
TEST(PreExpiry, DocumentWithUserAndSystemXattr) {
@@ -73,11 +73,9 @@ TEST(PreExpiry, DocumentWithUserAndSystemXattr) {
7373
info.value[0].iov_len = body.size();
7474
info.nbytes = gsl::narrow<uint32_t>(body.size());
7575
info.datatype = PROTOCOL_BINARY_DATATYPE_XATTR;
76-
EXPECT_TRUE(document_pre_expiry(info));
77-
EXPECT_EQ(PROTOCOL_BINARY_DATATYPE_XATTR, info.datatype);
78-
79-
// The body should have been modified (shrinked by stripping off user attr)
80-
EXPECT_GT(body.size(), info.datatype);
76+
auto rv = document_pre_expiry(info);
77+
EXPECT_FALSE(rv.empty());
78+
EXPECT_LT(rv.size(), body.size());
8179
}
8280

8381
TEST(PreExpiry, DocumentWithJsonBodyAndXattrs) {
@@ -95,10 +93,7 @@ TEST(PreExpiry, DocumentWithJsonBodyAndXattrs) {
9593
info.nbytes = gsl::narrow<uint32_t>(body.size());
9694
info.datatype =
9795
PROTOCOL_BINARY_DATATYPE_XATTR | PROTOCOL_BINARY_DATATYPE_JSON;
98-
EXPECT_TRUE(document_pre_expiry(info));
99-
// The JSON datatype should be stripped off
100-
EXPECT_EQ(PROTOCOL_BINARY_DATATYPE_XATTR, info.datatype);
101-
102-
// The body should have been modified (shrinked by stripping off user attr)
103-
EXPECT_GT(body.size(), info.datatype);
96+
auto rv = document_pre_expiry(info);
97+
EXPECT_FALSE(rv.empty());
98+
EXPECT_LT(rv.size(), body.size());
10499
}

0 commit comments

Comments
 (0)