【BUG】Heap buffer allocation failure and out-of-bounds write caused by uint32_t overflow in BlockPrefixIndex::Builder::Finish function for RocksDB-v6.26.1
Problem Description
The entry_index and num_blocks are obtained from the bytes of the kHashIndexMetadata block in the SST file, encoded using 5-byte varint format, and are completely controlled by the attacker. When BlockPrefixIndex::Create reads these two values, it only uses the size of the prefixes block to verify the prefix_size; since pos + prefix_size is calculated using 64-bit operations, this check does not wrap around. The entry_index and num_blocks themselves have no range validation. In Builder::Finish, num_blocks_per_bucket[bucket] += current->num_blocks is accumulated in uint32_t, and if it exceeds 2^32, it wraps around; total_block_array_entries is calculated based on the wrapped value, and line 120's new uint32_t[total_block_array_entries] is therefore allocated with an overly small buffer. The fill loop writes each record's original current->num_blocks times, with the write value being a decreasing sequence selected by the attacker (current->end_block - iter), and last_block traverses from the buffer's tail all the way down to the start of the allocated area. This insecure data flow can be reached solely based on untrusted SST bytes, and will be executed during database opening, importing, or checking: index_type=kHashSearch decodes from the file's own attributes, rep->table_prefix_extractor is created from the file's prefix_extractor_name attribute , HashIndexReader::Create directly sends these two attacker-controlled blocks to Create. The security consequence is heap memory corruption: the database is almost certain to crash (DoS) when opened, and a bounded but write values completely set by the attacker linear out-of-bounds write primitive is formed, which can overwrite the heap memory in front of the buffer.
How to reproduce
The attacker creates an SST file and tricks the target into opening, importing, or inspecting it. The methods include IngestExternalFile, restoring from backup, or having the target directly open the database directory containing the malicious file. This file declares in its attributes that index_type=kHashSearch and the prefix_extractor_name can be parsed as a valid transformation; the metadata block writes kHashIndexPrefixes and kHashIndexPrefixesMetadata, and the checksum is calculated by the attacker themselves. The block carries the constructed 5-byte varint. When num_buckets=3, two 1-byte prefixes are likely to collide into the same bucket. Record 1 takes entry_index=0 and num_blocks=0x80000000; Record 2 takes entry_index=0x80000005 and num_blocks=0x80000002. The two records are 6 index items apart (more than 1), so it is processed in a chained manner rather than merged. In Finish, 0x80000000 + 0x80000002 is converted to 2 by wrapping it as uint32_t, and only 3 uint32_t are allocated accordingly; the filling loop writes 0x80000002 + 0x80000000 entries with the original value, crossing the allocation area starting point from the end of the buffer, resulting in heap out-of-bounds write. After the target opens the file through SstFileReader::Open or rocksdb_open, the chain of calls passes through BlockBasedTable::Open, PrefetchIndexAndFilterBlocks, CreateIndexReader, HashIndexReader::Create, BlockPrefixIndex::Create, and finally lands at the defect point of Builder::Finish, without requiring authentication throughout. The result is memory corruption when the database is opened and almost certain crash, constituting a denial of service.
Content of the code evidence
num_blocks_per_bucket[bucket] += current->num_blocks;
}
// Calculate the block array buffer size
uint32_t total_block_array_entries = 0;
for (uint32_t i = 0; i < num_buckets; i++) {
uint32_t num_blocks = num_blocks_per_bucket[i];
if (num_blocks > 1) {
total_block_array_entries += (num_blocks + 1);
}
}
// Populate the final prefix block index
uint32_t* block_array_buffer = new uint32_t[total_block_array_entries];
uint32_t* buckets = new uint32_t[num_buckets];
uint32_t offset = 0;
for (uint32_t i = 0; i < num_buckets; i++) {
uint32_t num_blocks = num_blocks_per_bucket[i];
if (num_blocks == 0) {
assert(prefixes_per_bucket[i] == nullptr);
buckets[i] = kNoneBlock;
} else if (num_blocks == 1) {
assert(prefixes_per_bucket[i] != nullptr);
assert(prefixes_per_bucket[i]->next == nullptr);
buckets[i] = prefixes_per_bucket[i]->start_block;
} else {
assert(total_block_array_entries > 0);
assert(prefixes_per_bucket[i] != nullptr);
buckets[i] = EncodeIndex(offset);
block_array_buffer[offset] = num_blocks;
uint32_t* last_block = &block_array_buffer[offset + num_blocks];
auto current = prefixes_per_bucket[i];
// populate block ids from largest to smallest
while (current != nullptr) {
for (uint32_t iter = 0; iter < current->num_blocks; iter++) {
*last_block = current->end_block - iter;
last_block--;
}Source: facebook/rocksdb