Commit 06c6e36ee5e for nodejs
commit 06c6e36ee5eca6f8d5d4d86eb1fce4c351e0eb91
Author: Shelley Vohr <shelley.vohr@gmail.com>
Date: Sat Sep 26 10:50:59 2026 +0000
tools: lint thread_local in src
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>
PR-URL: https://github.com/nodejs/node/pull/66411
Refs: https://github.com/nodejs/node/pull/66239
Reviewed-By: Anna Henningsen <anna@addaleax.net>
diff --git a/src/api/environment.cc b/src/api/environment.cc
index f188b01b831..4a7b70168e6 100644
--- a/src/api/environment.cc
+++ b/src/api/environment.cc
@@ -1048,6 +1048,7 @@ Maybe<void> InitializePrimordials(Local<Context> context,
// in the first place. However, creating BuiltinLoader instances is
// relatively cheap and all the scripts that we may want to run at
// startup are always present in it.
+ // NOLINTNEXTLINE(runtime/thread_local)
thread_local builtins::BuiltinLoader builtin_loader;
// Primordials can always be just eagerly compiled.
builtin_loader.SetEagerCompile();
diff --git a/src/env.cc b/src/env.cc
index e96b6e6bb12..2da45b82f1a 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -1472,6 +1472,7 @@ void Environment::ClosePerEnvHandles() {
close_and_finish(reinterpret_cast<uv_handle_t*>(&task_queues_async_));
}
+// NOLINTNEXTLINE(runtime/thread_local)
thread_local int handle_cleanup_depth = 0;
void Environment::CleanupHandles() {
diff --git a/src/node_binding.cc b/src/node_binding.cc
index dd4733372e3..3c7dee9bb33 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -179,6 +179,7 @@ struct dl_wrap {
static Mutex dlhandles_mutex;
static std::unordered_set<dl_wrap*, dl_wrap::hash, dl_wrap::equal>
dlhandles;
+// NOLINTNEXTLINE(runtime/thread_local)
static thread_local std::string dlerror_storage;
char* wrapped_dlerror() {
@@ -285,6 +286,7 @@ using v8::Value;
// Globals per process
static node_module* modlist_internal;
static node_module* modlist_linked;
+// NOLINTNEXTLINE(runtime/thread_local)
static thread_local node_module* thread_local_modpending;
// This is set by node::Init() which is used by embedders
diff --git a/src/node_debug.cc b/src/node_debug.cc
index c995e791f25..8efb049a76f 100644
--- a/src/node_debug.cc
+++ b/src/node_debug.cc
@@ -24,8 +24,10 @@ using v8::Number;
using v8::Object;
using v8::Value;
+// NOLINTNEXTLINE(runtime/thread_local)
thread_local std::unordered_map<FastStringKey, int, FastStringKey::Hash>
generic_usage_counters;
+// NOLINTNEXTLINE(runtime/thread_local)
thread_local std::unordered_map<FastStringKey, int, FastStringKey::Hash>
v8_fast_api_call_counts;
diff --git a/src/node_errors.cc b/src/node_errors.cc
index cf000047d38..cf71884c88d 100644
--- a/src/node_errors.cc
+++ b/src/node_errors.cc
@@ -190,9 +190,11 @@ static std::string GetErrorSource(Isolate* isolate,
}
static std::atomic<bool> is_in_oom{false};
+// NOLINTNEXTLINE(runtime/thread_local)
static thread_local std::atomic<bool> is_retrieving_js_stacktrace{false};
// This is thread-local because it only guards re-entrancy within the current
// thread's uncaught-exception path; no cross-thread synchronization is needed.
+// NOLINTNEXTLINE(runtime/thread_local)
static thread_local bool is_in_uncaught_exception = false;
MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) {
if (isolate == nullptr) {
diff --git a/src/node_internals.h b/src/node_internals.h
index 1c4da8e2d20..bb45d73e909 100644
--- a/src/node_internals.h
+++ b/src/node_internals.h
@@ -286,6 +286,7 @@ class InternalCallbackScope {
// Non-zero while an Environment on this thread is closing its handles with JS
// disallowed isolate-wide; InternalCallbackScope re-allows it for the other
// Environments whose callbacks run in those loop turns.
+// NOLINTNEXTLINE(runtime/thread_local)
extern thread_local int handle_cleanup_depth;
class DebugSealHandleScope {
diff --git a/src/quic/data.cc b/src/quic/data.cc
index fd4c3253432..f15051da498 100644
--- a/src/quic/data.cc
+++ b/src/quic/data.cc
@@ -32,6 +32,7 @@ using v8::Undefined;
using v8::Value;
namespace quic {
+// NOLINTNEXTLINE(runtime/thread_local)
thread_local int DebugIndentScope::indent_ = 0;
Path::Path(const SocketAddress& local, const SocketAddress& remote) {
diff --git a/src/quic/defs.h b/src/quic/defs.h
index 45b4c77d158..2f249b4ee46 100644
--- a/src/quic/defs.h
+++ b/src/quic/defs.h
@@ -395,6 +395,7 @@ class DebugIndentScope final {
}
private:
+ // NOLINTNEXTLINE(runtime/thread_local)
static thread_local int indent_;
};
diff --git a/tools/cpplint.py b/tools/cpplint.py
index 464d95b8824..1e26e3ded39 100755
--- a/tools/cpplint.py
+++ b/tools/cpplint.py
@@ -350,6 +350,7 @@ _ERROR_CATEGORIES = [
"runtime/printf_format",
"runtime/references",
"runtime/string",
+ "runtime/thread_local",
"runtime/threadsafe_fn",
"runtime/vlog",
"runtime/v8_persistent",
@@ -7446,6 +7447,25 @@ def CheckStringValueUsage(filename, lines, error):
'Use node::TwoByteValue instead.')
+def CheckThreadLocalUsage(filename, lines, error):
+ """Logs an error if thread_local is used in src/.
+ Args:
+ filename: The name of the current file.
+ lines: An array of strings, each representing a line of the file.
+ error: The function to call with any errors found.
+ """
+ if not (filename.startswith('src/') or filename.startswith('src\\')):
+ return
+
+ for linenum, line in enumerate(lines):
+ if re.search(r'\bthread_local\b', line.split('//', 1)[0]):
+ error(filename, linenum, 'runtime/thread_local', 5,
+ 'Several Environments can share a thread, so keep state that '
+ 'belongs to one on the Environment or its BindingData. Mark '
+ 'intentionally per-thread state with '
+ 'NOLINTNEXTLINE(runtime/thread_local).')
+
+
def ProcessLine(
filename,
file_extension,
@@ -7609,6 +7629,8 @@ def ProcessFileData(filename, file_extension, lines, error, extra_check_function
CheckStringValueUsage(filename, lines, error)
+ CheckThreadLocalUsage(filename, lines, error)
+
def ProcessConfigOverrides(filename):
"""Loads the configuration files and processes the config overrides.