Skip to content

Commit 5dca62f

Browse files
test: cover DOMStorage reads from V8 interrupts
Read both stores over a remote session while the main thread runs JavaScript, so that the messages are dispatched from a V8 API interrupt: stores the application never touched, both stores in memory with sessionStorage created first, and a malformed localStorage file. The script counts calls to the storage getters and to an Error.prototype.code setter, so the test fails if a read calls into JavaScript even when V8 does not enforce it. Signed-off-by: Christian Aurich Zanettini Martins <christian.aurichzm@gmail.com>
1 parent c43f994 commit 5dca62f

2 files changed

Lines changed: 141 additions & 5 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
'use strict';
2+
const common = require('../common');
3+
common.skipIfSQLiteMissing();
4+
common.skipIfInspectorDisabled();
5+
6+
const assert = require('assert');
7+
const { spawnSync } = require('child_process');
8+
const { join } = require('path');
9+
const { DatabaseSync } = require('node:sqlite');
10+
const { NodeInstance } = require('../common/inspector-helper.js');
11+
const tmpdir = require('../common/tmpdir');
12+
tmpdir.refresh();
13+
14+
// While JavaScript runs, a message from a remote frontend is dispatched from a
15+
// V8 API interrupt, where JavaScript must not run. Reading a store has to work
16+
// from there, whether or not the application has touched it yet. The script
17+
// counts calls to the storage getters and to a setter that a JavaScript error
18+
// created by Node.js would hit, and reports the count once the reads are done.
19+
async function getItemsWhileBusy(localStorageFile, prelude, storageIds) {
20+
const script = `
21+
globalThis.jsCalls = 0;
22+
for (const name of ['localStorage', 'sessionStorage']) {
23+
const desc = Object.getOwnPropertyDescriptor(globalThis, name);
24+
Object.defineProperty(globalThis, name, {
25+
...desc,
26+
get() { globalThis.jsCalls++; return desc.get.call(this); },
27+
});
28+
}
29+
Object.defineProperty(Error.prototype, 'code', {
30+
set() { globalThis.jsCalls++; }, configurable: true,
31+
});
32+
${prelude}
33+
globalThis.jsCalls = 0;
34+
console.log('Ready');
35+
const deadline = Date.now() + ${common.platformTimeout(10000)};
36+
while (!globalThis.done && Date.now() < deadline);
37+
`;
38+
const child = new NodeInstance([
39+
'--inspect-brk=0',
40+
'--experimental-storage-inspection',
41+
`--localstorage-file=${localStorageFile}`,
42+
], script);
43+
const session = await child.connectInspectorSession();
44+
await session.send({ method: 'NodeRuntime.enable' });
45+
await session.waitForNotification('NodeRuntime.waitingForDebugger');
46+
await session.send([
47+
{ 'method': 'Runtime.enable' },
48+
{ 'method': 'Debugger.enable' },
49+
]);
50+
await session.send({ method: 'Runtime.runIfWaitingForDebugger' });
51+
await session.send({ method: 'NodeRuntime.disable' });
52+
await session.waitForNotification('Debugger.paused', 'Break on start');
53+
await session.send({ 'method': 'Debugger.resume' });
54+
await session.waitForConsoleOutput('log', ['Ready']);
55+
// Enabled late, so items set by the prelude have to be read from the stores.
56+
await session.send({ method: 'DOMStorage.enable' });
57+
58+
const { storageKey } = await session.send({ method: 'Storage.getStorageKey' });
59+
const results = [];
60+
for (const isLocalStorage of storageIds) {
61+
results.push(await session.send({
62+
method: 'DOMStorage.getDOMStorageItems',
63+
params: { storageId: { isLocalStorage, securityOrigin: '', storageKey } },
64+
}).catch((err) => err));
65+
}
66+
67+
const { result } = await session.send({
68+
method: 'Runtime.evaluate',
69+
params: { expression: 'globalThis.done = true; globalThis.jsCalls' },
70+
});
71+
assert.strictEqual(result.value, 0);
72+
await session.waitForDisconnect();
73+
assert.strictEqual((await child.expectShutdown()).exitCode, 0);
74+
return results;
75+
}
76+
77+
// Node's own tables are created with IF NOT EXISTS, so a file that already
78+
// holds tables of those names is adopted as-is, wrong types included.
79+
function malformedLocalStorage(name) {
80+
const file = join(tmpdir.path, name);
81+
const db = new DatabaseSync(file);
82+
db.exec(`
83+
CREATE TABLE nodejs_webstorage(
84+
key BLOB NOT NULL, value BLOB NOT NULL, PRIMARY KEY(key)
85+
);
86+
CREATE TABLE nodejs_webstorage_state(
87+
max_size INTEGER NOT NULL DEFAULT 10485760,
88+
total_size INTEGER NOT NULL,
89+
schema_version INTEGER NOT NULL DEFAULT 1,
90+
single_row_ INTEGER NOT NULL DEFAULT 1 CHECK(single_row_ = 1),
91+
PRIMARY KEY(single_row_)
92+
);
93+
INSERT INTO nodejs_webstorage_state (total_size, schema_version)
94+
VALUES (0, 'one');
95+
`);
96+
db.close();
97+
return file;
98+
}
99+
100+
async function runTest() {
101+
// Stores the application never touched, with localStorage written by an
102+
// earlier run.
103+
const file = join(tmpdir.path, 'storage.db');
104+
const { status } = spawnSync(process.execPath, [
105+
`--localstorage-file=${file}`,
106+
'-e', 'localStorage.setItem("l", "local")',
107+
]);
108+
assert.strictEqual(status, 0);
109+
const [local, session] = await getItemsWhileBusy(file, '', [true, false]);
110+
assert.deepStrictEqual(local.entries, [['l', 'local']]);
111+
assert.deepStrictEqual(session.entries, []);
112+
113+
// Both stores in memory and never touched.
114+
const untouched = await getItemsWhileBusy(':memory:', '', [true, false]);
115+
assert.deepStrictEqual(untouched.map((r) => r.entries), [[], []]);
116+
117+
// Both stores in memory, sessionStorage created first.
118+
const [memLocal, memSession] = await getItemsWhileBusy(':memory:', `
119+
sessionStorage.setItem('s', 'session');
120+
localStorage.setItem('l', 'local');
121+
`, [true, false]);
122+
assert.deepStrictEqual(memLocal.entries, [['l', 'local']]);
123+
assert.deepStrictEqual(memSession.entries, [['s', 'session']]);
124+
125+
// A file that cannot be opened is reported without creating a JavaScript
126+
// error.
127+
for (const [name, touch] of [['untouched.db', ''],
128+
['touched.db', 'globalThis.localStorage;']]) {
129+
const [err] = await getItemsWhileBusy(
130+
malformedLocalStorage(name), touch, [true]);
131+
assert.strictEqual(
132+
err.message,
133+
'Could not read DOM storage items: Uncaught Error: localStorage ' +
134+
'database is malformed: expected schema_version to be an integer');
135+
}
136+
}
137+
138+
runTest().then(common.mustCall());

‎test/parallel/test-inspector-dom-storage-malformed.js‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,14 @@ async function getDOMStorageItems(localStorageFile) {
8383
{ message: 'Could not read DOM storage items: the backing file is malformed' },
8484
);
8585

86-
// A wrong-typed schema_version makes Storage::Open() throw, which has to be
87-
// caught rather than left pending on an isolate with no JavaScript running.
88-
// Its message reaches the frontend.
86+
// A wrong-typed schema_version keeps the store from opening, and the reason
87+
// reaches the frontend.
8988
await assert.rejects(
9089
getDOMStorageItems(
9190
malformedLocalStorage(
9291
'bad-schema-version.db', 'one', Buffer.from('hello', 'utf16le'))),
9392
{
94-
// The reason comes off the v8::Message, hence the "Uncaught" prefix;
95-
// converting the exception itself would run user JavaScript.
93+
// Formatted like a v8::Message, hence the "Uncaught Error" prefix.
9694
message: 'Could not read DOM storage items: Uncaught Error: ' +
9795
'localStorage database is malformed: expected schema_version to be ' +
9896
'an integer',

0 commit comments

Comments
 (0)