A corrupt lock file can crash at boot, and a lockout that cannot be signed vanishes #64

Closed
opened 2026-08-20 21:22:37 -05:00 by null · 0 comments
Owner

What is true now. core/security/.../AppLockRepository.kt:81 exposes hasPin as store.data.map { ... } with NO .catch, unlike UserPreferencesRepository.preferences (core/datastore/.../UserPreferencesRepository.kt:36) which swallows IOException and emits defaults. It is collected in two places with no handler: AppLockViewModel.kt:85-94 (combine(...).stateIn(viewModelScope, ...)stateIn launches without the CoroutineExceptionHandler declared at :103) and MainActivity.kt:83 (appLock.hasPin.collect(::applySecureFlag) in lifecycleScope). An unreadable or corrupt app_lock file therefore propagates out of both — on the startup path, before any UI.

Separately, writeLockout (:182-189) does runCatching { macOf(blob) }.getOrNull() ?: return — when the counter cannot be signed, the failed attempt is silently not recorded, so wrong PINs stop costing time.

What to do. Give the lock flows the same .catch { if (it is IOException) emit(empty) else throw it } treatment, add a ReplaceFileCorruptionHandler to the app_lock DataStore, and route MainActivity/AppLockViewModel through flows that cannot throw. Decide and document the safe reading of a corrupt lock file: it should read as NO LOCK (the app opens) rather than as a lock nobody can open — the same policy VerifierRecord.decode already applies to a corrupt record (:73-82), on the reasoning that whoever can corrupt that file can read the database anyway, and the alternative bricks the app with the erase hatch also broken. For the lockout write, fail closed for the process rather than silently forgetting the attempt.

Traps. Without the corruption handler, a corrupt file makes every later edit fail too — the lock can never be set again and the 'Forgot your PIN?' erase itself fails. Do not log anything on these paths: app and core/security are both in modulesSeeingHealthData and checkNoHealthLogging forbids every logging call.

Verify: an unreadable lock file reads as off rather than crashing and a corrupt lock file is replaced so a lock can be set again in AppLockRepositoryTest, plus a failed attempt still costs time in this process when its counter cannot be signed.

**What is true now.** `core/security/.../AppLockRepository.kt:81` exposes `hasPin` as `store.data.map { ... }` with NO `.catch`, unlike `UserPreferencesRepository.preferences` (`core/datastore/.../UserPreferencesRepository.kt:36`) which swallows `IOException` and emits defaults. It is collected in two places with no handler: `AppLockViewModel.kt:85-94` (`combine(...).stateIn(viewModelScope, ...)` — `stateIn` launches without the `CoroutineExceptionHandler` declared at `:103`) and `MainActivity.kt:83` (`appLock.hasPin.collect(::applySecureFlag)` in `lifecycleScope`). An unreadable or corrupt `app_lock` file therefore propagates out of both — on the startup path, before any UI. Separately, `writeLockout` (`:182-189`) does `runCatching { macOf(blob) }.getOrNull() ?: return` — when the counter cannot be signed, the failed attempt is silently not recorded, so wrong PINs stop costing time. **What to do.** Give the lock flows the same `.catch { if (it is IOException) emit(empty) else throw it }` treatment, add a `ReplaceFileCorruptionHandler` to the `app_lock` DataStore, and route `MainActivity`/`AppLockViewModel` through flows that cannot throw. Decide and document the safe reading of a corrupt lock file: it should read as NO LOCK (the app opens) rather than as a lock nobody can open — the same policy `VerifierRecord.decode` already applies to a corrupt record (`:73-82`), on the reasoning that whoever can corrupt that file can read the database anyway, and the alternative bricks the app with the erase hatch also broken. For the lockout write, fail closed for the process rather than silently forgetting the attempt. **Traps.** Without the corruption handler, a corrupt file makes every later `edit` fail too — the lock can never be set again and the 'Forgot your PIN?' erase itself fails. Do not log anything on these paths: `app` and `core/security` are both in `modulesSeeingHealthData` and `checkNoHealthLogging` forbids every logging call. Verify: `an unreadable lock file reads as off rather than crashing` and `a corrupt lock file is replaced so a lock can be set again` in `AppLockRepositoryTest`, plus `a failed attempt still costs time in this process when its counter cannot be signed`.
null added this to the Batch 11 — Settings and the App Lock milestone 2026-08-20 21:22:37 -05:00
null added the
P1
label 2026-08-20 21:22:37 -05:00
null closed this issue 2026-08-20 21:53:56 -05:00
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#64
No description provided.