Skip to content

src: keep per-Environment state out of thread_locals - #66313

Closed
codebytere wants to merge 8 commits into
nodejs:mainfrom
codebytere:src-per-env-thread-locals
Closed

codebytere wants to merge 8 commits into
nodejs:mainfrom
codebytere:src-per-env-thread-locals

Conversation

@codebytere

Copy link
Copy Markdown
Member

Refs: #66239

Stacked on #66239; only the last three commits are new.

Two thread_locals in src/ hold state that belongs to one Environment, so they break when several Environments share a thread:

  • The root cert store: tls.setDefaultCACertificates() in one Environment replaced the trusted CAs of every other Environment on the thread. It now lives on the Environment, next to its other OpenSSL state.
  • The QUIC allocator: its BindingData pointer came from whichever BindingData last handed out an allocator, so a session in one Environment freed memory against another's counter and failed a CHECK. Each BindingData now owns its allocator state. nghttp3 buffers backing external strings can outlive the BindingData, so the state is deleted once the BindingData is gone and the last allocation made through it is freed.

A new cpplint rule rejects thread_local in src/ unless the declaration is marked with NOLINTNEXTLINE(runtime/thread_local). The 11 remaining uses are marked: re-entrancy guards, debug counters, dlopen bookkeeping, the context setup BuiltinLoader and the handle cleanup depth from #66239, all per-thread by design.

Both fixes come with a cctest in test_environment_shared_isolate.cc that fails without them.


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

The FreeEnvironment() fix for sibling Environments keeps the depth of
nested Environment::CleanupHandles() calls on the IsolateData, so that
InternalCallbackScope can re-allow JavaScript for sibling Environments
while one of them is being freed. Environments that each have their own
IsolateData on the same isolate and loop never see that counter and
still fail with "illegal access". Environments that share a loop share a
thread, so keep the depth in a thread_local instead.

Refs: nodejs#65977
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
…out one

An Environment created with kNoCreateInspector threw a bare string from
inspector.Session#connect(), inspector.open() and the other Agent entry
points, so callers could not tell the condition apart by error code. Use
the ERR_INSPECTOR_NOT_AVAILABLE code that connectToMainThread() already
throws for the same situation.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
FreeIsolateData() while an Environment created from it is still alive
left that Environment with a dangling pointer and failed later in
unrelated code. Count the Environments using an IsolateData and CHECK
in its destructor that none are left.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
With the FreeEnvironment() fix for sibling Environments and the handle
cleanup depth tracked per thread, only the Environment being freed
loses JavaScript while FreeEnvironment() runs the shared loop. Callbacks
of the other Environments on that loop run their JavaScript as usual.
Update embedding.md and the comment in node.h, which still describe
JavaScript as disallowed on the whole isolate.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
The root cert store and the certificates set through
tls.setDefaultCACertificates() were thread_local, with a cleanup hook on
whichever Environment used TLS first. When several Environments share a
thread, setting the default CA certificates in one of them replaced the
trusted CAs of the others. Keep both on the Environment, next to its
other OpenSSL state.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
The ngtcp2 and nghttp3 allocators shared one thread_local state whose
BindingData pointer was set by the last BindingData that handed out an
allocator. With several Environments on a thread, memory allocated for
a session in one Environment was accounted against another's
BindingData and failed a CHECK when freed.

Give each BindingData its own heap-allocated state. nghttp3 buffers
backing external strings can be freed after the BindingData is gone,
so the state counts live allocations and is deleted once the
BindingData has been destroyed and the last of them is freed.

Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
Several Environments can share a thread, so state in src/ that belongs
to one of them cannot be kept in a thread_local. Add a cpplint rule
that rejects thread_local in src/ unless the declaration is marked with
NOLINTNEXTLINE(runtime/thread_local), and mark the existing uses, which
are per-thread by design.

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

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto
  • @nodejs/inspector
  • @nodejs/quic

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run. labels Sep 26, 2026
@codebytere codebytere closed this Sep 26, 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++. lib / src Issues and PRs involving general changes in the lib/ or src/ directories. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants