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 + 5 also 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: 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:
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:
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:
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:
- 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: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:
- the attacker fully controls
handle.size();
- the code narrows
uint64_t to size_t and then computes n + 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");
}
poc.zip
ReadBlock block size wraparound bypasses truncated block validation and causes out of bounds access
1. Basic Information
7ee830d02b623e8ffe0b95d59a74db1e58da04c57ee830d02b62table/format.cc→ReadBlock()db/dumpfile.cc→Table::Open()→ReadBlock()2. Vulnerability Summary
ReadBlock()narrows attacker-controlledBlockHandle.sizetosize_tand then immediately uses it inn + kBlockTrailerSize. Whensize = UINT64_MAX - 4,n + 5wraps to0on 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;contents.size() != n + 5also uses the same wrapped value;switch (data[n])using the attacker-influenced huge logical size;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/.sstfiles may hit it.3. Affected Code
4. Technical Details
The core issue is that attacker controlled
BlockHandle.sizeis narrowed tosize_t, then used inn + 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 atdata[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 triggerSample file details:
48bytese417635f193420e34e33c8c6e37dbceb67815d7aa28681ee11fa9ee1ddd305eaPrerequisites
Required tools
Please ensure the target environment has:
gitcmakeclang/clang++;gcc/g++is also fine if sanitizer support is available)python3Working directory
First enter this submission folder and record its absolute path:
All relative paths below are based on that directory.
6. Reproduction Steps
One click reproduction
Run the following command from this folder:
The script will automatically:
poc/000125.ldb;leveldband check out the affected commit;leveldbutilwith ASAN and UBSAN;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=1andUBSAN_OPTIONS=halt_on_error=1intentionally 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
Validation:
Expected output:
Step 2. Build
leveldbutilwith ASAN/UBSANIf
clang/clang++is available, run:Then build:
Validation: the following binary should exist after the build:
Step 3. Return to the submission folder and generate the malicious SSTable
Go back to the submission folder:
Generate the payload using the included PoC:
Expected result:
poc/000125.ldb48bytese417635f193420e34e33c8c6e37dbceb67815d7aa28681ee11fa9ee1ddd305eaStep 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:
table/format.cc:102:11runtime error: addition of unsigned offset ... overflowedleveldb::ReadBlock(...)leveldb::Table::Open(...)leveldb::DumpFile(...)A representative output from one local verification run is:
Expected vs Actual Behavior
BlockHandle.sizebefore allocation, reading, or validation, and returnCorruption(or similar) instead of continuing with a wrapped length.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:
handle.size();uint64_ttosize_tand then computesn + kBlockTrailerSize;contents.size()validation;data[n], which leads to out of bounds access.8. Suggested Fix
Validate the original
uint64_tbefore any addition, allocation, or read length calculation:poc.zip