Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/tasks/batch_write_task.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,16 @@ KvError BatchWriteTask::WriteLargeValue(const WriteDataEntry &entry)
std::get_if<IoStringBuffer>(&entry.large_val_);
iosb != nullptr)
{
// The on-disk large-value header encodes the length in 31 bits (bit 31
// is the has-metadata flag), so a value longer than
// kLargeValueLengthMask (2 GiB - 1) cannot be represented: casting to
// uint32 would truncate it and/or collide with the metadata flag,
// corrupting the blob so every later read fails. EloqStore does not
// support values this large; reject.
if (iosb->Size() > kLargeValueLengthMask)
{
return KvError::InvalidArgs;
}
const auto &fragments = iosb->Fragments();
const uint32_t num_segments = fragments.size();
std::vector<const char *> ptrs(num_segments);
Expand All @@ -1504,6 +1514,12 @@ KvError BatchWriteTask::WriteLargeValue(const WriteDataEntry &entry)
const size_t size = pinned->second;
assert(base != nullptr);
assert(size > 0);
// See the IoStringBuffer path above: values longer than
// kLargeValueLengthMask (2 GiB - 1) cannot be encoded on disk.
if (size > kLargeValueLengthMask)
{
return KvError::InvalidArgs;
}
const uint32_t num_segments =
static_cast<uint32_t>((size + seg_size - 1) / seg_size);

Expand Down
30 changes: 30 additions & 0 deletions tests/large_value_concurrency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -978,3 +978,33 @@ TEST_CASE(
store->Stop();
CleanupStore(opts);
}

// A large value whose length exceeds
// kLargeValueLengthMask (2 GiB - 1) cannot be represented in the 31-bit
// on-disk length field (bit 31 is the has-metadata flag). WriteLargeValue must
// reject it up front instead of casting the length to uint32 -- which would
// truncate it and/or collide with the metadata flag, corrupting the blob so
// every later read fails. The guard runs before any memory is dereferenced, so
// a huge declared length over a small real pinned chunk exercises it without
// allocating gigabytes.
TEST_CASE("pinned large value over the 2 GiB length cap is rejected",
"[large-value-concurrency][pinned]")
{
PinnedMultiShardHarness h(kSegmentSize, /*num_shards=*/1);
eloqstore::KvOptions opts =
MakePinnedOpts(h, /*pinned_tail_scratch_slots=*/2);
eloqstore::EloqStore *store = InitStore(opts);

eloqstore::TableIdent tbl{"lv-cap", 0};
// 2 GiB == kLargeValueHasMetadataBit: one byte past the 31-bit length cap.
const size_t too_large = size_t{1} << 31;
std::pair<char *, size_t> dst{h.Base(0), too_large};

eloqstore::BatchWriteRequest req;
AsyncPinnedWrite(store, req, tbl, "big", dst, /*metadata=*/"", /*ts=*/1);
req.Wait();
REQUIRE(req.Error() == eloqstore::KvError::InvalidArgs);

store->Stop();
CleanupStore(opts);
}
Loading