[Bug] Manifest bad internal key causes DB Open process crash
Manifest bad internal key causes DB Open process crash
1. Basic Information
- Repository: https://github.com/google/leveldb
- Affected commit:
7ee830d02b623e8ffe0b95d59a74db1e58da04c5 - Affected version: leveldb 1.23.0 / HEAD@
7ee830d02b62 - Component: MANIFEST and VersionSet recovery path
- Primary sink:
db/version_edit.ccanddb/version_set.cc - Related path:
VersionEdit::DecodeFrom()->VersionSet::Builder::Apply()->VersionSet::Recover()->DB::Open() - Vulnerability type: CWE 191 Integer Underflow leading to process crash
2. Vulnerability Summary
VersionEdit::DecodeFrom() accepts malformed MANIFEST smallest and largest values as long as they are non empty. A 1 byte string is therefore accepted as an InternalKey even though a valid InternalKey must contain at least 8 trailing bytes for sequence and type.
Later, when recovery inserts these forged keys into the VersionSet::Builder set, InternalKeyComparator::Compare() calls ExtractUserKey() and performs internal_key.size() - 8. For a 1 byte key this underflows, turns into a huge size, and eventually reaches memcmp, where AddressSanitizer reports negative-size-param and aborts the process.
The result is a crash during DB::Open() when a target opens a crafted LevelDB directory.
3. Affected Code
static bool GetInternalKey(Slice* input, InternalKey* dst) {
Slice str;
if (GetLengthPrefixedSlice(input, &str)) {
return dst->DecodeFrom(str);
} else {
return false;
}
}bool DecodeFrom(const Slice& s) {
rep_.assign(s.data(), s.size());
return !rep_.empty();
}
inline Slice ExtractUserKey(const Slice& internal_key) {
assert(internal_key.size() >= 8);
return Slice(internal_key.data(), internal_key.size() - 8);
}4. Technical Details
The malformed MANIFEST record reaches VersionSet::Recover() through normal recovery logic. Because GetInternalKey() and InternalKey::DecodeFrom() only reject empty values, a 1 byte key is accepted and stored in file metadata. Once recovery compares two such file metadata entries, the comparator reaches ExtractUserKey() and underflows on size() - 8, which turns a malformed key into a memory unsafe comparison size and crashes the process.
5. Files
Files in this folder:
Manifest_Bad_InternalKey_DoS.md: reportpoc/generate_payloads.cc: payload generator sourcepoc/open_db.cc: minimalDB::Open()trigger sourcepoc/reproduce.sh: reproduction script
6. Reproduction Steps
Prerequisites
Required tools
Please ensure the environment has:
gitcmake- a working C and C++ compiler
Working directory
Enter this folder first:
cd /path/to/Manifest_Bad_InternalKey_DoS/submitOne click reproduction
Run:
bash ./poc/reproduce.shThe script will:
- clone
leveldband check out the affected commit; - build a sanitized
libleveldb.a; - build the payload generator and the
DB::Open()trigger; - generate a crafted LevelDB directory containing a malformed
MANIFEST-000001; - run the trigger and verify that the crash log contains
negative-size-param,VersionSet::Builder::Apply,VersionSet::Recover, andDB::Open.
If the script prints [*] reproduction succeeded, the issue was reproduced.
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 7ee830d02b623e8ffe0b95d59a74db1e58da04c5Step 2. Build sanitized leveldb
If clang and clang++ are 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 -fno-sanitize=vptr' \
-DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address,undefined'
cmake --build build -j2Step 3. Build the helper binaries
From the submit/ directory:
cd /path/to/Manifest_Bad_InternalKey_DoS/submit
"$CXX" -std=c++17 \
-fsanitize=address,undefined \
-fno-sanitize=vptr \
-fno-omit-frame-pointer \
-fno-exceptions \
-fno-rtti \
-I"$WORKDIR/leveldb/include" \
-I"$WORKDIR/leveldb" \
-I"$WORKDIR/leveldb/build/include" \
./poc/generate_payloads.cc "$WORKDIR/leveldb/build/libleveldb.a" -lpthread -o ./poc/gen_payloads
"$CXX" -std=c++17 \
-fsanitize=address,undefined \
-fno-sanitize=vptr \
-fno-omit-frame-pointer \
-fno-exceptions \
-fno-rtti \
-I"$WORKDIR/leveldb/include" \
-I"$WORKDIR/leveldb" \
-I"$WORKDIR/leveldb/build/include" \
./poc/open_db.cc "$WORKDIR/leveldb/build/libleveldb.a" -lpthread -o ./poc/open_dbStep 4. Generate the malicious database directory
./poc/gen_payloads ./workdirStep 5. Trigger the vulnerability
ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 \
UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 \
./poc/open_db ./workdir/manifest_bad_internal_key_dbThe output should contain:
ERROR: AddressSanitizer: negative-size-paramVersionSet::Builder::ApplyVersionSet::RecoverDB::Open
A local verification run produced the following key output:
[*] trigger exit code: 134
[*] non zero exit is expected here because ASAN aborts after the bug is triggered
==93267==ERROR: AddressSanitizer: negative-size-param: (size=-7)
#9 ... in leveldb::VersionSet::Builder::Apply(...)
#10 ... in leveldb::VersionSet::Recover(bool*)
#12 ... in leveldb::DB::Open(...)
SUMMARY: AddressSanitizer: negative-size-param ...
[*] reproduction succeededExpected vs Actual Behavior
- Expected behavior: opening a malformed database directory should return
Corruptionor another regular parse error. - Actual behavior: a malformed MANIFEST reaches the recovery comparator and crashes the process during
DB::Open().
7. Root Cause
The issue is not just missing validation. Recovery accepts malformed InternalKeys that are shorter than 8 bytes, and later comparator logic assumes that every InternalKey already satisfies that invariant. The assumption breaks in release builds, so the subtraction in ExtractUserKey() underflows and feeds an invalid size into the comparator.
8. Suggested Fix
Reject malformed InternalKeys during MANIFEST decoding and add an explicit guard before subtracting 8 bytes:
static bool GetInternalKey(Slice* input, InternalKey* dst) {
Slice str;
ParsedInternalKey parsed;
if (!GetLengthPrefixedSlice(input, &str)) return false;
if (!ParseInternalKey(str, &parsed)) return false;
dst->SetFrom(parsed);
return true;
}
inline Slice ExtractUserKey(const Slice& internal_key) {
if (internal_key.size() < 8) return Slice();
return Slice(internal_key.data(), internal_key.size() - 8);
}Source: google/leveldb