diff --git a/README.md b/README.md index 2e64716..f53e5b3 100644 --- a/README.md +++ b/README.md @@ -1494,11 +1494,12 @@ wrote last, the record says which build's shape it is in. Each write reads the record, changes it and writes it back, holding a lock from the read to the write: the browser's own (`navigator.locks`), taken through `withLock()` in `src/shared/browserApi.js`. Every page of the extension and its -background share it, except a page in a Firefox private window, which Firefox -gives locks of its own (see https://git.eeqj.de/sneak/AutistMask/issues/646). -The popup's `saveState()`, the background's `updateState()` and the -StateRecovery reset all take it, so among the pages and the background that -share it, no write lands between another write's read and its write, where it +background share it, except a page in a Firefox private window, which has locks +of its own. `manifest/firefox.json` sets `"incognito": "not_allowed"`, so such a +page cannot read or write extension storage. In Chrome, a page opened from an +incognito window still runs outside incognito and shares the lock. The popup's +`saveState()`, the background's `updateState()` and the StateRecovery reset all +take it, so no write lands between another write's read and its write, where it would be lost. Chrome has it from version 69, older than any Chrome that runs a Manifest V3 extension, in pages and in the service worker; Firefox from version 96, older than the 115 `manifest/firefox.json` declares. Where it is missing, a diff --git a/docs/README.md b/docs/README.md index 8349567..4c0cccd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -208,6 +208,9 @@ password. 3. Click "Load Temporary Add-on". 4. Select `dist/firefox/manifest.json`. +AutistMask does not work in Firefox private windows. If Firefox is set to "Never +remember history", every window is private, so AutistMask does not work at all. + ## Getting Started ### Creating a New Wallet diff --git a/manifest/firefox.json b/manifest/firefox.json index 74b6e6f..08d8c6a 100644 --- a/manifest/firefox.json +++ b/manifest/firefox.json @@ -4,6 +4,7 @@ "version": "0.1.0", "description": "Minimal Ethereum wallet for Firefox", "permissions": ["storage", "activeTab", "alarms", ""], + "incognito": "not_allowed", "content_security_policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'", "icons": { "16": "icons/icon16.png", diff --git a/src/background/state.js b/src/background/state.js index 4f29804..3710d37 100644 --- a/src/background/state.js +++ b/src/background/state.js @@ -18,11 +18,10 @@ // updateState(fn) read fresh, apply fn to that fresh record, write it // back — all holding the lock every write of the stored // record takes, so no other write lands in between, by -// the background or by any page except a page in a -// Firefox private window (README, "Stored state and its -// version"). The read is one storage round trip ahead of -// the write rather than a page lifetime ahead of it -// (which is what made the popup's saveState() need a +// the background or by any page (README, "Stored state +// and its version"). The read is one storage round trip +// ahead of the write rather than a page lifetime ahead of +// it (which is what made the popup's saveState() need a // per-field merge against a baseline at all). // // A handler that must both read and write therefore does its network work @@ -73,8 +72,7 @@ async function updateStateOnce(mutate) { // src/shared/browserApi.js) is held from the read to the write: two // background turns, or a turn and a page's save, never interleave // (https://git.eeqj.de/sneak/AutistMask/issues/641). Every page's save waits -// while it is held, except a page in a Firefox private window, which Firefox -// gives locks of its own (README, "Stored state and its version"). +// while it is held (README, "Stored state and its version"). // // `mutate` must also not call updateState() itself, directly or through // anything it awaits: the inner turn waits for the lock the outer one holds, diff --git a/src/popup/views/stateRecovery.js b/src/popup/views/stateRecovery.js index 5449fed..0d56f5b 100644 --- a/src/popup/views/stateRecovery.js +++ b/src/popup/views/stateRecovery.js @@ -143,8 +143,6 @@ async function resetProfile() { try { // Under the lock every write of the stored record takes, so the erase // cannot land between another write's read and its write and be undone. - // A page in a Firefox private window is the exception: Firefox gives - // it locks of its own (README, "Stored state and its version"). await withLock("autistmask", () => storageRemove("autistmask")); } catch (e) { log.errorf("state recovery: reset failed:", e); diff --git a/src/shared/state.js b/src/shared/state.js index 407c18f..227763c 100644 --- a/src/shared/state.js +++ b/src/shared/state.js @@ -581,9 +581,7 @@ function reportSaveFailure(err) { // Each save runs start to finish holding the lock every write of the stored // record takes (withLock() in src/shared/browserApi.js), so no write by // another page or the background lands between its read and its write and is -// lost (https://git.eeqj.de/sneak/AutistMask/issues/641). A page in a Firefox -// private window is the exception: Firefox gives it locks of its own (README, -// "Stored state and its version"). +// lost (https://git.eeqj.de/sneak/AutistMask/issues/641). // // The same lock orders this page's own saves. showView() calls saveState() on // every navigation without awaiting it, so two saves from one page can be diff --git a/tests/manifest.test.js b/tests/manifest.test.js index af5cdf0..f00f55d 100644 --- a/tests/manifest.test.js +++ b/tests/manifest.test.js @@ -165,6 +165,15 @@ describe("declared icons", () => { }); }); +// Firefox gives a page in a private window locks of its own, so that page's +// save could land between the background's read and its write (README, +// "Stored state and its version"). +describe("private windows", () => { + test("a page in a firefox private window cannot read or write extension storage", () => { + expect(readManifest("firefox").incognito).toBe("not_allowed"); + }); +}); + describe("shipped Content Security Policy", () => { // MV3 takes an object and applies extension_pages to the popup and the // background service worker, which is where libsodium runs. diff --git a/tests/stateWriteLock.test.js b/tests/stateWriteLock.test.js index 041950c..3b37f9b 100644 --- a/tests/stateWriteLock.test.js +++ b/tests/stateWriteLock.test.js @@ -6,9 +6,8 @@ // now holds a lock from its read to its write. // // The popup and the background run here as the real modules over one shared -// storage stub, and share the lock the way the background and a page outside -// a Firefox private window share the browser's -// (tests/support/navigatorLocks.js). +// storage stub, and share the lock the way the background and its pages share +// the browser's (tests/support/navigatorLocks.js). const { makeStorageStub } = require("./support/storageStub"); diff --git a/tests/support/navigatorLocks.js b/tests/support/navigatorLocks.js index f24f1b4..4722749 100644 --- a/tests/support/navigatorLocks.js +++ b/tests/support/navigatorLocks.js @@ -4,9 +4,8 @@ // the callback settles, whether it resolved or rejected. That is what the // browser's own does for the one call the extension makes, // `navigator.locks.request(name, callback)`. Every module a test file loads -// shares it, as the extension's background and its pages share the browser's, -// except a page in a Firefox private window (README, "Stored state and its -// version"). +// shares it, as the extension's background and its pages share the browser's +// (README, "Stored state and its version"). // // Run before each test file by `setupFiles` in package.json.