fix(security): harden app-lock recovery restart (self-restart race + lost syncNow enqueue) #99

Closed
opened 2026-07-02 02:55:42 +00:00 by JMR-dev · 0 comments
JMR-dev commented 2026-07-02 02:55:42 +00:00 (Migrated from github.com)

Origin: code review of PR #45 (screen-lock app gate). Below-the-cut correctness items — real but lower-severity than the 15 headline fixes being applied on a fix branch.

Problem

The key-invalidation recovery restart is unreliable in two ways:

  • Same-process self-restart race (AppLockViewModel.restartProcess): context.startActivity(launchIntent) is followed immediately by Runtime.getRuntime().exit(0) in the same process. ActivityManager may schedule the relaunch into the process being killed, so the restart is intermittently dropped and the app just closes (this is exactly why ProcessPhoenix uses a separate :process trampoline). Recovery then only happens on the next manual launch (the CLEAR_PENDING flag persists), i.e. a one-time silent app close.
  • Lost syncNow() enqueue (clearCacheAndRestart): syncScheduler.syncNow() is a fire-and-forget WorkManager.enqueueUniqueWork whose WorkSpec is persisted asynchronously on WorkManager's serial task executor; exit(0) races that insert, so the comment "persisted by WorkManager; survives the restart" is not guaranteed. (Currently masked by the account-wipe bug being fixed separately — once accounts survive, the lost re-sync becomes user-visible as an empty mailbox until the next periodic sync.)

Suggested fix

Use a separate-process trampoline (ProcessPhoenix-style) for the restart, or at minimum await the enqueue Operation (with a timeout) before exiting, and issue the restart from a component that survives the killed process. Verify the wipe+resync completes end-to-end on device.

Origin: code review of PR #45 (screen-lock app gate). Below-the-cut correctness items — real but lower-severity than the 15 headline fixes being applied on a fix branch. ## Problem The key-invalidation recovery restart is unreliable in two ways: - **Same-process self-restart race** (`AppLockViewModel.restartProcess`): `context.startActivity(launchIntent)` is followed immediately by `Runtime.getRuntime().exit(0)` in the *same* process. ActivityManager may schedule the relaunch into the process being killed, so the restart is intermittently dropped and the app just closes (this is exactly why ProcessPhoenix uses a separate `:process` trampoline). Recovery then only happens on the next manual launch (the `CLEAR_PENDING` flag persists), i.e. a one-time silent app close. - **Lost `syncNow()` enqueue** (`clearCacheAndRestart`): `syncScheduler.syncNow()` is a fire-and-forget `WorkManager.enqueueUniqueWork` whose WorkSpec is persisted asynchronously on WorkManager's serial task executor; `exit(0)` races that insert, so the comment "persisted by WorkManager; survives the restart" is not guaranteed. (Currently masked by the account-wipe bug being fixed separately — once accounts survive, the lost re-sync becomes user-visible as an empty mailbox until the next periodic sync.) ## Suggested fix Use a separate-process trampoline (ProcessPhoenix-style) for the restart, or at minimum `await` the enqueue `Operation` (with a timeout) before exiting, and issue the restart from a component that survives the killed process. Verify the wipe+resync completes end-to-end on device.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: JMR-dev/LibreMail#99