Commit 6dfe4eb0f07 for nodejs

commit 6dfe4eb0f075639cb285a10ef9b074e7a8483f4c
Author: Shelley Vohr <shelley.vohr@gmail.com>
Date:   Wed Sep 23 17:24:31 2026 +0200

    src: fix FreeEnvironment() breaking JS in sibling Environments

    `FreeEnvironment()` forbids JavaScript on the whole isolate with a
    `DisallowJavascriptExecutionScope` and then spins the event loop in
    `Environment::CleanupHandles()` until its handles are closed. When the
    loop and isolate are shared with other Environments, as they are for an
    embedder hosting several Environments on one thread, those loop turns
    also run the other Environments' timers and I/O callbacks, and every
    one of them failed with an "illegal access" exception thrown into
    unrelated code.

    While an Environment is closing its handles, let `InternalCallbackScope`
    re-allow JavaScript for callbacks of Environments that can still call
    into JS. The Environment being freed stays blocked: its callbacks fail
    the `can_call_into_js()` check before that point, and anything that
    bypasses `InternalCallbackScope` (V8 foreground tasks, raw addon
    callbacks) still runs under the disallow scope.

    Refs: https://github.com/nodejs/node/pull/33874
    Refs: https://github.com/nodejs/node/pull/65819
    Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/65977
    Reviewed-By: Anna Henningsen <anna@addaleax.net>
    Reviewed-By: James M Snell <jasnell@gmail.com>

diff --git a/src/api/callback.cc b/src/api/callback.cc
index 85da82ff84a..c3850fa4afe 100644
--- a/src/api/callback.cc
+++ b/src/api/callback.cc
@@ -96,6 +96,8 @@ InternalCallbackScope::InternalCallbackScope(
   }

   Isolate* isolate = env->isolate();
+  // See IsolateData::handle_cleanup_depth.
+  if (env->isolate_data()->handle_cleanup_depth > 0) allow_js_.emplace(isolate);

   HandleScope handle_scope(isolate);
   Local<Context> current_context = isolate->GetCurrentContext();
diff --git a/src/env.cc b/src/env.cc
index d35c28fcf7e..24cd7b62e4f 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -1453,6 +1453,8 @@ void Environment::CleanupHandles() {
   for (HandleWrap* handle : handle_wrap_queue_)
     handle->Close();

+  isolate_data()->handle_cleanup_depth++;
+  auto done = OnScopeLeave([&]() { isolate_data()->handle_cleanup_depth--; });
   while (handle_cleanup_waiting_ != 0 ||
          request_waiting_ != 0 ||
          !handle_wrap_queue_.IsEmpty()) {
diff --git a/src/env.h b/src/env.h
index 751e3acec59..57f8369f598 100644
--- a/src/env.h
+++ b/src/env.h
@@ -182,6 +182,11 @@ class NODE_EXTERN_PRIVATE IsolateData : public MemoryRetainer {
   inline worker::Worker* worker_context() const;
   inline void set_worker_context(worker::Worker* context);

+  // Non-zero while an Environment on this isolate is closing its handles with
+  // JS disallowed isolate-wide; InternalCallbackScope re-allows it for the
+  // other Environments whose callbacks run in those loop turns.
+  int handle_cleanup_depth = 0;
+
 #define VP(PropertyName, StringValue) V(v8::Private, PropertyName)
 #define VY(PropertyName, StringValue) V(v8::Symbol, PropertyName)
 #define VS(PropertyName, StringValue) V(v8::String, PropertyName)
diff --git a/src/node_internals.h b/src/node_internals.h
index cf3ce9e13f4..1eacd95e3a4 100644
--- a/src/node_internals.h
+++ b/src/node_internals.h
@@ -37,6 +37,7 @@
 #include <cstdint>
 #include <cstdlib>

+#include <optional>
 #include <string>
 #include <variant>
 #include <vector>
@@ -279,6 +280,7 @@ class InternalCallbackScope {
   bool pushed_ids_ = false;
   bool closed_ = false;
   v8::Global<v8::Value> prior_context_frame_;
+  std::optional<v8::Isolate::AllowJavascriptExecutionScope> allow_js_;
 };

 class DebugSealHandleScope {
diff --git a/test/cctest/test_environment.cc b/test/cctest/test_environment.cc
index 359f720c508..7b3458a380f 100644
--- a/test/cctest/test_environment.cc
+++ b/test/cctest/test_environment.cc
@@ -548,6 +548,32 @@ TEST_F(EnvironmentTest, CleanupHookRemovesItselfWhileRunning) {
   EXPECT_TRUE(hook.ran);
 }

+TEST_F(EnvironmentTest, FreeEnvironmentWhileSiblingHasActiveHandles) {
+  const v8::HandleScope handle_scope(isolate_);
+  const Argv argv;
+  Env env1{handle_scope, argv};
+  node::LoadEnvironment(*env1,
+                        "globalThis.ticks = 0;"
+                        "const t = setInterval(() => {"
+                        "  if (++globalThis.ticks == 20) clearInterval(t);"
+                        "}, 1);")
+      .ToLocalChecked();
+  {
+    Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
+    node::LoadEnvironment(*env2, "setInterval(() => {}, 1);").ToLocalChecked();
+    uv_sleep(5);
+  }
+  v8::Context::Scope context_scope(env1.context());
+  EXPECT_EQ(node::SpinEventLoop(*env1).FromJust(), 0);
+  v8::Local<v8::Value> ticks =
+      env1.context()
+          ->Global()
+          ->Get(env1.context(),
+                v8::String::NewFromUtf8Literal(isolate_, "ticks"))
+          .ToLocalChecked();
+  EXPECT_EQ(ticks->Int32Value(env1.context()).FromJust(), 20);
+}
+
 TEST_F(EnvironmentTest, NoEnvironmentSanity) {
   const v8::HandleScope handle_scope(isolate_);
   v8::Local<v8::Context> context = v8::Context::New(isolate_);