fix: version stored state, validate its shape, and give a corrupt blob a way out (closes #311)
Stored state had no version and no structural validation, so a corrupt blob produced a completely blank popup with no message and no recovery control, and made every dApp RPC call from every page answer a generic -32603. There was no reset or wipe control anywhere in the UI. saveState() now stamps a schema version and loadState() validates the shape. A version it does not understand, or a wallets array it cannot parse, lands on a recovery screen that names the problem, offers the stored record verbatim for export, and offers a destructive reset behind a typed confirmation. Unversioned but valid state -- which every existing install has -- migrates in place and keeps working; it is never shown a wipe prompt. A dApp call against unusable state answers -32007, which EIP-1474 leaves unassigned, rather than -32603. networkById() refuses an unknown id loudly instead of returning mainnet, and networkId is validated so a corrupt value cannot be used as an object key. Fields the gate does not refuse are floored by type, container and entries both: a malformed trackedTokens or tokenBalances entry is dropped rather than dereferenced. Verified by an independent sweep of 1152 corrupt blobs producing no blank popup, with the same harness showing 9 blanks against the previous revision.
This commit was merged in pull request #360.
This commit is contained in:
@@ -29,6 +29,12 @@ const {
|
||||
normalizePersisted,
|
||||
} = require("./persistedState");
|
||||
|
||||
const {
|
||||
STATE_SCHEMA_VERSION,
|
||||
assertStateUsable,
|
||||
migrationNeeded,
|
||||
} = require("./stateSchema");
|
||||
|
||||
const { storageGet, storageSet } = require("./browserApi");
|
||||
const { log } = require("./log");
|
||||
|
||||
@@ -446,6 +452,14 @@ function mergeNetworkEndpoints(base, ours, theirs) {
|
||||
async function saveStateOnce() {
|
||||
const current = snapshotPersisted();
|
||||
const result = await storageGet("autistmask");
|
||||
// The record in storage right now is about to be merged into and written
|
||||
// back, so it is validated exactly like a load validates it. Without this,
|
||||
// a page whose own load succeeded would normalize a record it does not
|
||||
// understand — one a NEWER build wrote in the meantime, say — and write
|
||||
// the result back over it, destroying the only copy of whatever that
|
||||
// record held. Refusing is louder than that and loses nothing: the live
|
||||
// state is untouched and the next save retries.
|
||||
assertStateUsable(result.autistmask);
|
||||
// Normalized, not raw: a field this page did not change still has to
|
||||
// come from storage in its loaded (self-healed) shape. See
|
||||
// normalizePersisted() in persistedState.js.
|
||||
@@ -481,6 +495,10 @@ async function saveStateOnce() {
|
||||
}
|
||||
}
|
||||
merged.hasWallet = Boolean(merged.wallets && merged.wallets.length > 0);
|
||||
// Stamped on every write, never merged or diffed: the record that goes to
|
||||
// storage is in THIS build's shape whatever shape it was read in, which is
|
||||
// what migrates the unversioned records every install in the field holds.
|
||||
merged.schemaVersion = STATE_SCHEMA_VERSION;
|
||||
|
||||
await storageSet({ autistmask: merged });
|
||||
|
||||
@@ -512,8 +530,24 @@ function saveState() {
|
||||
return turn;
|
||||
}
|
||||
|
||||
// Rejects with StateUnusableError for a stored record this build cannot make
|
||||
// sense of. Nothing is assigned and `loaded` stays false in that case, so a
|
||||
// caller that ignores the rejection gets StateNotLoadedError on the first
|
||||
// read rather than a half-populated profile. The caller that does NOT ignore
|
||||
// it is the popup entry point, which shows the recovery screen
|
||||
// (src/popup/views/stateRecovery.js) instead of proceeding.
|
||||
async function loadState() {
|
||||
const result = await storageGet("autistmask");
|
||||
// Before normalization, on the raw bytes: normalizing first would paper
|
||||
// over the very shapes this refuses, which is how a corrupt record used to
|
||||
// reach the popup and blank it (issue #311).
|
||||
assertStateUsable(result.autistmask);
|
||||
if (migrationNeeded(result.autistmask)) {
|
||||
log.infof(
|
||||
"state: migrating an unversioned profile to schema version",
|
||||
STATE_SCHEMA_VERSION,
|
||||
);
|
||||
}
|
||||
if (result.autistmask) {
|
||||
Object.assign(rawState, normalizePersisted(result.autistmask));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user