Skip to content

src: keep the first snapshot blob alive for later isolates - #65779

Open
codebytere wants to merge 2 commits into
nodejs:mainfrom
codebytere:fix/embedder-snapshot-first-params-uaf
Open

src: keep the first snapshot blob alive for later isolates#65779
codebytere wants to merge 2 commits into
nodejs:mainfrom
codebytere:fix/embedder-snapshot-first-params-uaf

Conversation

@codebytere

@codebytere codebytere commented Sep 4, 2026

Copy link
Copy Markdown
Member

An embedder that creates a CommonEnvironmentSetup from an EmbedderSnapshotData, tears it down, releases the snapshot data and later creates a second setup hands V8 freed memory, because every isolate in a process is created from the first isolate's snapshot blob and NewIsolate() only kept a pointer to the first caller's CreateParams; ASAN reports a use-after-free in Snapshot::Initialize. A second commit fixes a race in the same first-isolate setup: two threads creating their first isolate at once could both finalize the external reference list.

NewIsolate() now records the first blob and external references under a mutex and ~SnapshotData() leaves that one blob allocated; the external reference list is finalized once in a function-local static. Nothing is copied and node itself is unaffected. node.h now states the lifetime snapshot_data needs.

Tests: test/embedding/test-embedding-snapshot-twice.js (use-after-free under ASAN before) and EnvironmentTest.CollectExternalReferencesFromSeveralThreads (TSAN before); the rest of test/embedding passes.

Refs: #45885
Refs: #32984


Disclosure: the code, tests and this description were written by Claude Code, directed and reviewed by @codebytere.

`NewIsolate()` creates every isolate from the snapshot blob the first
isolate in the process used, because V8 shares the read-only heap
between isolates, and did so by keeping a pointer to the first
`CreateParams`. When that blob came from an `EmbedderSnapshotData` the
embedder had since released, e.g. a second
`CommonEnvironmentSetup::CreateFromSnapshot()` after the first setup and
its snapshot were destroyed, V8 deserialized freed memory.

Record the first blob and external references under a mutex instead of
copying the caller's `CreateParams`, and make `~SnapshotData()` leave
that one blob allocated, since its owner can go away before the last
isolate is created. Nothing is copied and `node` itself is unaffected.
embedtest grows an `--embedder-run-twice` switch so the sequence can be
tested.

Refs: nodejs#45885
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/startup

@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 4, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.21%. Comparing base (2befec5) to head (0cda701).
⚠️ Report is 119 commits behind head on main.

Files with missing lines Patch % Lines
src/node_snapshotable.cc 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65779      +/-   ##
==========================================
+ Coverage   89.99%   90.21%   +0.21%     
==========================================
  Files         757      771      +14     
  Lines      257739   264638    +6899     
  Branches    48881    50233    +1352     
==========================================
+ Hits       231961   238730    +6769     
- Misses      16861    16914      +53     
- Partials     8917     8994      +77     
Files with missing lines Coverage Δ
src/api/environment.cc 79.16% <100.00%> (+0.60%) ⬆️
src/node.h 91.66% <ø> (ø)
src/node_internals.h 80.35% <ø> (ø)
src/node_snapshotable.cc 73.61% <80.00%> (+0.08%) ⬆️

... and 127 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.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Two threads creating their first isolate at the same time (two
`CommonEnvironmentSetup`s on their own threads, or an embedder's setup
racing a Worker) could corrupt or misread the external reference list
handed to V8: `SnapshotBuilder::CollectExternalReferences()` creates its
registry in a thread-safe function static, but then calls
`external_references()` on every call, and that method appends the
terminating nullptr and flips `is_finalized_` the first time through
without any locking, so both threads can append, or one can read the
vector while the other reallocates it. TSAN reports it for any two
concurrent setups.

Keep the finalized list in a second function static so finalization
runs exactly once, under that static's initialization guard.

Refs: nodejs#32984
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@panva panva added the author ready PRs with CI started, the required approvals, and no outstanding review comments. label Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

author ready PRs with CI started, the required approvals, and no outstanding review comments. c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants