Skip to content

src: throw on a malformed localStorage file - #65879

Open
TrevorBurnham wants to merge 2 commits into
nodejs:mainfrom
TrevorBurnham:webstorage-throw-on-malformed-file
Open

src: throw on a malformed localStorage file#65879
TrevorBurnham wants to merge 2 commits into
nodejs:mainfrom
TrevorBurnham:webstorage-throw-on-malformed-file

Conversation

@TrevorBurnham

@TrevorBurnham TrevorBurnham commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Fixes: #65878
Fixes: #64640

src/node_webstorage.cc asserted the SQLite type of every column it read. But the backing file is a user-specified path, and the schema is created with CREATE TABLE IF NOT EXISTS, so a file that already contains tables of those names is adopted as-is and its values may have any type. A wrong-typed schema_version is the worst case: the assertion is in Storage::Open(), so any access aborts and the application can't inspect or repair the file first.

This PR makes localStorage methods throw ERR_INVALID_STATE in that scenario:

$ node --localstorage-file=/tmp/ws.db -e "localStorage.length"
Error: localStorage database is malformed: expected schema_version to be an integer
  code: 'ERR_INVALID_STATE'

Making these paths non-fatal exposed three further problems, all fixed here:

  • The connection leaked on every failed open. Open() only adopted the sqlite3* into db_ at the very end, so an early return dropped it, and because db_ stayed null the next access opened another. The old CHECK aborted on the first attempt, so this never accumulated; now a try { localStorage.length } catch {} loop leaked two descriptors per iteration until the limit was exhausted and the error degraded into unable to open database file. This is Web Storage Maybe leaks SQLite connections when database initialization fails #64640.
  • Storage::GetAll() ignored errors. It checked neither the result of sqlite3_prepare_v2() nor the status its row loop ended on, so a pre-existing table missing a column, or a corrupt read mid-scan, was reported to the inspector as an empty store while every JavaScript accessor threw for the same file. Both now return std::nullopt.
  • A remote debugger still aborted, which is the second commit. Protocol messages from a remote frontend are dispatched from a libuv callback with no HandleScope on the stack, inside the SealHandleScope that MainThreadInterface::DispatchMessages() installs, so throwing from Open() was fatal: FATAL ERROR: v8::HandleScope::CreateHandle() Cannot create a handle without a HandleScope. Every Open() failure was affected, including a --localstorage-file that names a directory, so this did not need a malformed file to reach. getWebStorage() already opens a HandleScope and a TryCatch for its own handle use; the GetAll() call now does the same.

Storage::GetAll() reports failure with std::optional rather than throwing, because its only caller is the inspector agent and a pending exception there has no JavaScript to propagate to. Storage::Length() keeps its CHECK: its query is SELECT count(*), which is always an integer.

Tests

New tests cover the four JavaScript-reachable assertions and the descriptor leak in test-webstorage.js, plus both inspector failures in a new test-inspector-dom-storage-malformed.js.

Out of scope

One possible follow-up: THROW_SQLITE_ERROR uses sqlite3_errstr(code), so a prepare failure reports SQL logic error rather than no such column: schema_version. Switching it to sqlite3_errmsg(db) would improve every throw in the file.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/inspector

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. labels Sep 7, 2026
@TrevorBurnham TrevorBurnham changed the title webstorage: throw on a malformed backing file src: throw on a malformed localStorage file Sep 7, 2026
@TrevorBurnham
TrevorBurnham force-pushed the webstorage-throw-on-malformed-file branch 3 times, most recently from 881d7fc to 2f08679 Compare September 7, 2026 21:26
The localStorage backing file is a user-specified path, and the schema
is created with CREATE TABLE IF NOT EXISTS, so a file that already
contains tables of those names is adopted as-is. Its stored values may
then have any SQLite type, but every read asserted the expected type
with CHECK, so a wrong-typed value aborted the process. A bad
schema_version was the worst case: that assertion is in
Storage::Open(), so any access aborted and the application had no
chance to inspect or repair the file.

Report these as ERR_INVALID_STATE instead, matching the throw four
lines below the schema_version assertion for a version that is too new.
Storage::GetAll() has no JavaScript caller to throw at, so it returns
std::nullopt and the DOM storage inspector agent reports a protocol
error.

Now that a failed open returns instead of aborting, Open() has to clean
up after itself: adopt the sqlite3* into a conn_unique_ptr immediately,
so that an error does not leak the connection and leave the next access
to open another one.

Storage::GetAll() also ignored the result of sqlite3_prepare_v2() and
the status its row loop ended on, reporting a malformed file or a
mid-scan error as an empty store. Both now return std::nullopt.

Also drop a redundant second sqlite3_exec() of the init SQL that
clobbered the result of the sqlite3_prepare_v2() above it, hiding
prepare failures behind a misleading "bad parameter or other API
misuse".

Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: Claude Opus 5
A protocol message from a remote frontend is dispatched from a libuv
callback with no HandleScope on the stack, inside the SealHandleScope
that MainThreadInterface::DispatchMessages() installs. Opening the
localStorage backing file can throw, so allocating the error object was
fatal:

  FATAL ERROR: v8::HandleScope::CreateHandle()
  Cannot create a handle without a HandleScope

Every Storage::Open() failure was affected, including a
--localstorage-file that names a directory, so this did not need a
malformed file to reach. getWebStorage() already opens a HandleScope
and a TryCatch for its own handle use; do the same around the GetAll()
call and report the failure as a protocol error.

Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Assisted-by: Claude Opus 5
@TrevorBurnham
TrevorBurnham force-pushed the webstorage-throw-on-malformed-file branch from 2f08679 to 43e1abd Compare September 7, 2026 22:26
@TrevorBurnham
TrevorBurnham marked this pull request as ready for review September 7, 2026 22:41
@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.18%. Comparing base (9f7ae86) to head (43e1abd).
⚠️ Report is 15 commits behind head on main.

Files with missing lines Patch % Lines
src/inspector/dom_storage_agent.cc 14.28% 10 Missing and 2 partials ⚠️
src/node_webstorage.cc 50.00% 4 Missing and 4 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65879      +/-   ##
==========================================
- Coverage   90.19%   90.18%   -0.02%     
==========================================
  Files         771      771              
  Lines      264911   265116     +205     
  Branches    50323    50358      +35     
==========================================
+ Hits       238939   239087     +148     
- Misses      16922    16978      +56     
- Partials     9050     9051       +1     
Files with missing lines Coverage Δ
src/node_webstorage.h 83.33% <ø> (ø)
src/node_webstorage.cc 77.64% <50.00%> (+2.70%) ⬆️
src/inspector/dom_storage_agent.cc 81.41% <14.28%> (-4.91%) ⬇️

... and 32 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@trivikr trivikr added sqlite Issues and PRs related to the SQLite subsystem. inspector Issues and PRs related to the V8 inspector protocol. labels Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. inspector Issues and PRs related to the V8 inspector protocol. needs-ci PRs that need a full CI run. sqlite Issues and PRs related to the SQLite subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

localStorage: a malformed backing file aborts the process via CHECK Web Storage Maybe leaks SQLite connections when database initialization fails

3 participants