Summary
There is an implicit lifecycle dependency between the DB instance and the Iterator objects it creates. The Iterator (specifically IterState inside DBImpl::NewInternalIterator) holds a raw pointer to DBImpl::mutex_.
If the DB object is destroyed before the Iterator object (e.g., in a concurrent environment where the DB shutdown race happens against an iterator going out of scope), the Iterator's destructor triggers a Use-After-Free (UAF) by attempting to lock the already-destroyed mutex in CleanupIteratorState.
While users should ensure iterators are destroyed before the DB, the library does not enforce this safety, leading to hard-to-debug crashes (Segfaults) rather than a clean error or assertion in Release builds.
Reproduction Steps
Here is a minimal reproduction script that demonstrates the crash on the latest main branch.
reproduce_uaf.cc:
#include <cassert>
#include <iostream>
#include "leveldb/db.h"
int main() {
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
// 1. Open the DB
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb_uaf", &db);
assert(status.ok());
db->Put(leveldb::WriteOptions(), "key", "value");
// 2. Create an Iterator (this holds a raw pointer to db->mutex_)
leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
it->SeekToFirst(); // Just to make it active
// 3. Destroy the DB
// This destroys DBImpl and its mutex_, but the Iterator is still alive.
delete db;
std::cout << "DB destroyed." << std::endl;
// 4. Destroy the Iterator
// CRASH: This calls CleanupIteratorState, which tries to access the freed mutex_.
delete it;
std::cout << "Iterator destroyed." << std::endl;
return 0;
}
Compilation & Execution:
g++ reproduce_uaf.cc -o reproduce_uaf -lleveldb -lpthread
./reproduce_uaf
Actual Behavior
- Release Build: Segmentation fault (core dumped) or Undefined Behavior (depending on memory allocator state).
- Debug Build / ASAN: AddressSanitizer consistently reports SEGV on unknown address or Heap-use-after-free.
Stack Trace (ASAN Summary):
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1587593==ERROR: AddressSanitizer: SEGV on unknown address ...
#0 0x559a8e291b17 in leveldb::UnrefEntry(void*, void*) ...
#1 0x559a8e29ef44 in leveldb::Iterator::~Iterator() ...
#2 0x559a8e2a58e4 in leveldb::(anonymous namespace)::TwoLevelIterator::~TwoLevelIterator() ...
...
Root Cause Analysis
In db/db_impl.cc:
-
NewInternalIterator captures &mutex_ (a member of DBImpl) into IterState:
IterState* cleanup = new IterState(&mutex_, mem_, imm_, versions_->current());
internal_iter->RegisterCleanup(CleanupIteratorState, cleanup, nullptr);
-
CleanupIteratorState uses this pointer unconditionally:
static void CleanupIteratorState(void* arg1, void* arg2) {
IterState* state = reinterpret_cast<IterState*>(arg1);
state->mu->Lock(); // <--- UAF if DBImpl is already deleted
// ...
}
Summary
There is an implicit lifecycle dependency between the
DBinstance and theIteratorobjects it creates. TheIterator(specificallyIterStateinsideDBImpl::NewInternalIterator) holds a raw pointer toDBImpl::mutex_.If the
DBobject is destroyed before theIteratorobject (e.g., in a concurrent environment where the DB shutdown race happens against an iterator going out of scope), theIterator's destructor triggers a Use-After-Free (UAF) by attempting to lock the already-destroyed mutex inCleanupIteratorState.While users should ensure iterators are destroyed before the DB, the library does not enforce this safety, leading to hard-to-debug crashes (Segfaults) rather than a clean error or assertion in Release builds.
Reproduction Steps
Here is a minimal reproduction script that demonstrates the crash on the latest
mainbranch.reproduce_uaf.cc:Compilation & Execution:
Actual Behavior
Stack Trace (ASAN Summary):
Root Cause Analysis
In
db/db_impl.cc:NewInternalIteratorcaptures&mutex_(a member ofDBImpl) intoIterState:CleanupIteratorStateuses this pointer unconditionally: