\n
RealmDB

Realm Database Forensics

What is Realm, and why is it in your case?

Realm is an object database for mobile applications. It stores objects directly rather than rows in tables, which is why developers chose it, and it is why a SQLite tool opens a Realm file and shows nothing.

MongoDB acquired Realm in 2019 and later wound down the hosted synchronisation service. That did not remove Realm from devices. The local database is still open source, applications built on it are still installed, and the files are still being written. Imgur and Life360 are two you are likely to meet.

Realm is also the format where a deleted object has the best chance of coming back, because of a design decision the developers made for performance and never intended as a forensic gift.

Byte level

What does a Realm file look like on disk?

A Realm database is a single .realm file, usually accompanied by .realm.lock, a .realm.management folder and sometimes a .realm.note. The data is in the .realm file. The others are coordination between processes.

The file opens with a 24 byte header, and the identifying bytes are not at the start. They sit at offset 16.

The 24 byte file header
00 top_ref[0] 8 bytes, little-endian
08 top_ref[1] 8 bytes, little-endian
10 54 2D 44 42 "T-DB" the magic
14 format_version[0] 1 byte
15 format_version[1] 1 byte
16 reserved 1 byte
17 flags bit 0 selects the active top_ref

Everything after the header is a tree of arrays, and every array announces itself with the same four bytes. When Realm flushes an array to disk it replaces the in-memory allocator capacity with a fixed marker, so the file is full of literal capital A characters.

Every array header, 8 bytes
41 41 41 41 "AAAA" the on-disk marker
FF flags: inner node, has refs, width
?? ?? ?? element count, 24-bit BIG-endian

Two details there catch people out. The element count is big-endian while the header's own pointers are little-endian. And arrays always begin on an 8 byte boundary, which means a scan for these markers can step eight bytes at a time rather than one.

The design decision

Why does the file contain a previous version of itself?

Look at the header again. There are two root pointers, not one, and a flag that says which is active.

This is how Realm commits safely. It never modifies existing data. It writes the new state into free space, then flips one bit in the header to point at it. If power fails mid-write, the old pointer is still the active one and the database is intact. The technique is called copy-on-write, and the two slots alternate.

The forensic consequence is that the inactive slot still points at a complete, valid, previous committed state of the entire database. Not a fragment. A whole earlier version, reachable by following one pointer the running application no longer uses.

How much earlier depends on write activity. On a busy database it is the state one commit ago. On a database that has been quiet since the events you care about, it can be considerably older.

Reading the flag
flags & 0x01 == 0 → top_ref[0] is live top_ref[1] is the previous state flags & 0x01 == 1 → top_ref[1] is live top_ref[0] is the previous state
What this means in a report

You can compare two committed states of the same database from a single file, with no backup, no shadow copy and no second acquisition. Differences between them are changes the application made, and they are readable rather than inferred.

The same principle appears in SQLite as the write-ahead log, where superseded page images survive alongside the current ones. See Write-Ahead Log Viewer.

Structure

How is an object stored?

Not as a row. Realm stores each class as a cluster tree, and the leaves of that tree are where object data lives. A leaf holds at most 256 objects. Larger classes grow an inner tree above them.

Inside a leaf, the layout is the part that matters for recovery. Slot 0 holds the object keys. Every slot after it is a pointer to a separate array holding one column for every object in that leaf.

A cluster leaf
slot 0 → object keys (or an inline count)
slot 1 → column 1 array all objects' first field
slot 2 → column 2 array all objects' second field
slot 3 → column 3 array …in schema order

An object is therefore not stored in one place. It is spread across as many arrays as it has fields, and it is assembled by reading the same position out of each. That is column-major storage, and it is the reason recovery behaves the way section five describes.

The types you will meet

Realm has eighteen column types. Most are familiar: Int, Bool, String, Binary, Float, Double, Decimal128, Timestamp, UUID, ObjectId and Mixed. Four are not, and they are the ones worth knowing.

TypeWhat it holdsWhy it matters
LinkA direct pointer to another objectA real relationship, not a foreign key you have to join
LinkListAn ordered list of pointersMembership and order are both recorded
BackLinkThe reverse of a link, maintained automaticallyShows what pointed at an object, including from classes you were not looking at
TypedLinkA pointer that also records the target classResolvable without knowing the schema in advance

Links are the reason a Realm file can answer questions a flat export cannot. A message object points at its conversation and at its sender. Follow the backlinks from a person and you get everything that referenced them, without writing a join or guessing at a naming convention.

Reachable

Found by walking from the active root pointer. This is the live database.

On the free list

The allocator itself marked this space as released. Higher confidence.

Orphaned

A valid array header that nothing points at. A previous version, or a deletion.

Overwritten

A later write reused the space. These are gone, and saying so is part of the job.

Recovery

Why do deleted objects survive?

For the same reason the previous version survives. Realm does not edit in place, so deleting an object does not clear its bytes. It removes the object from the new version of the tree and releases the space it occupied.

Released is not erased. The bytes stay exactly where they were, still carrying their array markers, until some later write happens to need that space and reuses it.

Finding them is a scan, not a carve in the traditional sense. Walk every array reachable from the active root pointer and record where you went. Then scan the whole file on eight byte boundaries for array markers. Anything the scan finds that the walk did not reach is a candidate, and these are structured arrays with intact headers rather than fragments pulled out of slack.

The free list makes the result sharper. Arrays the allocator explicitly released are stronger candidates than orphans found by scanning alone, because an orphan can also be a leftover from an older file format version or an unreferenced stale snapshot.

What to expect

Which deleted values come back, and which do not?

This is where column-major storage stops being a design note and starts deciding your result. Because each column is its own array, the columns of a deleted object are released as separate blocks of different sizes, and the allocator reuses those sizes at different rates.

Column typeSurvivalWhy
Int Double Bool Timestamp Good Fixed width arrays. The released block lands in a size class that later small allocations rarely match, so the bytes sit untouched across many commits.
String Poor Realm packs strings densely, and the released block is usually the same size as the next small string the application writes. Reuse is often within two or three commits.
Binary Mixed Depends entirely on size. A large blob occupies a block nothing else fits into.

The practical effect is that a recovered object frequently comes back with its numbers and its times intact and some of its text missing. Coordinates survive. Timestamps survive. The label describing what happened there is often the field that was reused.

This is a property, not a defect

A partial reconstruction is the honest result. The fields that decoded are evidence. The fields that did not are unavailable because a later write reused their space, and that is a statement you can make and defend. A tool that fills those gaps with plausible values is doing something else.

The trap

Why is attributing a recovered row dangerous?

Recovering bytes is the easy half. Saying which object they belonged to is where a Realm analysis goes wrong, and the failure is quiet enough that nobody notices until the report is challenged.

Object keys in a cluster leaf are stored relative to a base value that comes from the leaf's parent in the tree. An orphaned leaf has lost its parent. Its keys are therefore relative to a base you cannot determine.

  1. The mistake

    Read the recovered keys as if they were absolute and compare them against the live objects. Key 8 in the orphan is matched to key 8 in the live database.

  2. What follows

    Every comparison is against the wrong object. The tool now reports that a field changed, or that an object was deleted, and names a live object that had nothing to do with it.

  3. Why it is hard to catch

    The output looks entirely reasonable. Real keys, real field names, real values, a plausible history. Nothing signals that the two sides were never the same object.

  4. The correct approach

    Anchor on application level identity instead. Use the primary key value when the class has one, full content matching when it does not, and report the row as an unattributed fragment when neither settles it.

A second guard worth applying

Before claiming a recovered object was deleted, require more than its primary key to decode. A reused block can produce a clean, plausible key that matches nothing live, which looks exactly like a deletion and is not one. One readable field is not enough to support the claim.

There is a related trap in the values themselves. If a recovered object's column array is still reachable from the live root pointer, the value you decode cannot be attributed to the recovered object as independent evidence. Either the two share an unchanged array through copy-on-write, or a newer object reused it. Both cases mean the value belongs to the live database, not to the deleted one.

The workspace

Where does Elusive Data Firefly fit?

Firefly reads the Realm file directly rather than through the Realm SDK, which is what makes the previous version and the orphaned arrays visible at all. An SDK shows you the live database, because that is its purpose.

Recovery runs in three passes. Walk everything reachable from the active root pointer. Collect the allocator's free list. Then scan the file on eight byte boundaries for array markers and treat what the walk never reached as candidates.

Candidates are matched against the live schema by shape, checking that each slot holds a leaf consistent with the type that column should be. Above a match threshold, the same decoder that reads the live grid reads the candidate, so a recovered value and a live value are produced by identical code.

Each result is then classified, and the fourth classification is the one that matters.

Recovery classification
Deleted the object is absent from the live database
Modified it exists live, with different field values
Snapshot an earlier version of a live object
Fragment bytes decoded, identity not established
Fragment is the point

When the recovered bytes cannot be tied to a specific object, Firefly says so and stops. It does not compare raw keys against live objects, because that fabricates matches and produces confident claims about the wrong record.

Both versions readable

The active root pointer and the previous one, from the same file.

Free list separated

Allocator-released arrays are distinguished from orphans found by scanning.

Partial rows stay partial

Fields whose arrays were reused decode as unavailable, never as a guess.

Payloads identified

Binary columns and orphan blobs are labelled by signature: PNG, JPEG, HEIC, PDF and more.

Related: SQLite Forensic Analysis, LevelDB and IndexedDB and Apple Biome and SEGB.

Training

Where do you learn the method?

Realm is a different format, not a different discipline. Read the header, find the root, learn what marks a release, and know the difference between a value you recovered and a value you can attribute. Those habits come from working a format you can check your answer against, which is why the training is built on SQLite.

Complete · 24 CPE · Certificate

Certified SQLite Forensics

The foundation the rest of it stands on. Record headers, serial types, varints, recovery and validation, taught on real training files you work on in your own tools.

  • Read a record header and know what each field is doing
  • Recover deleted content and name the mechanism that preserved it
  • Tell a recovered value apart from an attributed one
  • Verify a timestamp interpretation instead of accepting a conversion
  • Validate incomplete or unsupported tool output against the source
  • Explain how a finding was reached, in terms a reviewer can follow
Focused · 5 CPE · On demand

WAL Frames and SHM Index

A shorter course on transaction files. Realm's two root pointers and SQLite's write-ahead log solve the same problem, and both leave a superseded state behind.

  • Read a WAL frame header and find transaction boundaries
  • Recover page images written before the last checkpoint
  • Handle a file set without checkpointing away the evidence

Certified SQLite Forensics also runs live online and on-site by arrangement. See the formats.

Last updated 7 September 2026