Skip to content

[Bug] ReadBlock block size wraparound bypasses truncated block validation and causes out of bounds access #1310

@Sakura-501

Description

@Sakura-501

ReadBlock block size wraparound bypasses truncated block validation and causes out of bounds access

1. Basic Information

  • Repository: https://github.com/google/leveldb
  • Affected commit: 7ee830d02b623e8ffe0b95d59a74db1e58da04c5
  • Affected version: leveldb 1.23.0 / HEAD@7ee830d02b62
  • Component: generic SSTable block parsing logic
  • Primary sink: table/format.ccReadBlock()
  • Related path: db/dumpfile.ccTable::Open()ReadBlock()
  • Vulnerability type: CWE-190 Integer Overflow / Wraparound, leading to OOB access / DoS

2. Vulnerability Summary

ReadBlock() narrows attacker-controlled BlockHandle.size to size_t and then immediately uses it in n + kBlockTrailerSize. When size = UINT64_MAX - 4, n + 5 wraps to 0 on a 64-bit build, which means:

  1. new char[n + 5] allocates using the wrapped tiny length;
  2. file->Read(..., n + 5, ...) reads using the same wrapped tiny length;
  3. the truncated-block check contents.size() != n + 5 also uses the same wrapped value;
  4. the safety check is therefore bypassed in a self-consistent way;
  5. the code still performs switch (data[n]) using the attacker-influenced huge logical size;
  6. execution reaches an out-of-bounds pointer computation at table/format.cc:102, terminating the process.

This is not limited to test-only code. The bug sits in LevelDB's generic SSTable parsing path, so any program that opens attacker-supplied .ldb/.sst files may hit it.

3. Affected Code

size_t n = static_cast<size_t>(handle.size());
char* buf = new char[n + kBlockTrailerSize];
Slice contents;
Status s = file->Read(handle.offset(), n + kBlockTrailerSize, &contents, buf);
...
if (contents.size() != n + kBlockTrailerSize) {
  delete[] buf;
  return Status::Corruption("truncated block read");
}
...
switch (data[n]) {

4. Technical Details

The core issue is that attacker controlled BlockHandle.size is narrowed to size_t, then used in n + kBlockTrailerSize. The wrapped result is reused for allocation reading and truncated block validation, which bypasses the intended check and eventually reaches an out of bounds access at data[n].

5. Contents of This Submission Bundle

Files in this folder:

  • ReadBlock_Size_Wrap_OOB.md: report
  • poc/exploit.py: minimal PoC generator for the malicious 000125.ldb
  • poc/000125.ldb: pre-generated 48-byte sample payload (can be used directly or regenerated)
  • poc/reproduce.sh: reproduction script that performs clone build and trigger

Sample file details:

  • size: 48 bytes
  • SHA-256: e417635f193420e34e33c8c6e37dbceb67815d7aa28681ee11fa9ee1ddd305ea

Prerequisites

Required tools

Please ensure the target environment has:

  • git
  • cmake
  • a working C/C++ compiler (preferably clang/clang++; gcc/g++ is also fine if sanitizer support is available)
  • python3

Working directory

First enter this submission folder and record its absolute path:

cd /path/to/ReadBlock_Size_Wrap_OOB/submit
SUBMIT_DIR="$(pwd)"

All relative paths below are based on that directory.

6. Reproduction Steps

One click reproduction

Run the following command from this folder:

bash ./poc/reproduce.sh

The script will automatically:

  • regenerate poc/000125.ldb;
  • clone leveldb and check out the affected commit;
  • build leveldbutil with ASAN and UBSAN;
  • trigger the bug and print the run log;
  • verify that the log contains format.cc:102:11, ReadBlock, Table::Open, and DumpFile.

If the script prints [*] reproduction succeeded, the one click reproduction completed successfully.

A non zero exit from the trigger command is expected here because ASAN_OPTIONS=abort_on_error=1 and UBSAN_OPTIONS=halt_on_error=1 intentionally abort the process after the bug is hit. The script therefore checks the run log for the expected stack markers and returns success when they are present.

Manual reproduction

Step 1. Obtain the affected source tree

WORKDIR="$(mktemp -d)"
git clone https://github.com/google/leveldb.git "$WORKDIR/leveldb"
cd "$WORKDIR/leveldb"
git checkout 7ee830d02b623e8ffe0b95d59a74db1e58da04c5

Validation:

git rev-parse HEAD

Expected output:

7ee830d02b623e8ffe0b95d59a74db1e58da04c5

Step 2. Build leveldbutil with ASAN/UBSAN

Building with sanitizers makes the out of bounds access easier to observe at runtime.

If clang/clang++ is available, run:

export CC=clang
export CXX=clang++

Then build:

cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -DLEVELDB_BUILD_TESTS=OFF \
  -DLEVELDB_BUILD_BENCHMARKS=OFF \
  -DCMAKE_C_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \
  -DCMAKE_CXX_FLAGS='-fsanitize=address,undefined -fno-omit-frame-pointer' \
  -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined'
cmake --build build -j2

Validation: the following binary should exist after the build:

ls -l ./build/leveldbutil

Step 3. Return to the submission folder and generate the malicious SSTable

Go back to the submission folder:

cd "$SUBMIT_DIR"

Generate the payload using the included PoC:

python3 ./poc/exploit.py ./poc/000125.ldb
ls -l ./poc/000125.ldb
shasum -a 256 ./poc/000125.ldb

Expected result:

  • the script prints poc/000125.ldb
  • the file size is 48 bytes
  • the SHA-256 is e417635f193420e34e33c8c6e37dbceb67815d7aa28681ee11fa9ee1ddd305ea

Important: the file name must remain in a LevelDB-recognized table-file form such as 000125.ldb. Renaming it to something like poc.ldb will make leveldbutil reject it as unknown file type, preventing execution from reaching the vulnerable path.

Step 4. Trigger the bug

From the submission folder, run:

ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
"$WORKDIR/leveldb/build/leveldbutil" dump ./poc/000125.ldb

Step 5. Verify successful reproduction

A successful trigger should show all of the following:

  1. the report points to table/format.cc:102:11
  2. the output contains runtime error: addition of unsigned offset ... overflowed
  3. the stack includes:
    • leveldb::ReadBlock(...)
    • leveldb::Table::Open(...)
    • leveldb::DumpFile(...)

A representative output from one local verification run is:

.../table/format.cc:102:11: runtime error: addition of unsigned offset ... overflowed ...
    #0 ... in leveldb::ReadBlock(...) format.cc:102
    #1 ... in leveldb::Table::Open(...) table.cc:61
    #2 ... in leveldb::DumpFile(...) dumpfile.cc:225
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior .../table/format.cc:102:11

Expected vs Actual Behavior

  • Expected behavior: the parser should reject an oversized BlockHandle.size before allocation, reading, or validation, and return Corruption (or similar) instead of continuing with a wrapped length.
  • Actual behavior: integer wraparound in n + 5 influences allocation, reading, and truncated-block validation at the same time, bypassing the intended safety check; the code then dereferences data[n] and reaches an out-of-bounds access detected by the sanitizer.

7. Root Cause

This is not merely a missing bounds check. It is a logic bypass caused by integer wraparound:

  1. the attacker fully controls handle.size();
  2. the code narrows uint64_t to size_t and then computes n + kBlockTrailerSize;
  3. the wrapped value is reused for allocation, reading, and contents.size() validation;
  4. those three operations become self consistent around the wrapped value, so the truncation check is bypassed;
  5. the code still uses the huge logical index data[n], which leads to out of bounds access.

8. Suggested Fix

Validate the original uint64_t before any addition, allocation, or read length calculation:

uint64_t raw_size = handle.size();
if (raw_size > std::numeric_limits<size_t>::max() - kBlockTrailerSize) {
  return Status::Corruption("block is too large");
}
size_t n = static_cast<size_t>(raw_size);
size_t read_len = n + kBlockTrailerSize;
char* buf = new char[read_len];
Status s = file->Read(handle.offset(), read_len, &contents, buf);
if (contents.size() != read_len) {
  delete[] buf;
  return Status::Corruption("truncated block read");
}

poc.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions