diff --git a/common.gypi b/common.gypi index 0b01ec8c49fe..74a7cf42d225 100644 --- a/common.gypi +++ b/common.gypi @@ -42,7 +42,7 @@ # Reset this number to 0 on major V8 upgrades. # Increment by one for each non-official patch applied to deps/v8. - 'v8_embedder_string': '-node.28', + 'v8_embedder_string': '-node.29', ##### V8 defaults for Node.js ##### @@ -54,6 +54,8 @@ # Refs: https://github.com/nodejs/node/issues/23167 # Enable compiler warnings when using V8_DEPRECATED apis from V8 code. 'v8_deprecation_warnings': 0, + # Check that JavaScript execution is disallowed in V8 API interrupts. + 'v8_disallow_js_in_api_interrupts_is_checked': 1, # Enable compiler warnings when using V8_DEPRECATE_SOON apis from V8 code. 'v8_imminent_deprecation_warnings': 0, @@ -534,6 +536,9 @@ ['v8_deprecation_warnings == 1', { 'defines': ['V8_DEPRECATION_WARNINGS',], }], + ['v8_disallow_js_in_api_interrupts_is_checked == 1', { + 'defines': ['V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED',], + }], ['v8_imminent_deprecation_warnings == 1', { 'defines': ['V8_IMMINENT_DEPRECATION_WARNINGS',], }], diff --git a/deps/v8/BUILD.bazel b/deps/v8/BUILD.bazel index f28fea4e8aa1..53c06e3ceccf 100644 --- a/deps/v8/BUILD.bazel +++ b/deps/v8/BUILD.bazel @@ -148,6 +148,11 @@ v8_flag(name = "v8_enable_trace_maps") v8_flag(name = "v8_enable_v8_checks") +v8_flag( + name = "v8_disallow_js_in_api_interrupts_is_checked", + default = True, +) + v8_flag(name = "v8_enable_verify_csa") v8_flag(name = "v8_enable_verify_heap") @@ -496,6 +501,7 @@ v8_config( "v8_android_log_stdout": "V8_ANDROID_LOG_STDOUT", "v8_code_comments": "V8_CODE_COMMENTS", "v8_deprecation_warnings": "V8_DEPRECATION_WARNINGS", + "v8_disallow_js_in_api_interrupts_is_checked": "V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED", "v8_imminent_deprecation_warnings": "V8_IMMINENT_DEPRECATION_WARNINGS", "v8_enable_debug_code": "V8_ENABLE_DEBUG_CODE", "v8_enable_disassembler": "ENABLE_DISASSEMBLER", diff --git a/deps/v8/BUILD.gn b/deps/v8/BUILD.gn index e81430fbc393..ea335df90341 100644 --- a/deps/v8/BUILD.gn +++ b/deps/v8/BUILD.gn @@ -59,6 +59,10 @@ declare_args() { # Enable compiler warnings when using V8_DEPRECATED apis. v8_deprecation_warnings = true + # Check that executing JavaScript inside API interrupt callbacks is + # disallowed. + v8_disallow_js_in_api_interrupts_is_checked = true + # Enable compiler warnings when using V8_DEPRECATE_SOON apis. v8_imminent_deprecation_warnings = true @@ -999,6 +1003,7 @@ external_v8_defines = [ "V8_COMPRESS_ZONES", "V8_ENABLE_SANDBOX", "V8_DEPRECATION_WARNINGS", + "V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED", "V8_IMMINENT_DEPRECATION_WARNINGS", "V8_USE_PERFETTO", "V8_USE_PERFETTO_JSON_EXPORT", @@ -1047,6 +1052,10 @@ if (v8_enable_sandbox) { if (v8_deprecation_warnings) { enabled_external_v8_defines += [ "V8_DEPRECATION_WARNINGS" ] } +if (v8_disallow_js_in_api_interrupts_is_checked) { + enabled_external_v8_defines += + [ "V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED" ] +} if (v8_imminent_deprecation_warnings) { enabled_external_v8_defines += [ "V8_IMMINENT_DEPRECATION_WARNINGS" ] } @@ -3093,6 +3102,8 @@ generated_file("v8_generate_features_json") { output_conversion = "json" contents = { v8_deprecation_warnings = v8_deprecation_warnings + v8_disallow_js_in_api_interrupts_is_checked = + v8_disallow_js_in_api_interrupts_is_checked v8_enable_31bit_smis_on_64bit_arch = v8_enable_31bit_smis_on_64bit_arch v8_enable_direct_handle = v8_enable_direct_handle v8_enable_extensible_ro_snapshot = v8_enable_extensible_ro_snapshot diff --git a/deps/v8/src/common/assert-scope.h b/deps/v8/src/common/assert-scope.h index cf82be0bc8ee..494d330b3332 100644 --- a/deps/v8/src/common/assert-scope.h +++ b/deps/v8/src/common/assert-scope.h @@ -110,7 +110,7 @@ class V8_NODISCARD PerThreadAssertScope ScopeType& operator=(const ScopeType&) = delete; \ V8_EXPORT_PRIVATE ~ScopeType(); \ \ - static bool IsAllowed(Isolate* isolate); \ + V8_EXPORT_PRIVATE static bool IsAllowed(Isolate* isolate); \ \ V8_EXPORT_PRIVATE static void Open(Isolate* isolate, \ bool* was_execution_allowed); \ diff --git a/deps/v8/src/execution/isolate.cc b/deps/v8/src/execution/isolate.cc index 06fa14bf6b10..e86eabdbf07d 100644 --- a/deps/v8/src/execution/isolate.cc +++ b/deps/v8/src/execution/isolate.cc @@ -2245,6 +2245,13 @@ void Isolate::InvokeApiInterruptCallbacks() { } VMState state(this); HandleScope handle_scope(this); + // API interrupt callbacks are forbidden from executing JavaScript on the + // interrupted Isolate (see v8::Isolate::RequestInterrupt contract in + // v8-isolate.h: "Registered |callback| must not reenter interrupted + // Isolate."). +#ifdef V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED + DisallowJavascriptExecution no_js(this); +#endif // V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED entry.first(reinterpret_cast(this), entry.second); } } diff --git a/deps/v8/src/inspector/v8-debugger.cc b/deps/v8/src/inspector/v8-debugger.cc index 69fb293610bc..747abefcedd5 100644 --- a/deps/v8/src/inspector/v8-debugger.cc +++ b/deps/v8/src/inspector/v8-debugger.cc @@ -532,6 +532,7 @@ void V8Debugger::handleProgramBreak( } }); { + v8::Isolate::AllowJavascriptExecutionScope allow_script(m_isolate); v8::Context::Scope scope(pausedContext); m_inspector->forEachSession( diff --git a/deps/v8/src/inspector/v8-inspector-impl.cc b/deps/v8/src/inspector/v8-inspector-impl.cc index a4610e0d9b33..f085f7f72ec7 100644 --- a/deps/v8/src/inspector/v8-inspector-impl.cc +++ b/deps/v8/src/inspector/v8-inspector-impl.cc @@ -96,6 +96,7 @@ int V8InspectorImpl::resolveUniqueContextId( v8::MaybeLocal V8InspectorImpl::compileAndRunInternalScript( v8::Local context, v8::Local source) { + v8::Isolate::AllowJavascriptExecutionScope allow_script(m_isolate); v8::Local unboundScript; if (!v8::debug::CompileInspectorScript(m_isolate, source) .ToLocal(&unboundScript)) diff --git a/deps/v8/src/inspector/v8-inspector-session-impl.cc b/deps/v8/src/inspector/v8-inspector-session-impl.cc index 95ae57032b32..38d8dd93ea41 100644 --- a/deps/v8/src/inspector/v8-inspector-session-impl.cc +++ b/deps/v8/src/inspector/v8-inspector-session-impl.cc @@ -355,6 +355,8 @@ void V8InspectorSessionImpl::reportAllContexts(V8RuntimeAgentImpl* agent) { } void V8InspectorSessionImpl::dispatchProtocolMessage(StringView message) { + v8::Isolate::AllowJavascriptExecutionScope allow_script( + m_inspector->isolate()); KeepSessionAliveScope keepAlive(*this); using v8_crdtp::span; diff --git a/deps/v8/src/inspector/v8-regex.cc b/deps/v8/src/inspector/v8-regex.cc index 4adf347843ff..09c308f84d38 100644 --- a/deps/v8/src/inspector/v8-regex.cc +++ b/deps/v8/src/inspector/v8-regex.cc @@ -38,6 +38,7 @@ V8Regex::V8Regex(V8InspectorImpl* inspector, const String16& pattern, v8::Local regex; // Protect against reentrant debugger calls via interrupts. v8::debug::PostponeInterruptsScope no_interrupts(m_inspector->isolate()); + v8::Isolate::AllowJavascriptExecutionScope allow_js(m_inspector->isolate()); if (v8::RegExp::New(context, toV8String(isolate, pattern), static_cast(flags)) .ToLocal(®ex)) @@ -69,6 +70,7 @@ int V8Regex::match(const String16& string, int startFrom, v8::MicrotasksScope::kDoNotRunMicrotasks); // Protect against reentrant debugger calls via interrupts. v8::debug::PostponeInterruptsScope no_interrupts(m_inspector->isolate()); + v8::Isolate::AllowJavascriptExecutionScope allow_js(m_inspector->isolate()); v8::TryCatch tryCatch(isolate); v8::Local regex = m_regex.Get(isolate); diff --git a/deps/v8/test/cctest/test-api.cc b/deps/v8/test/cctest/test-api.cc index ba7e079f411c..9b5fdd3cfd96 100644 --- a/deps/v8/test/cctest/test-api.cc +++ b/deps/v8/test/cctest/test-api.cc @@ -22306,6 +22306,26 @@ TEST(RequestInterruptSmallScripts) { CHECK(interrupt_was_called); } +#ifdef V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED +static bool interrupt_check_no_js = false; +void DisallowJsInterruptCallback(v8::Isolate* isolate, void* data) { + CHECK(!i::AllowJavascriptExecution::IsAllowed( + reinterpret_cast(isolate))); + interrupt_check_no_js = true; +} + +TEST(RequestInterruptDisallowsJavascript) { + LocalContext env; + v8::Isolate* isolate = CcTest::isolate(); + v8::HandleScope scope(isolate); + + interrupt_check_no_js = false; + isolate->RequestInterrupt(&DisallowJsInterruptCallback, nullptr); + CompileRun("(function(x){return x;})(1);"); + CHECK(interrupt_check_no_js); +} +#endif // V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED + static v8::Global function_new_expected_env_global; static void FunctionNewCallback(const v8::FunctionCallbackInfo& info) { v8::Isolate* isolate = info.GetIsolate(); diff --git a/deps/v8/test/cctest/test-regexp.cc b/deps/v8/test/cctest/test-regexp.cc index 946695833c64..08be47b221a8 100644 --- a/deps/v8/test/cctest/test-regexp.cc +++ b/deps/v8/test/cctest/test-regexp.cc @@ -124,7 +124,13 @@ class InterruptTest { CHECK(string->ContainsOnlyOneByte()); // Internalize the subject by using it as a computed property name in an // object. - CompileRun("o = { [subject_string]: 'foo' }"); + { + // This test is technically wrong for running JS in a C++ interrupt. + // However we know that the interuptee here is the regexp engine, which + // does not care. + Isolate::AllowJavascriptExecutionScope allow_script(isolate); + CompileRun("o = { [subject_string]: 'foo' }"); + } CHECK(string->IsOneByte()); } diff --git a/deps/v8/test/debugger/debug/futex-reentrant-wait.js b/deps/v8/test/debugger/debug/futex-reentrant-wait.js new file mode 100644 index 000000000000..fc82beac534e --- /dev/null +++ b/deps/v8/test/debugger/debug/futex-reentrant-wait.js @@ -0,0 +1,35 @@ +// Copyright 2026 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flags: --allow-natives-syntax + +var Debug = debug.Debug; +var exception = null; + +Debug.setListener(function (event, exec_state, event_data, data) { + if (event == Debug.DebugEvent.Break) { + try { + var sab2 = new SharedArrayBuffer(4); + var i32a2 = new Int32Array(sab2); + Atomics.wait(i32a2, 0, 0, 10); + } catch (e) { + exception = e; + } + } +}); + +let sab = new SharedArrayBuffer(4); +let i32a = new Int32Array(sab); + +let timeout = { + valueOf: function() { + %ScheduleBreak(); + return 10; + } +}; + +Atomics.wait(i32a, 0, 0, timeout); + +assertNotNull(exception); +assertTrue(exception.message.includes("cannot be called in this context")); diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 4bb34e56bcbf..fbafc55f1b74 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -79,6 +79,52 @@ static std::atomic_bool start_io_thread_async_initialized { false }; // Protects the Agent* stored in start_io_thread_async.data. static Mutex start_io_thread_async_mutex; +template +void SyncJavaScriptHookState(Environment* env, + JavaScriptHookState* state, + Local enable_function, + Local disable_function, + bool require_bootstrapping, + Defer defer, + OnFailure on_failure) { + // The debugger can request an interrupt from within a hook's JS toggle + // function. A nested sync only records the new requested state; the + // outermost sync sees it when re-checking the loop after each toggle. + if (state->syncing) return; + state->syncing = true; + auto on_exit = OnScopeLeave([state]() { state->syncing = false; }); + + Isolate* isolate = env->isolate(); + while (state->wanted != state->enabled) { + // No hook events will be emitted during cleanup, and calling into JS is no + // longer possible. + // Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039 + if (!env->can_call_into_js()) return; + + bool enable = state->wanted; + Local function = enable ? enable_function : disable_function; + if (function.IsEmpty()) return; + + if (env->is_processing_v8_interrupt()) { + defer(); + return; + } + + if (require_bootstrapping) CHECK(env->has_run_bootstrapping_code()); + Local context = env->context(); + v8::TryCatch try_catch(isolate); + USE(function->Call(context, Undefined(isolate), 0, nullptr)); + if (try_catch.HasCaught()) { + // Termination may abort the toggle invocation. Do not record a state that + // may not have taken effect, so a later sync can retry it. + if (try_catch.HasTerminated()) return; + PrintCaughtException(isolate, context, try_catch); + on_failure(); + } + state->enabled = enable; + } +} + // Called on the main thread. void StartIoThreadAsyncCallback(uv_async_t* handle) { static_cast(handle->data)->StartIoThread(); @@ -475,6 +521,16 @@ class SameThreadInspectorSession : public InspectorSession { void NotifyClusterWorkersDebugEnabled(Environment* env) { Isolate* isolate = env->isolate(); + + // Starting the inspector from an interrupt is safe, but emitting the cluster + // notification calls into JS. + if (env->is_processing_v8_interrupt()) { + env->SetImmediate( + [](Environment* env) { NotifyClusterWorkersDebugEnabled(env); }, + CallbackFlags::kUnrefed); + return; + } + HandleScope handle_scope(isolate); // Send message to enable debug in cluster workers @@ -979,54 +1035,40 @@ void Agent::SetupNetworkTracking(Local enable_function, Local disable_function) { parent_env_->set_inspector_enable_network_tracking(enable_function); parent_env_->set_inspector_disable_network_tracking(disable_function); - if (pending_enable_network_tracking) { - pending_enable_network_tracking = false; - EnableNetworkTracking(); - } else if (pending_disable_network_tracking) { - pending_disable_network_tracking = false; - DisableNetworkTracking(); - } + SyncNetworkTrackingState(); } void Agent::EnableNetworkTracking() { - if (network_tracking_enabled_) { - return; - } - HandleScope scope(parent_env_->isolate()); - Local enable = parent_env_->inspector_enable_network_tracking(); - if (enable.IsEmpty()) { - pending_enable_network_tracking = true; - } else { - ToggleNetworkTracking(parent_env_->isolate(), enable); - network_tracking_enabled_ = true; - } + network_tracking_state_.wanted = true; + SyncNetworkTrackingState(); } void Agent::DisableNetworkTracking() { - if (!network_tracking_enabled_) { - return; - } - HandleScope scope(parent_env_->isolate()); - Local disable = parent_env_->inspector_disable_network_tracking(); - if (disable.IsEmpty()) { - pending_disable_network_tracking = true; - } else if (!client_->hasConnectedSessions()) { - ToggleNetworkTracking(parent_env_->isolate(), disable); - network_tracking_enabled_ = false; - } + if (client_->hasConnectedSessions()) return; + network_tracking_state_.wanted = false; + SyncNetworkTrackingState(); } -void Agent::ToggleNetworkTracking(Isolate* isolate, Local fn) { - if (!parent_env_->can_call_into_js()) return; - auto context = parent_env_->context(); +void Agent::SyncNetworkTrackingState() { + Isolate* isolate = parent_env_->isolate(); HandleScope scope(isolate); - CHECK(!fn.IsEmpty()); - v8::TryCatch try_catch(isolate); - USE(fn->Call(context, Undefined(isolate), 0, nullptr)); - if (try_catch.HasCaught() && !try_catch.HasTerminated()) { - PrintCaughtException(isolate, context, try_catch); - UNREACHABLE("Cannot toggle network tracking, please report this."); - } + SyncJavaScriptHookState( + parent_env_, + &network_tracking_state_, + parent_env_->inspector_enable_network_tracking(), + parent_env_->inspector_disable_network_tracking(), + false, + [this]() { + parent_env_->SetImmediate( + [](Environment* env) { + Agent* agent = env->inspector_agent(); + if (agent != nullptr) agent->SyncNetworkTrackingState(); + }, + CallbackFlags::kUnrefed); + }, + []() { + UNREACHABLE("Cannot toggle network tracking, please report this."); + }); } void Agent::WaitForDisconnect() { @@ -1077,7 +1119,7 @@ void Agent::RegisterAsyncHook(Isolate* isolate, } void Agent::SetAsyncHookTrackingEnabled(bool enabled) { - async_hook_wanted_ = enabled; + async_hook_state_.wanted = enabled; SyncAsyncHookState(); } @@ -1091,52 +1133,25 @@ void Agent::SetAsyncHookTrackingEnabled(bool enabled) { // When it's not safe to call into JS, this is a no-op and we'll try again in // RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2). void Agent::SyncAsyncHookState() { - // The debugger can request an interrupt within the toggle JS function itself, - // A nested call only records the new requested state, the outermost call sees - // it when re-checking the loop condition after each toggle. - if (syncing_async_hook_state_) return; - syncing_async_hook_state_ = true; - auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; }); - Isolate* isolate = parent_env_->isolate(); HandleScope scope(isolate); - while (async_hook_wanted_ != async_hook_enabled_) { - // Guard against running this during cleanup -- no async events will be - // emitted anyway at that point anymore, and calling into JS is not - // possible. This should probably not be something we're attempting in the - // first place, - // Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039 - if (!parent_env_->can_call_into_js()) return; - - bool enable = async_hook_wanted_; - Local fn = enable ? parent_env_->inspector_enable_async_hooks() - : parent_env_->inspector_disable_async_hooks(); - if (fn.IsEmpty()) return; - - if (parent_env_->is_processing_v8_interrupt()) { - parent_env_->SetImmediate( - [](Environment* env) { - Agent* agent = env->inspector_agent(); - if (agent != nullptr) agent->SyncAsyncHookState(); - }, - CallbackFlags::kUnrefed); - return; - } - - CHECK(parent_env_->has_run_bootstrapping_code()); - Local context = parent_env_->context(); - v8::TryCatch try_catch(isolate); - USE(fn->Call(context, Undefined(isolate), 0, nullptr)); - if (try_catch.HasCaught()) { - // Termination may abort the toggle invocation, retrying now would just - // be terminated again. Instead of recording the toggle that may not have - // taken effect, leave the states as-is so that a later sync retries. - if (try_catch.HasTerminated()) return; - PrintCaughtException(isolate, context, try_catch); - UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this."); - } - async_hook_enabled_ = enable; - } + SyncJavaScriptHookState( + parent_env_, + &async_hook_state_, + parent_env_->inspector_enable_async_hooks(), + parent_env_->inspector_disable_async_hooks(), + true, + [this]() { + parent_env_->SetImmediate( + [](Environment* env) { + Agent* agent = env->inspector_agent(); + if (agent != nullptr) agent->SyncAsyncHookState(); + }, + CallbackFlags::kUnrefed); + }, + []() { + UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this."); + }); } void Agent::AsyncTaskScheduled(const StringView& task_name, void* task, diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 932e4e8dce89..73835d2a4d1d 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -28,6 +28,12 @@ class ParentInspectorHandle; class NodeInspectorClient; class WorkerManager; +struct JavaScriptHookState { + bool wanted = false; + bool enabled = false; + bool syncing = false; +}; + class InspectorSession { public: virtual ~InspectorSession() = default; @@ -132,7 +138,7 @@ class Agent { private: void SyncAsyncHookState(); - void ToggleNetworkTracking(v8::Isolate* isolate, v8::Local fn); + void SyncNetworkTrackingState(); node::Environment* parent_env_; // Encapsulates majority of the Inspector functionality @@ -152,13 +158,11 @@ class Agent { // The state of the async hook used for async stack traces that the protocol // last requested, and the state JS currently has. SyncAsyncHookState() // reconciles the two when it is possible and safe to call into JS. - bool async_hook_wanted_ = false; - bool async_hook_enabled_ = false; - bool syncing_async_hook_state_ = false; + JavaScriptHookState async_hook_state_; - bool network_tracking_enabled_ = false; - bool pending_enable_network_tracking = false; - bool pending_disable_network_tracking = false; + // Network tracking uses JS hooks. Reconcile the protocol requested and + // applied states after leaving a V8 interrupt. + JavaScriptHookState network_tracking_state_; std::shared_ptr network_resource_manager_; }; diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index ef1f99d7f38a..09f0d7e84ce6 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -60,13 +60,31 @@ struct MainThreadConnection { template class JSBindingsConnection : public BaseObject { public: + struct PendingMicrotask { + explicit PendingMicrotask(JSBindingsConnection* connection) + : env(connection->env()), connection(connection) { + env->AddCleanupHook(Delete, this); + } + + static void Delete(void* data) { + delete static_cast(data); + } + + static void Run(void* data) { + std::unique_ptr pending( + static_cast(data)); + pending->env->RemoveCleanupHook(Delete, pending.get()); + pending->connection->FlushPendingMessages(); + } + + Environment* env; + BaseObjectPtr connection; + }; + class JSBindingsSessionDelegate : public InspectorSessionDelegate { public: - JSBindingsSessionDelegate(Environment* env, - JSBindingsConnection* connection) - : env_(env), - connection_(connection) { - } + explicit JSBindingsSessionDelegate(JSBindingsConnection* connection) + : connection_(connection) {} void SendMessageToFrontend(const v8_inspector::StringView& message) override { @@ -74,7 +92,6 @@ class JSBindingsConnection : public BaseObject { } private: - Environment* env_; BaseObjectPtr connection_; }; @@ -84,10 +101,10 @@ class JSBindingsConnection : public BaseObject { : BaseObject(env, wrap), callback_(env->isolate(), callback) { Agent* inspector = env->inspector_agent(); session_ = ConnectionType::Connect( - inspector, std::make_unique(env, this)); - // Inspector responses may be produced from a GC weak callback, where - // invoking the JavaScript session callback is forbidden. Defer delivery - // until the GC has completed. + inspector, std::make_unique(this)); + // Inspector responses may be produced from a GC weak callback or a V8 + // interrupt, where invoking the JavaScript session callback is forbidden. + // Defer delivery until it is safe to call into JavaScript. env->isolate()->AddGCPrologueCallback(GCPrologueCallback, this); env->isolate()->AddGCEpilogueCallback(GCEpilogueCallback, this); } @@ -98,7 +115,8 @@ class JSBindingsConnection : public BaseObject { } void SendMessageToFrontend(const v8_inspector::StringView& message) { - if (in_gc_ || delivering_ || !pending_messages_.empty()) { + if (in_gc_ || env()->is_processing_v8_interrupt() || delivering_ || + !pending_messages_.empty()) { pending_messages_.emplace_back( message.is8Bit() ? std::u16string(message.characters8(), @@ -126,6 +144,12 @@ class JSBindingsConnection : public BaseObject { void ScheduleFlush() { if (flush_scheduled_ || delivering_) return; flush_scheduled_ = true; + if (!in_gc_ && env()->is_processing_v8_interrupt()) { + auto* pending = new PendingMicrotask(this); + env()->context()->GetMicrotaskQueue()->EnqueueMicrotask( + env()->isolate(), PendingMicrotask::Run, pending); + return; + } BaseObjectPtr strong_ref{this}; env()->SetImmediate( [strong_ref](Environment*) { strong_ref->FlushPendingMessages(); }, diff --git a/tools/v8_gypfiles/features.gypi b/tools/v8_gypfiles/features.gypi index a5227687d22d..38d91315bc43 100644 --- a/tools/v8_gypfiles/features.gypi +++ b/tools/v8_gypfiles/features.gypi @@ -107,6 +107,9 @@ # Enable compiler warnings when using V8_DEPRECATED apis. 'v8_deprecation_warnings%': 0, + # Check that JavaScript execution is disallowed in V8 API interrupts. + 'v8_disallow_js_in_api_interrupts_is_checked%': 0, + # Enable compiler warnings when using V8_DEPRECATE_SOON apis. 'v8_imminent_deprecation_warnings%': 0, @@ -410,6 +413,9 @@ ['v8_deprecation_warnings==1', { 'defines': ['V8_DEPRECATION_WARNINGS',], }], + ['v8_disallow_js_in_api_interrupts_is_checked==1', { + 'defines': ['V8_DISALLOW_JS_IN_API_INTERRUPTS_IS_CHECKED',], + }], ['v8_imminent_deprecation_warnings==1', { 'defines': ['V8_IMMINENT_DEPRECATION_WARNINGS',], }],