Skip to content

# [Bug] Use-After-Free / Segfault when destroying Iterator after DB is closed #1292

@yxscc

Description

@yxscc

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:

  1. NewInternalIterator captures &mutex_ (a member of DBImpl) into IterState:

    IterState* cleanup = new IterState(&mutex_, mem_, imm_, versions_->current());
    internal_iter->RegisterCleanup(CleanupIteratorState, cleanup, nullptr);
  2. 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
      // ...
    }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions