Chromium LevelDB format for forensics
2026-06-21 · 4 min
LevelDB is a simple key/value store, and Chromium leans on it hard: Local Storage, Session Storage and IndexedDB all live there. Each origin or store gets its own LevelDB directory. Understand the on-disk format and you understand both how to read these artifacts and why deleted values survive long after the user thinks they are gone.
One directory per store
LevelDB is not a single file — it is a directory. Chromium creates one per artifact class, and inside IndexedDB, one per origin:
…\<profile>\Local Storage\leveldb\
…\<profile>\Session Storage\
…\<profile>\IndexedDB\<origin>.indexeddb.leveldb\
Copy the whole directory. A loose .ldb file out of context is much
harder to interpret, and LevelDB itself will refuse to open an
incomplete set.
The files inside
A LevelDB directory is a small, fixed cast of files:
- CURRENT — a one-line pointer to the live
MANIFESTfile. - MANIFEST-NNNNNN — the version edit log. It records file metadata:
which
.ldbfiles exist, at which level, and their key ranges. This is the index that tells LevelDB which SSTables are actually live. - *.log — the write-ahead log backing the in-memory memtable. Recent writes that have not yet been flushed to an SSTable live here.
- *.ldb / *.sst — Sorted String Tables: immutable, sorted blocks of key/value records. The bulk of the data.
- LOCK — the process lock file (empty; do not let a live tool grab it).
- LOG — a human-readable operational log of compactions and flushes.
To read a store correctly you follow CURRENT to the live MANIFEST, learn
which SSTables are live from the MANIFEST, then merge those SSTables with
the .log memtable — newest sequence number wins per key.
Records, sequence numbers and tombstones
Every record carries a sequence number and a type: a value write or a deletion tombstone. LevelDB never edits records in place. A delete is just a new record with a tombstone type and a higher sequence number; an update is a new value record. The old record stays on disk in its original SSTable until compaction rewrites that file.
This is the forensic gift. Because SSTables are immutable and compaction
is lazy, old and deleted values routinely survive in older .ldb and
.log files. The live merge hides them — but a raw scan over every file
surfaces superseded values and the tombstones that buried them. Until
compaction runs, "deleted" Local Storage and IndexedDB entries are often
still right there.
Block encoding and key formats
Block data is Snappy-compressed; a per-block trailer flags whether to decompress. Once decoded, the key tells you which artifact you have:
- Local Storage keys look like
_<origin>\x00<key>. The value carries a one-byte encoding marker (UTF-16LE vs Latin-1). - Session Storage is namespaced through a
namespace -> map -> keyindirection; resolve the meta keys before reading values. - IndexedDB encodes object stores and indexes into the key space and is considerably more complex; values are V8 structured-clone blobs.
A raw key/value view is invaluable here: it shows you the literal bytes when the higher-level decoder cannot make sense of a fragment.
Reading it in the tool
BrowserForensics reads LevelDB stores entirely client-side — Local and
Session Storage, best-effort IndexedDB, plus a raw key/value view for
everything else. Nothing leaves the browser. Point it at a copied
leveldb directory and it walks CURRENT, the MANIFEST and the SSTable
plus .log set the way LevelDB itself would, while exposing the
superseded records a live read would discard.
Acquisition notes
- Collect the entire directory, including
.log. The most recent writes may exist only in the memtable log, not yet in any.ldb. - Never open a store with a live, read-write tool first — it can trigger compaction and destroy the very superseded records you wanted.
- Treat the
LOGfile as a timeline hint: its compaction entries tell you when older values were last at risk of being rewritten.