Bug: RepairDB pushes tables with empty smallest/largest InternalKeys causing assertion failure in Encode
Summary
Repairer::ScanTable tracks whether any valid key was found via an empty flag but checks only the iterator status before pushing the table into tables_. When a corrupted SST opens with OK status but every key fails ParseInternalKey, the table is pushed with default-constructed (empty) smallest/largest InternalKeys; WriteDescriptor later calls InternalKey::Encode(), which asserts !rep_.empty() and aborts.
Version
$ git describe --tags
1.23-91-g7ee830dDescription
ScanTable in db/repair.cc scans each table to extract smallest/largest:
// db/repair.cc:249-290 (excerpt)
bool empty = true;
ParsedInternalKey parsed;
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
Slice key = iter->key();
if (!ParseInternalKey(key, &parsed)) {
continue; // corrupted key skipped; empty stays true
}
counter++;
if (empty) {
empty = false;
t.meta.smallest.DecodeFrom(key);
}
t.meta.largest.DecodeFrom(key);
}
if (!iter->status().ok()) {
status = iter->status();
}
// ...
if (status.ok()) {
tables_.push_back(t); // BUG: pushed even when empty == true
}When the SST's data blocks are corrupted but the index block and footer remain intact, Table::Open succeeds and the iterator yields entries with OK status. Every key's bytes are garbage, so ParseInternalKey (db/dbformat.h:171) returns false for each — either the key is shorter than 8 bytes or the type byte exceeds kTypeValue. The loop never enters the body, counter stays 0, empty stays true, and t.meta.smallest/largest are never populated. Yet because status is OK, the table is pushed into tables_ with invalid (empty-rep_) InternalKeys.
Later, WriteDescriptor serializes every table:
// db/repair.cc:368-372
for (size_t i = 0; i < tables_.size(); i++) {
const TableInfo& t = tables_[i];
edit_.AddFile(0, t.meta.number, t.meta.file_size, t.meta.smallest,
t.meta.largest);
}VersionEdit::EncodeTo calls f.smallest.Encode(), which hits:
// db/dbformat.h:149-151
Slice Encode() const {
assert(!rep_.empty()); // CRASH
return rep_;
}This is a library bug, not API misuse:
RepairDBis a publicLEVELDB_EXPORTAPI (include/leveldb/db.h:157-163) documented as the recovery tool for databases that cannot be opened — "resurrect as much of the contents as possible." Corruption is the expected input, not a forbidden one.ScanTablealready maintains theemptyflag tracking whether any valid key was found, but the push at line 288 guards onstatus.ok()alone, ignoringempty.InternalKey's own constructor comment (db/dbformat.h:139) states an emptyrep_means "invalid" — yet the library pushes an invalidInternalKeyintotables_and later unconditionally serializes it viaEncode(), which asserts the very invariant the library itself violated.- The assertion checks internal state (
rep_non-emptiness), not an input requirement on the caller.
In release builds (-DNDEBUG) the assertion is compiled out: RepairDB returns Status::OK() but writes a MANIFEST containing a new-file entry with zero-length smallest/largest keys. Reopening the repaired DB fails with Corruption: VersionEdit: new-file entry (the GetInternalKey decoder at version_edit.cc:181 rejects the empty key). Thus RepairDB silently produces an unopenable database — failing its core contract with no error indication.
PoC Code
#include <leveldb/db.h>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <string>
int main(){
std::string p="/tmp/ldb_poc";
system("rm -rf /tmp/ldb_poc; mkdir -p /tmp/ldb_poc");
// Create a DB, write one key, compact to produce an SST (.ldb) on disk
leveldb::Options o; o.create_if_missing=true;
o.compression=leveldb::kSnappyCompression;
leveldb::DB* d=nullptr;
if(!leveldb::DB::Open(o,p,&d).ok())return 0;
d->Put({},"a",std::string(500,'x'));
d->CompactRange(nullptr,nullptr);
delete d;
// Corrupt each .ldb: parse the footer to find where data blocks end,
// then zero only that region. The index block + footer stay intact so
// Table::Open succeeds (iterator status OK), but every data key becomes
// garbage that ParseInternalKey rejects.
DIR*dir=opendir(p.c_str());struct dirent*e;
while((e=readdir(dir))){
std::string n=e->d_name; if(n.size()<5||n.substr(n.size()-4)!=".ldb")continue;
FILE*f=fopen((p+"/"+n).c_str(),"r+b");if(!f)continue;
fseek(f,0,SEEK_END);long sz=ftell(f);fseek(f,0,SEEK_SET);
if(sz<48){fclose(f);continue;}
char*b=new char[sz];fread(b,1,sz,f);
uint8_t*ft=(uint8_t*)b+sz-48;uint64_t mo=0;int sh=0,i=0;
while(1){uint8_t by=ft[i++];mo|=uint64_t(by&0x7f)<<sh;if(!
(by&0x80))break;sh+=7;}
if(mo<(uint64_t)sz)memset(b,0,mo);
fseek(f,0,SEEK_SET);fwrite(b,1,sz,f);fclose(f);delete[]b;
}
closedir(dir);
// RepairDB -> ScanTable pushes the table with empty smallest/largest
// InternalKeys -> WriteDescriptor -> InternalKey::Encode() ->
// assert(!rep_.empty()) crashes.
leveldb::RepairDB(p,o);
}Stack Trace
ERROR: AddressSanitizer: ABRT on unknown address 0x000000003aea (pc 0x7e7a35247b2c bp 0x7ffe0c086960 sp 0x7ffe0c086920 T0)
#0 0x7e7a35247b2c in __pthread_kill_implementation nptl/pthread_kill.c:44:76
#1 0x7e7a35247b2c in __pthread_kill_internal nptl/pthread_kill.c:78:10
#2 0x7e7a35247b2c in pthread_kill nptl/pthread_kill.c:89:10
#3 0x7e7a351ee27d in raise signal/../sysdeps/posix/raise.c:26:13
#4 0x7e7a351d18fe in abort stdlib/abort.c:79:7
#5 0x7e7a351d181a in __assert_fail_base assert/assert.c:96:3
#6 0x7e7a351e4516 in __assert_fail assert/assert.c:105:3
#7 0x5966b9d0847a in leveldb::InternalKey::Encode() const /db/dbformat.h:150:5
#8 0x5966b9d059ec in leveldb::VersionEdit::EncodeTo(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>>*) const /db/version_edit.cc:82:44
#9 0x5966b9cf885b in leveldb::(anonymous namespace)::Repairer::WriteDescriptor() /db/repair.cc:380:13
#10 0x5966b9cf61b6 in leveldb::(anonymous namespace)::Repairer::Run() /db/repair.cc:75:16
#11 0x5966b9cf58f9 in leveldb::RepairDB(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, leveldb::Options const&) /db/repair.cc:448:19
#12 0x5966b9c8ebef in main /poc.cpp:43:3
#13 0x7e7a351d31c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#14 0x7e7a351d328a in __libc_start_main csu/../csu/libc-start.c:360:3
#15 0x5966b9ba28e4 in _start (/poc+0xe38e4) (BuildId: 1574ecc6605c554f613b342f73c70db3962a6d39)
Register values:
rax = 0x0000000000000000 rbx = 0x0000000000003aea rcx = 0x00007e7a35247b2c rdx = 0x0000000000000006
rdi = 0x0000000000003aea rsi = 0x0000000000003aea rbp = 0x00007ffe0c086960 rsp = 0x00007ffe0c086920
r8 = 0x0000000000000085 r9 = 0x00007cba345e0000 r10 = 0x0000000000000008 r11 = 0x0000000000000246
r12 = 0x0000000000000006 r13 = 0x00005966b9db26cf r14 = 0x0000000000000016 r15 = 0x00005966b9dd5a20
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: ABRT nptl/pthread_kill.c:44:76 in __pthread_kill_implementationReproduction Step
# 1. Checkout leveldb at the version above
git clone https://github.com/google/leveldb.git
cd leveldb
git checkout 7ee830d02b623e8ffe0b95d59a74db1e58da04c5
# 2. Build the library (sanitizer flags)
export CC=clang CXX=clang++
export CFLAGS="-fsanitize=address,undefined -g -O0 -fPIC -fno-omit-frame-pointer"
export CXXFLAGS="$CFLAGS"
export WORK=$(pwd)/build/sanitizer
mkdir -p build && cd build
cmake -DCMAKE_INSTALL_PREFIX="$WORK" \
-DCMAKE_C_COMPILER="$CC" -DCMAKE_CXX_COMPILER="$CXX" \
-DCMAKE_C_FLAGS="$CFLAGS" -DCMAKE_CXX_FLAGS="$CXXFLAGS" \
-DBUILD_SHARED_LIBS=OFF \
-DLEVELDB_BUILD_TESTS=OFF \
-DLEVELDB_BUILD_BENCHMARKS=OFF \
-DLEVELDB_INSTALL=ON \
..
make -j$(nproc) && make install
# 3. Build the PoC against that build
clang++ -g -O0 -fsanitize=address,undefined \
-I"$WORK/include" \
poc.cpp -o poc \
-Wl,--start-group $WORK/lib/lib*.a -Wl,--end-group \
-lsnappy
# 4. Run
./pocSubmission Statement
This report was produced by FuzzAnything's AI-assisted library fuzzer and manually verified by a team member. We reviewed the PoC against the upstream API documentation — call order, parameters, and memory ownership — and found no API misuse.
Signed-off-by: FuzzAnything [email protected]
Source: google/leveldb