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.