#15209·rocksdb

【BUG】In the Block::Block (located in block_based/block.cc) file, the calculation of restart_offset_ results in an integer overflow for RocksDB-v6.26.1

Author: wkxNiubiCreated Sep 14, 2026Updated Sep 14, 2026

Problem Description

In the kDataBlockBinaryAndHash path of Block::Block, restart_offset_ is calculated as map_offset - num_restarts_ * sizeof(uint32_t). Here, num_restarts_ is of type uint32_t, and the maximum value it can take according to data_block_footer.cc is 0x7FFFFFFF; map_offset is a uint16_t parsed from the attacker's bytes by DataBlockHashIndex::Initialize. The 64-bit product is truncated when assigned to the uint32_t member restart_offset_, so any num_restarts_ that is less than or equal to map_offset modulo 2^32 can bypass the wrap-around check. When num_restarts_ = 2^30 (footer 0x40000000; the 31-bit num_restarts field contains bit 30, which is a valid value), the product is exactly 2^32, and restart_offset_ is equal to map_offset. The check "restart_offset_ > map_offset" does not hold, and the block is accepted. Subsequently, Block claims to have a restart array starting internally in a small buffer, containing 2^30 four-byte entries; during DB opening, compaction, iteration, and ingestion of SST data blocks provided by the attacker, BlockIter::GetRestartPoint and BinarySeek/ParseNextKey resolve the dereference of data_ + restarts_ + index*4 each time Seek/Next/Prev is performed, with the maximum out-of-bounds access being approximately 4GB. The upstream subsequently fixed this defect by setting the upper limit of num_restarts to 2^28 - 1 and rejecting footer retention bits (DataBlockFooter), and described the previous behavior as "num_restarts is multiplied by 4, resulting in overflow, and the check is silently ignored."

How to reproduce

An attacker with local access conditions constructs a malicious SST file and sends it to the target process. The latter opens/reopens the file through SstFileReader::Open, SstFileDumper::GetTableReader, or DB (VersionSet::Recover, RepairDB, rocksdb_open), or by using ExternalSstFileIngestionJob::GetIngestedFileInfo or ImportColumnFamilyJob::GetIngestedFileInfo to ingest it as an external SST file. Most of these entry points do not require authentication; since the attacker controls all the bytes of the SST, the table-level checksum and format check can also be satisfied by the attacker themselves. The attacker sets the last 4 bytes of the target block footer to 0x40000000 (BinarySearch path) or to 0xC0000000 and attaches the minimum legal hash index prefix (BinaryAndHash path), making num_restarts_ = 2^30. The call chain is SstFileReader::Open → BlockBasedTable::Open → BlockBasedTable::ReadMetaIndexBlock → BlockBasedTable::RetrieveBlock → Block::Block (table/block_based/block.cc:997): The table reader reads the footer and metaindex, RetrieveBlock reads in the original block bytes and constructs Block, and the 64-bit product of num_restarts_sizeof(uint32_t) in Block::Block is truncated to uint32_t when stored in restart_offset_. When num_restarts_ = 2^30, the product is exactly 2^32, with the lower 32 bits being 0, and restart_offset_ equals map_offset (BinarySearch path is equal to size_-4), and the condition "restart_offset_ > map_offset" does not hold, so the malicious block is silently accepted. Subsequently, any Seek/SeekToFirst/Next/Prev that causes the iterator to enter the restart array will decode data_ + restarts_ + index4: BinarySeek first probes the index ≈ 2^29, reads approximately 2GB beyond the small block buffer area, and the maximum out-of-bounds access can reach approximately 4GB; a 40-byte block can trigger it, potentially causing the process to crash or memory content leakage due to out-of-bounds reading. Out-of-bounds reading occurs only when the iterator enters the restart array, not only when constructing the Block.

Content of the code evidence

cpp
      case BlockBasedTableOptions::kDataBlockBinaryAndHash:
        if (size_ < sizeof(uint32_t) /* block footer */ +
                        sizeof(uint16_t) /* NUM_BUCK */) {
          size_ = 0;
          break;
        }

        uint16_t map_offset;
        data_block_hash_index_.Initialize(
            contents.data.data(),
            static_cast<uint16_t>(contents.data.size() -
                                  sizeof(uint32_t)), /*chop off
                                                 NUM_RESTARTS*/
            &map_offset);

        restart_offset_ = map_offset - num_restarts_ * sizeof(uint32_t);

        if (restart_offset_ > map_offset) {
          // map_offset is too small for NumRestarts() and
          // therefore restart_offset_ wrapped around.
          size_ = 0;
          break;
        }