A popup save can undo a network switch the background saved while the save was in flight #641

Open
opened 2026-10-20 13:18:49 +02:00 by clawbot · 2 comments
Collaborator

Seen by the worker on #638, as an intermittent failure of the Chrome browser suite on unchanged code.

Where

saveStateOnce() in src/shared/state.js reads the stored record, merges this page's changes into it, and writes it back. Nothing stops another page or the background from writing between that read and that write. The background's network switch after the user approves a site's wallet_switchEthereumChain (updateState() in src/background/) is one such write.

What happens

The popup starts a save (on navigation, or on its ten-second balance refresh) and reads the record. The background then saves the network the user just approved. The popup writes its merged copy, built from the record as it read it, so the network goes back to the one before the switch. In the Chrome suite, after a site's switch back to mainnet resolved, the stored network stayed on Sepolia, and the next test started on Sepolia; a second run on the same commit passed.

What should happen

A save never undoes a change another page or the background made after the save read the record: the network the user approved is the one stored.

Severity: major

Definition of done

  • No write by another page or the background between a save's read and its write is lost.
  • A unit test lands the background's write between the popup's read and write and checks the stored network, failing on next.

Model: opus-5-5

Seen by the worker on https://git.eeqj.de/sneak/AutistMask/pulls/638, as an intermittent failure of the Chrome browser suite on unchanged code. ## Where `saveStateOnce()` in `src/shared/state.js` reads the stored record, merges this page's changes into it, and writes it back. Nothing stops another page or the background from writing between that read and that write. The background's network switch after the user approves a site's `wallet_switchEthereumChain` (`updateState()` in `src/background/`) is one such write. ## What happens The popup starts a save (on navigation, or on its ten-second balance refresh) and reads the record. The background then saves the network the user just approved. The popup writes its merged copy, built from the record as it read it, so the network goes back to the one before the switch. In the Chrome suite, after a site's switch back to mainnet resolved, the stored network stayed on Sepolia, and the next test started on Sepolia; a second run on the same commit passed. ## What should happen A save never undoes a change another page or the background made after the save read the record: the network the user approved is the one stored. Severity: major ## Definition of done - [ ] No write by another page or the background between a save's read and its write is lost. - [ ] A unit test lands the background's write between the popup's read and write and checks the stored network, failing on `next`. Model: opus-5-5
clawbot self-assigned this 2026-10-20 13:18:49 +02:00
Author
Collaborator

Plan: one PR to next. Two writers read, merge and write the stored record, each queued only within its own context: saveStateOnce() in src/shared/state.js (every popup or approval page) and updateStateOnce() in src/background/state.js. Both hold one lock across their read and their write, the browser's own cross-context lock (navigator.locks, shared by every page and the background of the extension), through one helper beside storageGet()/storageSet() in src/shared/browserApi.js, so a write by one can never land between the read and the write of the other. No silent fallback when the lock is missing: the extension runs only where it exists. A Jest test lands the background's network write between the popup's read and write and checks the stored network, failing on next.

Model: opus-5-5

Plan: one PR to `next`. Two writers read, merge and write the stored record, each queued only within its own context: `saveStateOnce()` in `src/shared/state.js` (every popup or approval page) and `updateStateOnce()` in `src/background/state.js`. Both hold one lock across their read and their write, the browser's own cross-context lock (`navigator.locks`, shared by every page and the background of the extension), through one helper beside `storageGet()`/`storageSet()` in `src/shared/browserApi.js`, so a write by one can never land between the read and the write of the other. No silent fallback when the lock is missing: the extension runs only where it exists. A Jest test lands the background's network write between the popup's read and write and checks the stored network, failing on `next`. Model: opus-5-5
Author
Collaborator

Done as planned in #644. Beyond the plan: the per-context queues are removed, since the lock also orders each context's own writes, and the StateRecovery reset takes the lock too.

Model: opus-5-5

Done as planned in https://git.eeqj.de/sneak/AutistMask/pulls/644. Beyond the plan: the per-context queues are removed, since the lock also orders each context's own writes, and the StateRecovery reset takes the lock too. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/AutistMask#641