[Bug] ReadBlock block size wraparound bypasses truncated block validation and causes out of bounds access
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.cc→ReadBlock() - Related path:
db/dumpfile.cc→Table::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:
new char[n + 5]allocates using the wrapped tiny length;file->Read(..., n + 5, ...)reads using the same wrapped tiny length;- the truncated-block check
contents.size() != n + 5also uses the same wrapped value; - the safety check is therefore bypassed in a self-consistent way;
- the code still performs
switch (data[n])using the attacker-influenced huge logical size; - 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: reportpoc/exploit.py: minimal PoC generator for the malicious000125.ldbpoc/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:
48bytes - SHA-256:
e417635f193420e34e33c8c6e37dbceb67815d7aa28681ee11fa9ee1ddd305ea
Prerequisites
Required tools
Please ensure the target environment has:
gitcmake- 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.shThe script will automatically:
- regenerate
poc/000125.ldb; - clone
leveldband check out the affected commit; - build
leveldbutilwith ASAN and UBSAN; - trigger the bug and print the run log;
- verify that the log contains
format.cc:102:11,ReadBlock,Table::Open, andDumpFile.
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 7ee830d02b623e8ffe0b95d59a74db1e58da04c5Validation:
git rev-parse HEADExpected output:
7ee830d02b623e8ffe0b95d59a74db1e58da04c5Step 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 -j2Validation: the following binary should exist after the build:
ls -l ./build/leveldbutilStep 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.ldbExpected result:
- the script prints
poc/000125.ldb - the file size is
48bytes - 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 likepoc.ldbwill makeleveldbutilreject it asunknown 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.ldbStep 5. Verify successful reproduction
A successful trigger should show all of the following:
- the report points to
table/format.cc:102:11 - the output contains
runtime error: addition of unsigned offset ... overflowed - 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:11Expected vs Actual Behavior
- Expected behavior: the parser should reject an oversized
BlockHandle.sizebefore allocation, reading, or validation, and returnCorruption(or similar) instead of continuing with a wrapped length. - Actual behavior: integer wraparound in
n + 5influences allocation, reading, and truncated-block validation at the same time, bypassing the intended safety check; the code then dereferencesdata[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:
- the attacker fully controls
handle.size(); - the code narrows
uint64_ttosize_tand then computesn + kBlockTrailerSize; - the wrapped value is reused for allocation, reading, and
contents.size()validation; - those three operations become self consistent around the wrapped value, so the truncation check is bypassed;
- 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");
}Source: google/leveldb