On-device crash investigation (physical Pixel 10 Pro XL, Android 17 / SDK 37) surfaced a SECOND crash cluster distinct from #354: a SQLCipher UnsatisfiedLinkError opening the encrypted Room cache.
Crash
java.lang.UnsatisfiedLinkError … SQLiteConnection.nativeOpen — x4 on 2026-07-03 09:02:22-57 (dropbox data_app_crash, in the on-device bugreport). The SQLCipher native library fails to load when opening the encrypted cache DB; settings later show encryptCache=false (ran unencrypted).
Likely root cause: 16 KB page size
Android 15+ devices (incl. this Pixel on SDK 37; the API-37 emulator image is literally google_apis_ps16k) use 16 KB memory pages. A native .so not built/aligned for 16 KB pages fails to load with exactly this UnsatisfiedLinkError. The bundled SQLCipher/sqlcipher-android native lib is the prime suspect — verify its 16 KB-alignment / bump to a 16 KB-compatible build. (Worth auditing any other bundled .so too.)
Why it matters
This is the opt-in SQLCipher cache-encryption path (security-posture work). On affected devices, enabling encryption crashes instead of degrading, and encryption silently does not apply. Fix: rebuild/upgrade the native lib for 16 KB pages, AND make the open path catch UnsatisfiedLinkError and fail gracefully (clear state, no crash) rather than throw.
Evidence
scratchpad\perf\bugreport.zip (dropbox), scratchpad\perf\crash\dropbox_head.txt. Distinct from the IdleService FGS crash loop (#354).
On-device crash investigation (physical Pixel 10 Pro XL, Android 17 / SDK 37) surfaced a SECOND crash cluster distinct from #354: a **SQLCipher `UnsatisfiedLinkError`** opening the encrypted Room cache.
## Crash
`java.lang.UnsatisfiedLinkError … SQLiteConnection.nativeOpen` — **x4** on 2026-07-03 09:02:22-57 (dropbox `data_app_crash`, in the on-device bugreport). The SQLCipher native library fails to load when opening the encrypted cache DB; settings later show `encryptCache=false` (ran unencrypted).
## Likely root cause: 16 KB page size
Android 15+ devices (incl. this Pixel on SDK 37; the API-37 emulator image is literally `google_apis_ps16k`) use **16 KB memory pages**. A native `.so` not built/aligned for 16 KB pages fails to load with exactly this `UnsatisfiedLinkError`. The bundled SQLCipher/`sqlcipher-android` native lib is the prime suspect — verify its 16 KB-alignment / bump to a 16 KB-compatible build. (Worth auditing any other bundled `.so` too.)
## Why it matters
This is the opt-in SQLCipher cache-encryption path (security-posture work). On affected devices, enabling encryption **crashes** instead of degrading, and encryption silently does not apply. Fix: rebuild/upgrade the native lib for 16 KB pages, AND make the open path catch `UnsatisfiedLinkError` and fail gracefully (clear state, no crash) rather than throw.
## Evidence
`scratchpad\perf\bugreport.zip` (dropbox), `scratchpad\perf\crash\dropbox_head.txt`. Distinct from the IdleService FGS crash loop (#354).
Spike result (draft PR #423) — the 16 KB-alignment hypothesis is REFUTED
On-device spike (SqlCipherOpenSpikeTest, Pixel 8 Pro / real SDK 37 / arm64-v8a):
Alignment is NOT the bug. Every bundled native lib — incl. arm64-v8a libsqlcipher.so (sqlcipher-android:4.16.0, pinned since the original encrypted-cache commit 96c9f71) — is already 16 KB-aligned (PT_LOAD p_align = 0x4000, ELF-verified across all 4 ABIs). CI's green e2e-preview (16 KB x86_64) corroborates. So the crashing 2026-07-03 build shipped the same aligned lib.
4 KB arm64 baseline (Pixel 8): all 3 stages PASS — loadLibrary("sqlcipher") -> nativeOpen (keyed) -> full Room SupportOpenHelperFactory path.
16 KB arm64: BLOCKED. The Pixel "Boot with 16 KB page size" toggle (Settings.Global enable_16k_pages) is gated behind a bootloader unlock + factory wipe (device dialog confirms) — not a simple toggle+reboot — so the Pixel 8 cannot reach arm64-16 KB without wiping it. Setting the flag + reboot is ignored by init on a locked bootloader. Device restored (4 KB, flag=0).
Redirected root-cause hypothesis
The UnsatisfiedLinkError @ SQLiteConnection.nativeOpen is most likely a library-load-ordering bug — a keyed nativeOpen reached without a preceding successful System.loadLibrary("sqlcipher") (the #221/#367 class; a data_app_crash dropbox means a nativeOpen escaped #367 fail-closed try/catch). Next: audit every keyed-cache opener (workers, AccountDatabase, AccountDataMigrator legacy path, cold-open) for load-before-open + handler coverage.
To finish the same-device A/B
Need a genuine 16 KB arm64 environment: the Pixel 10 Pro XL (real 16 KB device; SqlCipherOpenSpikeTest emits the exact failing stage + cause chain the moment it runs there), or authorization to bootloader-unlock + wipe a spare Pixel 8.
## Spike result (draft PR #423) — the 16 KB-alignment hypothesis is REFUTED
On-device spike (`SqlCipherOpenSpikeTest`, Pixel 8 Pro / real SDK 37 / arm64-v8a):
- **Alignment is NOT the bug.** Every bundled native lib — incl. arm64-v8a `libsqlcipher.so` (`sqlcipher-android:4.16.0`, pinned since the original encrypted-cache commit `96c9f71`) — is already **16 KB-aligned** (`PT_LOAD p_align = 0x4000`, ELF-verified across all 4 ABIs). CI's green `e2e-preview` (16 KB **x86_64**) corroborates. So the crashing 2026-07-03 build shipped the same aligned lib.
- **4 KB arm64 baseline (Pixel 8):** all 3 stages PASS — `loadLibrary("sqlcipher")` -> `nativeOpen` (keyed) -> full Room `SupportOpenHelperFactory` path.
- **16 KB arm64: BLOCKED.** The Pixel "Boot with 16 KB page size" toggle (`Settings.Global enable_16k_pages`) is gated behind a **bootloader unlock + factory wipe** (device dialog confirms) — not a simple toggle+reboot — so the Pixel 8 cannot reach arm64-16 KB without wiping it. Setting the flag + reboot is ignored by init on a locked bootloader. Device restored (4 KB, flag=0).
## Redirected root-cause hypothesis
The `UnsatisfiedLinkError @ SQLiteConnection.nativeOpen` is most likely a **library-load-ordering** bug — a keyed `nativeOpen` reached without a preceding successful `System.loadLibrary("sqlcipher")` (the #221/#367 class; a `data_app_crash` dropbox means a `nativeOpen` escaped #367 fail-closed `try/catch`). **Next:** audit every keyed-cache opener (workers, `AccountDatabase`, `AccountDataMigrator` legacy path, cold-open) for load-before-open + handler coverage.
## To finish the same-device A/B
Need a genuine **16 KB arm64** environment: the **Pixel 10 Pro XL** (real 16 KB device; `SqlCipherOpenSpikeTest` emits the exact failing stage + cause chain the moment it runs there), or authorization to bootloader-unlock + wipe a spare Pixel 8.
Audit verdict: PERSISTS — a fail-closed handler-COVERAGE gap (post-#367)
The UnsatisfiedLinkError @ SQLiteConnection.nativeOpen still has a live UNCAUGHT path. The audit ruled out both prior hypotheses:
NOT alignment (refuted by the on-device spike #423 — all native libs are 16 KB-aligned).
NOT load-ordering — every SQLCipher open IS preceded by ensureNativeLibraryLoaded() in the same function.
NOT catch-type — #367's catch (LinkageError) correctly catches UnsatisfiedLinkError.
The real bug is the handler's SCOPE:#367's try/catch in DatabaseProvisioner.runStartupSequence() wraps only resolveOpenMode(...), but the crash-prone keyed opens sit OUTSIDE it:
Primary — AccountDataMigrator.migrateIfNeeded() (DatabaseProvisioner.kt:111, ~12 lines above the try): keyed SQLCipher openOrCreateDatabase+ATTACH KEY (AccountDataMigrator.kt:143-144,156) with an empty key even when encryption is OFF → EVERY pre-#111 upgrader hits the native lib; a LinkageError there → uncaught → crash-loop on every launch.
Secondary — Room's deferred keyed nativeOpen (DatabaseModuleDeferredOpenHelperFactory, DatabaseModule.kt:97-106) fires AFTER prepareCache() returns, outside any handler → the exact UnsatisfiedLinkError @ nativeOpen signature.
Amplifier — headless components (SyncWorker/BackfillWorker/SendWorker/PruneWorker/IdleService) run with NO CacheEncryptionGate → uncaught there too.
Test blind spot:DatabaseProvisionerTest mocks the migrator as just Runs, hiding the gap.
Fix in progress (widen handler scope to the migrator, keep the LinkageError type; probe the real keyed open inside the fail-closed try; headless tolerance via EncryptedCacheGuard; regression tests). PR incoming.
## Audit verdict: PERSISTS — a fail-closed handler-COVERAGE gap (post-#367)
The `UnsatisfiedLinkError @ SQLiteConnection.nativeOpen` still has a live UNCAUGHT path. The audit **ruled out** both prior hypotheses:
- **NOT alignment** (refuted by the on-device spike #423 — all native libs are 16 KB-aligned).
- **NOT load-ordering** — every SQLCipher open IS preceded by `ensureNativeLibraryLoaded()` in the same function.
- **NOT catch-type** — #367's `catch (LinkageError)` correctly catches `UnsatisfiedLinkError`.
**The real bug is the handler's SCOPE:** #367's `try/catch` in `DatabaseProvisioner.runStartupSequence()` wraps only `resolveOpenMode(...)`, but the crash-prone keyed opens sit OUTSIDE it:
1. **Primary — `AccountDataMigrator.migrateIfNeeded()`** (`DatabaseProvisioner.kt:111`, ~12 lines above the `try`): keyed SQLCipher `openOrCreateDatabase`+`ATTACH KEY` (`AccountDataMigrator.kt:143-144,156`) **with an empty key even when encryption is OFF** → EVERY pre-#111 upgrader hits the native lib; a `LinkageError` there → uncaught → **crash-loop on every launch**.
2. **Secondary — Room's deferred keyed `nativeOpen`** (`DatabaseModule` `DeferredOpenHelperFactory`, `DatabaseModule.kt:97-106`) fires AFTER `prepareCache()` returns, outside any handler → the exact `UnsatisfiedLinkError @ nativeOpen` signature.
3. **Amplifier — headless components** (`SyncWorker`/`BackfillWorker`/`SendWorker`/`PruneWorker`/`IdleService`) run with NO `CacheEncryptionGate` → uncaught there too.
**Test blind spot:** `DatabaseProvisionerTest` mocks the migrator as `just Runs`, hiding the gap.
**Fix in progress** (widen handler scope to the migrator, keep the `LinkageError` type; probe the real keyed open inside the fail-closed try; headless tolerance via `EncryptedCacheGuard`; regression tests). PR incoming.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
On-device crash investigation (physical Pixel 10 Pro XL, Android 17 / SDK 37) surfaced a SECOND crash cluster distinct from #354: a SQLCipher
UnsatisfiedLinkErroropening the encrypted Room cache.Crash
java.lang.UnsatisfiedLinkError … SQLiteConnection.nativeOpen— x4 on 2026-07-03 09:02:22-57 (dropboxdata_app_crash, in the on-device bugreport). The SQLCipher native library fails to load when opening the encrypted cache DB; settings later showencryptCache=false(ran unencrypted).Likely root cause: 16 KB page size
Android 15+ devices (incl. this Pixel on SDK 37; the API-37 emulator image is literally
google_apis_ps16k) use 16 KB memory pages. A native.sonot built/aligned for 16 KB pages fails to load with exactly thisUnsatisfiedLinkError. The bundled SQLCipher/sqlcipher-androidnative lib is the prime suspect — verify its 16 KB-alignment / bump to a 16 KB-compatible build. (Worth auditing any other bundled.sotoo.)Why it matters
This is the opt-in SQLCipher cache-encryption path (security-posture work). On affected devices, enabling encryption crashes instead of degrading, and encryption silently does not apply. Fix: rebuild/upgrade the native lib for 16 KB pages, AND make the open path catch
UnsatisfiedLinkErrorand fail gracefully (clear state, no crash) rather than throw.Evidence
scratchpad\perf\bugreport.zip(dropbox),scratchpad\perf\crash\dropbox_head.txt. Distinct from the IdleService FGS crash loop (#354).Spike result (draft PR #423) — the 16 KB-alignment hypothesis is REFUTED
On-device spike (
SqlCipherOpenSpikeTest, Pixel 8 Pro / real SDK 37 / arm64-v8a):libsqlcipher.so(sqlcipher-android:4.16.0, pinned since the original encrypted-cache commit96c9f71) — is already 16 KB-aligned (PT_LOAD p_align = 0x4000, ELF-verified across all 4 ABIs). CI's greene2e-preview(16 KB x86_64) corroborates. So the crashing 2026-07-03 build shipped the same aligned lib.loadLibrary("sqlcipher")->nativeOpen(keyed) -> full RoomSupportOpenHelperFactorypath.Settings.Global enable_16k_pages) is gated behind a bootloader unlock + factory wipe (device dialog confirms) — not a simple toggle+reboot — so the Pixel 8 cannot reach arm64-16 KB without wiping it. Setting the flag + reboot is ignored by init on a locked bootloader. Device restored (4 KB, flag=0).Redirected root-cause hypothesis
The
UnsatisfiedLinkError @ SQLiteConnection.nativeOpenis most likely a library-load-ordering bug — a keyednativeOpenreached without a preceding successfulSystem.loadLibrary("sqlcipher")(the #221/#367 class; adata_app_crashdropbox means anativeOpenescaped #367 fail-closedtry/catch). Next: audit every keyed-cache opener (workers,AccountDatabase,AccountDataMigratorlegacy path, cold-open) for load-before-open + handler coverage.To finish the same-device A/B
Need a genuine 16 KB arm64 environment: the Pixel 10 Pro XL (real 16 KB device;
SqlCipherOpenSpikeTestemits the exact failing stage + cause chain the moment it runs there), or authorization to bootloader-unlock + wipe a spare Pixel 8.Audit verdict: PERSISTS — a fail-closed handler-COVERAGE gap (post-#367)
The
UnsatisfiedLinkError @ SQLiteConnection.nativeOpenstill has a live UNCAUGHT path. The audit ruled out both prior hypotheses:ensureNativeLibraryLoaded()in the same function.catch (LinkageError)correctly catchesUnsatisfiedLinkError.The real bug is the handler's SCOPE: #367's
try/catchinDatabaseProvisioner.runStartupSequence()wraps onlyresolveOpenMode(...), but the crash-prone keyed opens sit OUTSIDE it:AccountDataMigrator.migrateIfNeeded()(DatabaseProvisioner.kt:111, ~12 lines above thetry): keyed SQLCipheropenOrCreateDatabase+ATTACH KEY(AccountDataMigrator.kt:143-144,156) with an empty key even when encryption is OFF → EVERY pre-#111 upgrader hits the native lib; aLinkageErrorthere → uncaught → crash-loop on every launch.nativeOpen(DatabaseModuleDeferredOpenHelperFactory,DatabaseModule.kt:97-106) fires AFTERprepareCache()returns, outside any handler → the exactUnsatisfiedLinkError @ nativeOpensignature.SyncWorker/BackfillWorker/SendWorker/PruneWorker/IdleService) run with NOCacheEncryptionGate→ uncaught there too.Test blind spot:
DatabaseProvisionerTestmocks the migrator asjust Runs, hiding the gap.Fix in progress (widen handler scope to the migrator, keep the
LinkageErrortype; probe the real keyed open inside the fail-closed try; headless tolerance viaEncryptedCacheGuard; regression tests). PR incoming.