Fix it now
- Drop the file
- Repair runs locally
- Download the result
Both of Android's persistence layers write ordinary SQLite: Room generates its schema on top of the same engine the old SQLiteOpenHelper uses, and both land a single file at /data/data/<package>/databases/<name>.db. When that file is damaged, Room throws android.database.sqlite.SQLiteDatabaseCorruptException: database disk image is malformed (code 11 SQLITE_CORRUPT) and the app force-closes or shows an empty screen. Drop the pulled .db above — and add its -wal sidecar in the optional slot if you have it — and the tool scans the pages, rebuilds the schema, and re-inserts every row it can still decode into a fresh database, all inside your browser tab. The file is never uploaded.
Where an Android app keeps its database — and what's inside it
On any Android device the database lives in the app's private sandbox at /data/data/<package>/databases/, usually as three files: the main <name>.db, a write-ahead log <name>.db-wal, and a shared-memory index <name>.db-shm. The main file opens with the 16-byte magic string "SQLite format 3\000"; the two-byte page size sits at byte offset 16, and the file-format read/write version bytes at offsets 18 and 19 read 2 when the database is in WAL mode — which Room turns on by default.
Room adds fingerprints you can use to confirm you have the right file. It creates an android_metadata table holding a single locale row (for example en_US) — that table is written by Android's SQLiteDatabase for every app DB, Room or not — and a room_master_table with columns id and identity_hash, where Room stores the schema hash at the fixed row id = 42. Your entity tables (annotated @Entity) and their indices sit alongside them, all listed in the schema table sqlite_master (aliased sqlite_schema in newer SQLite). A plain SQLiteOpenHelper app skips room_master_table but is otherwise the same standard SQLite file.
How the file goes malformed — and the exact errors
A SQLite database is a flat array of fixed-size pages (4096 bytes by default). Page 1 carries the header and the schema; every table and index is a b-tree whose interior pages hold cell pointers down to leaf pages, and the leaves hold your actual records. The engine reads the header, finds the schema in sqlite_master, then follows those pointers to the rows.
The pointers are the weak point. When the app process is killed by Android's low-memory killer mid-write, the phone loses power, storage fills up, or two processes open the same file without proper locking, a single interior page or a header field can be left inconsistent. SQLite then stops at the first problem and refuses the whole file with database disk image is malformed — result code 11 (SQLITE_CORRUPT) — even though the leaf pages behind the break still hold intact, self-describing records. A related failure, file is not a database — result code 26 (SQLITE_NOTADB) — means the 16-byte header magic is wrong: either the header bytes were damaged (the pages behind them are often fine and recoverable) or the file is genuinely something else, such as an SQLCipher-encrypted database that reads as high-entropy noise.
One Android-specific trap makes this urgent: the framework's DefaultDatabaseErrorHandler.onCorruption() deletes the database file when it detects corruption on open. So an app that hit the malformed error may already have wiped the file on its next launch. Copy the database off the device — and work on that copy — before opening the app again.
The -wal and -shm sidecars: pull all three
Room enables write-ahead logging by default (enableWriteAheadLogging()), which means recent committed changes are staged in the sibling <name>.db-wal file until a checkpoint folds them back into the main .db. After a crash, your newest rows can live only in that WAL. Pull the main file without it and the recovered database will be missing everything written since the last checkpoint — a common reason people think "half my data is gone".
The WAL is not the main database: it starts with a 32-byte header whose magic is 0x377f0682 or 0x377f0683 (the low bit selects big- or little-endian frame checksums), followed by frames of a 24-byte frame header plus one page image each. The -shm file is just a shared-memory index into the WAL; it is regenerated automatically and safe to leave behind. So pull the main .db and the -wal together: drop the database above, then add the -wal in the optional sidecar slot and its committed frames are overlaid before the scan, so recovery reflects the last committed transaction rather than the last checkpoint.
Getting the database off the device safely
On a debuggable build you can reach the private folder with run-as: for example adb exec-out run-as <package> tar c ./databases | tar xv, or copy the files to shared storage with run-as <package> cp databases/<name>.db /sdcard/ and then adb pull. Release (non-debuggable) apps refuse run-as, so you need a rooted device or the app's own export feature. The old adb backup -f backup.ab <package> path was deprecated in Android 12 and honours android:allowBackup, so it is unreliable for this. Whichever route you take, grab <name>.db, <name>.db-wal and <name>.db-shm in one go so the WAL matches the main file.
With the copy on your machine, the classic check is sqlite3 name.db "PRAGMA integrity_check;" (or the faster quick_check), and the classic fix is SQLite's own .recover command, which reads out every decodable row and writes it into a brand-new file. This page does exactly that in the browser: it walks the pages, rebuilds the b-trees, falls back to a raw page sweep for rows the pointer chain can no longer reach, and drops anything it cannot attribute to a known table into a lost_and_found table rather than discarding it — so nothing is thrown away and nothing is uploaded.
What this can and can't fix
Can fix
- "database disk image is malformed (code 11 SQLITE_CORRUPT)" from a damaged page or b-tree cell pointer, with intact leaf pages behind it
- A wrong or damaged header (bad page-size or page-count field) when the pages after it are still readable
- Rows on pages the pointer chain can no longer reach, pulled out by a raw page scan into a lost_and_found table
- Uncheckpointed rows from a supplied
.db-wal sidecar (the newest data after a crash) - Room and SQLiteOpenHelper databases copied off the device before the app's error handler deleted them
Can't fix
- Rows physically overwritten or truncated off the end of the file — those bytes no longer exist
- SQLCipher-encrypted databases (Signal, or Room apps using a SupportFactory) without the passphrase — the pages are ciphertext
- A database that DefaultDatabaseErrorHandler already deleted — that's a device data-recovery problem first
- Exact column affinities: values come back as their on-disk storage class (NULL/INTEGER/REAL/TEXT/BLOB)
- A 0-byte .db file — there is nothing on disk to decode
If a repair fails, we tell you why (missing data versus broken structure), and you are never charged for a failed repair.