fix(cache): load SQLCipher native lib before every keyed open #208

Merged
JMR-dev merged 2 commits from fix-sqlcipher-native-lib-load into main 2026-07-03 14:38:36 +00:00
JMR-dev commented 2026-07-03 14:22:43 +00:00 (Migrated from github.com)

Closes #210

Problem

The opt-in encrypted cache crash-loops on launch for any user who has enabled it, on the first cold start after enabling (e.g. an app upgrade):

FATAL EXCEPTION: arch_disk_io
java.lang.UnsatisfiedLinkError: No implementation found for long
  net.zetetic.database.sqlcipher.SQLiteConnection.nativeOpen(...) — is the library loaded, e.g. System.loadLibrary?
    at ...SupportHelper.getWritableDatabase(...)   ← Room opening the encrypted cache

Root cause

System.loadLibrary("sqlcipher") (in DatabaseEncryption.ensureNativeLibraryLoaded()) is only called as a side effect of:

  • an actual plaintext↔encrypted conversion (DatabaseEncryption.migrate), or
  • the one-time #111 account migration (AccountDataMigrator.copyAccountTables).

On a steady-state start the cache is already encrypted (so ensureEncrypted early-returns) and the account migration is already done (so it no-ops), so nothing loads the native library before Room opens the keyed DB via SupportOpenHelperFactory → nativeOpen. The previous process only survived because an earlier conversion had loaded the .so in-memory; the next cold start (an upgrade, or any force-stop + relaunch) crashes.

Not an ABI/packaging problem — lib/arm64-v8a/libsqlcipher.so is present in the APK and loads fine; it was simply never asked to load.

Fix

Load the library explicitly in DatabaseProvisioner whenever it commits to an encrypted open (idempotent; no-ops when already loaded), so the invariant "prepareCache() returns Encrypted ⇒ the native lib is loaded" holds for every consumer.

Regression assertions added to DatabaseProvisionerTest: the encrypted path must load the lib; the plaintext path must not.

Testing

  • Preflight green: assembleDebug, testDebugUnitTest, lintDebug, ktlintCheck, detekt (+ androidTest compiles).
  • Verified on a Pixel 10 Pro XL: rebuilt the minified release, adb install -r (in-place update preserving the already-encrypted cache); the app now launches to MainActivity with a stable PID and an empty crash buffer instead of crash-looping.

🤖 Generated with Claude Code

Closes #210 ## Problem The opt-in encrypted cache crash-loops on launch for any user who has enabled it, on the first cold start after enabling (e.g. an app upgrade): ``` FATAL EXCEPTION: arch_disk_io java.lang.UnsatisfiedLinkError: No implementation found for long net.zetetic.database.sqlcipher.SQLiteConnection.nativeOpen(...) — is the library loaded, e.g. System.loadLibrary? at ...SupportHelper.getWritableDatabase(...) ← Room opening the encrypted cache ``` ## Root cause `System.loadLibrary("sqlcipher")` (in `DatabaseEncryption.ensureNativeLibraryLoaded()`) is only called as a **side effect** of: - an actual plaintext↔encrypted conversion (`DatabaseEncryption.migrate`), or - the one-time #111 account migration (`AccountDataMigrator.copyAccountTables`). On a steady-state start the cache is **already** encrypted (so `ensureEncrypted` early-returns) and the account migration is **already** done (so it no-ops), so nothing loads the native library before Room opens the keyed DB via `SupportOpenHelperFactory` → `nativeOpen`. The previous process only survived because an earlier conversion had loaded the `.so` in-memory; the next cold start (an upgrade, or any force-stop + relaunch) crashes. Not an ABI/packaging problem — `lib/arm64-v8a/libsqlcipher.so` is present in the APK and loads fine; it was simply never asked to load. ## Fix Load the library explicitly in `DatabaseProvisioner` whenever it commits to an encrypted open (idempotent; no-ops when already loaded), so the invariant *"`prepareCache()` returns `Encrypted` ⇒ the native lib is loaded"* holds for every consumer. Regression assertions added to `DatabaseProvisionerTest`: the encrypted path **must** load the lib; the plaintext path **must not**. ## Testing - **Preflight green**: `assembleDebug`, `testDebugUnitTest`, `lintDebug`, `ktlintCheck`, `detekt` (+ `androidTest` compiles). - **Verified on a Pixel 10 Pro XL**: rebuilt the minified release, `adb install -r` (in-place update preserving the already-encrypted cache); the app now launches to `MainActivity` with a stable PID and an empty crash buffer instead of crash-looping. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.