fix: the restored viewStack can contain views the popup may not reopen onto, giving a dead-end screen #224

Closed
opened 2026-08-11 15:06:25 +02:00 by clawbot · 2 comments
Collaborator

state.viewStack is persisted and restored verbatim in src/shared/state.js. RESTORABLE_VIEWS correctly stops the popup opening ONTO a non-restorable view, but nothing filters those views out of the restored stack itself.

Consequence: a secret-bearing screen such as export-privkey — or the new show-phrase — can sit in the restored stack. The popup opens on a permitted view, the user presses Back, and lands on an empty dead-end screen whose content was deliberately never re-rendered. On show-phrase there is no Back control either, so it is not merely ugly.

To be clear about what this is and is not: no secret leaks. The views render empty because their content is never restored. This is a navigation defect, not a disclosure one.

General to every non-restorable view rather than specific to any screen, which is why it was left out of #215 — fixing it there would have changed export-privkey navigation as a side effect of a feature PR. Found by the independent review of that PR.

Implementation requirements

  • Filter the persisted stack against RESTORABLE_VIEWS in loadState(), so a restored stack contains only views the popup is willing to render.
  • Decide what happens to entries BELOW a dropped view: dropping one entry from the middle silently changes where Back goes. Truncating at the first non-restorable entry is likely more predictable than splicing it out. Pick one, say why in the PR body.
  • Make sure the result is never an empty stack on a view that needs somewhere to go back to.
  • Consider whether the stack should be filtered on save instead of on load; argue for whichever you pick.

Definition of done

  • A persisted stack containing a non-restorable view restores to a stack that does not contain it.
  • Pressing Back after reopening never lands on an unrendered screen.
  • Back behaviour for ordinary restorable stacks is unchanged.
  • Unit tests cover a stack with a non-restorable view at the top, in the middle, and at the bottom, demonstrated failing against the current code first.
  • TODO.md updated in the same commit.
  • make check passes.
`state.viewStack` is persisted and restored verbatim in `src/shared/state.js`. `RESTORABLE_VIEWS` correctly stops the popup opening ONTO a non-restorable view, but nothing filters those views out of the restored stack itself. Consequence: a secret-bearing screen such as `export-privkey` — or the new `show-phrase` — can sit in the restored stack. The popup opens on a permitted view, the user presses Back, and lands on an empty dead-end screen whose content was deliberately never re-rendered. On `show-phrase` there is no Back control either, so it is not merely ugly. To be clear about what this is and is not: no secret leaks. The views render empty because their content is never restored. This is a navigation defect, not a disclosure one. General to every non-restorable view rather than specific to any screen, which is why it was left out of https://git.eeqj.de/sneak/AutistMask/pulls/215 — fixing it there would have changed `export-privkey` navigation as a side effect of a feature PR. Found by the independent review of that PR. ## Implementation requirements - Filter the persisted stack against `RESTORABLE_VIEWS` in `loadState()`, so a restored stack contains only views the popup is willing to render. - Decide what happens to entries BELOW a dropped view: dropping one entry from the middle silently changes where Back goes. Truncating at the first non-restorable entry is likely more predictable than splicing it out. Pick one, say why in the PR body. - Make sure the result is never an empty stack on a view that needs somewhere to go back to. - Consider whether the stack should be filtered on save instead of on load; argue for whichever you pick. ## Definition of done - [ ] A persisted stack containing a non-restorable view restores to a stack that does not contain it. - [ ] Pressing Back after reopening never lands on an unrendered screen. - [ ] Back behaviour for ordinary restorable stacks is unchanged. - [ ] Unit tests cover a stack with a non-restorable view at the top, in the middle, and at the bottom, demonstrated failing against the current code first. - [ ] `TODO.md` updated in the same commit. - [ ] `make check` passes.
clawbot added this to the 1.0.0 milestone 2026-08-11 15:06:25 +02:00
Author
Collaborator

Implemented in #266.

loadState() now truncates the stored stack at the first entry outside RESTORABLE_VIEWS (that entry and everything above it), rather than splicing it out: the result is always a prefix of what was stored, so every surviving entry keeps exactly the Back target it had. Splicing would silently re-point the entry above the hole. If truncation leaves nothing and the restored view is a restorable non-root view, main is put beneath it so Back always has somewhere to go.

Filtered on load, not on save: only a load-side filter repairs the stacks already in storage (including ones written when a view was still in the set), and the live in-session stack is legitimate while the popup is open. saveState() is unchanged.

Verified: the new cases in tests/state.test.js (non-restorable view at top, middle, bottom, plus an unknown view name and the restore-onto-main case) were run against unmodified next first and failed 6 of 6; they pass with the fix. make check green on the rebased branch — 585 tests in 25 suites, test-verify-build 18 cases, prettier clean — and green again inside the container via script/cibuild with docker build --no-cache on this image, where the RUN make check layer executed rather than reporting CACHED. src/popup/restorableViews.js is untouched.

Implemented in [#266](https://git.eeqj.de/sneak/AutistMask/pulls/266). `loadState()` now truncates the stored stack at the first entry outside `RESTORABLE_VIEWS` (that entry and everything above it), rather than splicing it out: the result is always a prefix of what was stored, so every surviving entry keeps exactly the Back target it had. Splicing would silently re-point the entry above the hole. If truncation leaves nothing and the restored view is a restorable non-root view, `main` is put beneath it so Back always has somewhere to go. Filtered on load, not on save: only a load-side filter repairs the stacks already in storage (including ones written when a view was still in the set), and the live in-session stack is legitimate while the popup is open. `saveState()` is unchanged. Verified: the new cases in `tests/state.test.js` (non-restorable view at top, middle, bottom, plus an unknown view name and the restore-onto-`main` case) were run against unmodified `next` first and failed 6 of 6; they pass with the fix. `make check` green on the rebased branch — 585 tests in 25 suites, `test-verify-build` 18 cases, prettier clean — and green again inside the container via `script/cibuild` with `docker build --no-cache` on this image, where the `RUN make check` layer executed rather than reporting `CACHED`. `src/popup/restorableViews.js` is untouched.
Author
Collaborator

Narrowing the second DoD item. "Pressing Back after reopening never lands on an unrendered screen" is not achievable by filtering the stack, and the independent review of #266 proved why: goBack() re-renders nothing, so Back lands on a blank template for ordinary restorable views too — no non-restorable view required. That is a wider, pre-existing defect and is now #268.

This issue's item 2 therefore reads: pressing Back after reopening never lands on a view the popup declined to restore. The general blank-on-Back case belongs to #268 and this unit should not grow to cover it.

Narrowing the second DoD item. "Pressing Back after reopening never lands on an unrendered screen" is not achievable by filtering the stack, and the independent review of https://git.eeqj.de/sneak/AutistMask/pulls/266 proved why: `goBack()` re-renders nothing, so Back lands on a blank template for ordinary restorable views too — no non-restorable view required. That is a wider, pre-existing defect and is now https://git.eeqj.de/sneak/AutistMask/issues/268. This issue's item 2 therefore reads: pressing Back after reopening never lands on a view the popup declined to restore. The general blank-on-Back case belongs to https://git.eeqj.de/sneak/AutistMask/issues/268 and this unit should not grow to cover it.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#224