fix: AutistMask does not run in Firefox private windows (closes #646)
Firefox gives an extension page in a private window locks of its own. A popup there could save between the background's read and its write of the stored record, and undo a network switch the background had saved. manifest/firefox.json now sets "incognito": "not_allowed", so Firefox never runs AutistMask in a private window. tests/manifest.test.js pins the key. The README and five code comments no longer describe a private-window exception to the lock. Model: opus-5-5
This commit is contained in:
@@ -1494,15 +1494,16 @@ 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
|
||||
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
|
||||
write fails rather than going ahead without it.
|
||||
background share it. `manifest/firefox.json` sets `"incognito": "not_allowed"`,
|
||||
so no page runs in a Firefox private window. Firefox would give a page there
|
||||
locks of its own. 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 write
|
||||
fails rather than going ahead without it.
|
||||
|
||||
Version 1 is the shape that shipped before versions existed, so a stored record
|
||||
with no `schemaVersion` is version 1 rather than a defect: it loads normally and
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"version": "0.1.0",
|
||||
"description": "Minimal Ethereum wallet for Firefox",
|
||||
"permissions": ["storage", "activeTab", "alarms", "<all_urls>"],
|
||||
"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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-3
@@ -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
|
||||
|
||||
@@ -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("firefox never runs AutistMask in a private window", () => {
|
||||
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.
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user