Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Commit 0998016

Browse files
joyeecheungaduh95
authored andcommitted
inspector: avoid calling into JS from V8 interrupts
Our inspector implementation dispatches inspector messages from a V8 interrupt handler, so they could be handled during an arbitrary point of JS execution where re-calling into another irrelevant JS code is not safe. This patch tracks V8 interrupt state in this case and rewrite the async hook toggling as state reconciliation, so requests only record the desired state, which is applied once calling into JS is possible and safe, and the actual invocation is deferred to an immediate when inside an interrupt. This simplifies the previous mechanism and makes re-entracy and early termination safer. Drive-by: skip installing command line API extensions during teardown when calling into JS is no longer safe. Signed-off-by: Joyee Cheung <joyeec9h3@gmail.com> PR-URL: #65028 Refs: https://issues.chromium.org/u/1/issues/42212250 Refs: https://chromium-review.googlesource.com/c/v8/v8/+/8173727 Refs: #26935 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent a9b31df commit 0998016

6 files changed

Lines changed: 83 additions & 59 deletions

File tree

‎src/env-inl.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ inline void Environment::set_can_call_into_js(bool can_call_into_js) {
623623
can_call_into_js_ = can_call_into_js;
624624
}
625625

626+
inlineboolEnvironment::is_processing_v8_interrupt() const {
627+
return is_processing_v8_interrupt_;
628+
}
629+
626630
inlineboolEnvironment::has_run_bootstrapping_code() const {
627631
return principal_realm_->has_run_bootstrapping_code();
628632
}

‎src/env.cc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,7 +1549,9 @@ void Environment::RequestInterruptFromV8() {
15491549
return;
15501550
}
15511551
env->interrupt_data_.store(nullptr);
1552+
env->is_processing_v8_interrupt_ = true;
15521553
env->RunAndClearInterrupts();
1554+
env->is_processing_v8_interrupt_ = false;
15531555
}, interrupt_data);
15541556
}
15551557

‎src/env.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -799,6 +799,12 @@ class Environment final : public MemoryRetainer {
799799
inlineboolcan_call_into_js() const;
800800
inlinevoidset_can_call_into_js(bool can_call_into_js);
801801

802+
// True while RequestInterrupt() callbacks are being invoked from the
803+
// v8::Isolate::RequestInterrupt() handler, i.e. potentially at an
804+
// arbitrary point during JS execution. Calling into JS must be avoided
805+
// in that case.
806+
inlineboolis_processing_v8_interrupt() const;
807+
802808
// Increase or decrease a counter that manages whether this Environment
803809
// keeps the event loop alive on its own or not. The counter starts out at 0,
804810
// meaning it does not, and any positive value will make it keep the event
@@ -1252,6 +1258,7 @@ class Environment final : public MemoryRetainer {
12521258
bool task_queues_async_initialized_ = false;
12531259

12541260
std::atomic<Environment**> interrupt_data_ {nullptr};
1261+
bool is_processing_v8_interrupt_ = false;
12551262
voidRequestInterruptFromV8();
12561263
staticvoidCheckImmediate(uv_check_t* handle);
12571264

‎src/inspector_agent.cc‎

Lines changed: 60 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,7 @@ class NodeInspectorClient : public V8InspectorClient {
555555
return;
556556
}
557557
if (auto agent = env_->inspector_agent()) {
558-
if (depth == 0) {
559-
agent->DisableAsyncHook();
560-
} else {
561-
agent->EnableAsyncHook();
562-
}
558+
agent->SetAsyncHookTrackingEnabled(depth != 0);
563559
}
564560
}
565561

@@ -655,6 +651,7 @@ class NodeInspectorClient : public V8InspectorClient {
655651

656652
voidinstallAdditionalCommandLineAPI(Local<Context> context,
657653
Local<Object> target) override {
654+
if (!env_->can_call_into_js()) return;
658655
Local<Function> installer = env_->inspector_console_extension_installer();
659656
if (!installer.IsEmpty()) {
660657
Local<Value> argv[] = {target};
@@ -1076,58 +1073,69 @@ void Agent::RegisterAsyncHook(Isolate* isolate,
10761073
Local<Function> disable_function) {
10771074
parent_env_->set_inspector_enable_async_hooks(enable_function);
10781075
parent_env_->set_inspector_disable_async_hooks(disable_function);
1079-
if (pending_enable_async_hook_) {
1080-
CHECK(!pending_disable_async_hook_);
1081-
pending_enable_async_hook_ = false;
1082-
EnableAsyncHook();
1083-
} elseif (pending_disable_async_hook_) {
1084-
CHECK(!pending_enable_async_hook_);
1085-
pending_disable_async_hook_ = false;
1086-
DisableAsyncHook();
1087-
}
1076+
SyncAsyncHookState();
10881077
}
10891078

1090-
voidAgent::EnableAsyncHook() {
1091-
HandleScope scope(parent_env_->isolate());
1092-
Local<Function> enable = parent_env_->inspector_enable_async_hooks();
1093-
if (!enable.IsEmpty()) {
1094-
ToggleAsyncHook(parent_env_->isolate(), enable);
1095-
} elseif (pending_disable_async_hook_) {
1096-
CHECK(!pending_enable_async_hook_);
1097-
pending_disable_async_hook_ = false;
1098-
} else {
1099-
pending_enable_async_hook_ = true;
1100-
}
1079+
voidAgent::SetAsyncHookTrackingEnabled(bool enabled) {
1080+
async_hook_wanted_ = enabled;
1081+
SyncAsyncHookState();
11011082
}
11021083

1103-
voidAgent::DisableAsyncHook() {
1104-
HandleScope scope(parent_env_->isolate());
1105-
Local<Function> disable = parent_env_->inspector_disable_async_hooks();
1106-
if (!disable.IsEmpty()) {
1107-
ToggleAsyncHook(parent_env_->isolate(), disable);
1108-
} elseif (pending_enable_async_hook_) {
1109-
CHECK(!pending_disable_async_hook_);
1110-
pending_enable_async_hook_ = false;
1111-
} else {
1112-
pending_disable_async_hook_ = true;
1113-
}
1114-
}
1084+
// Reconcile the state of the async hook used for async stack traces with the
1085+
// state last requested by the protocol. The hook is set up in JS land,
1086+
// (see inspector_async_hooks.js), which isn't safe to do when:
1087+
// 1. We are in early bootstrap and the setup functions aren't registered in
1088+
// C++ yet.
1089+
// 2. We are in a V8 interrupt requested by inspector protocol message
1090+
// dispatch e.g. from maxAsyncCallStackDepthChanged() notifications.
1091+
// When it's not safe to call into JS, this is a no-op and we'll try again in
1092+
// RegisterAsyncHook() (for 1) or from a scheduled immediate (for 2).
1093+
voidAgent::SyncAsyncHookState() {
1094+
// The debugger can request an interrupt within the toggle JS function itself,
1095+
// A nested call only records the new requested state, the outermost call sees
1096+
// it when re-checking the loop condition after each toggle.
1097+
if (syncing_async_hook_state_) return;
1098+
syncing_async_hook_state_ = true;
1099+
auto on_exit = OnScopeLeave([this]() { syncing_async_hook_state_ = false; });
1100+
1101+
Isolate* isolate = parent_env_->isolate();
1102+
HandleScope scope(isolate);
1103+
while (async_hook_wanted_ != async_hook_enabled_) {
1104+
// Guard against running this during cleanup -- no async events will be
1105+
// emitted anyway at that point anymore, and calling into JS is not
1106+
// possible. This should probably not be something we're attempting in the
1107+
// first place,
1108+
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1109+
if (!parent_env_->can_call_into_js()) return;
1110+
1111+
bool enable = async_hook_wanted_;
1112+
Local<Function> fn = enable ? parent_env_->inspector_enable_async_hooks()
1113+
: parent_env_->inspector_disable_async_hooks();
1114+
if (fn.IsEmpty()) return;
1115+
1116+
if (parent_env_->is_processing_v8_interrupt()) {
1117+
parent_env_->SetImmediate(
1118+
[](Environment* env) {
1119+
Agent* agent = env->inspector_agent();
1120+
if (agent != nullptr) agent->SyncAsyncHookState();
1121+
},
1122+
CallbackFlags::kUnrefed);
1123+
return;
1124+
}
11151125

1116-
voidAgent::ToggleAsyncHook(Isolate* isolate, Local<Function> fn) {
1117-
// Guard against running this during cleanup -- no async events will be
1118-
// emitted anyway at that point anymore, and calling into JS is not possible.
1119-
// This should probably not be something we're attempting in the first place,
1120-
// Refs: https://github.com/nodejs/node/pull/34362#discussion_r456006039
1121-
if (!parent_env_->can_call_into_js()) return;
1122-
CHECK(parent_env_->has_run_bootstrapping_code());
1123-
HandleScope handle_scope(isolate);
1124-
CHECK(!fn.IsEmpty());
1125-
auto context = parent_env_->context();
1126-
v8::TryCatch try_catch(isolate);
1127-
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1128-
if (try_catch.HasCaught() && !try_catch.HasTerminated()) {
1129-
PrintCaughtException(isolate, context, try_catch);
1130-
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1126+
CHECK(parent_env_->has_run_bootstrapping_code());
1127+
Local<Context> context = parent_env_->context();
1128+
v8::TryCatch try_catch(isolate);
1129+
USE(fn->Call(context, Undefined(isolate), 0, nullptr));
1130+
if (try_catch.HasCaught()) {
1131+
// Termination may abort the toggle invocation, retrying now would just
1132+
// be terminated again. Instead of recording the toggle that may not have
1133+
// taken effect, leave the states as-is so that a later sync retries.
1134+
if (try_catch.HasTerminated()) return;
1135+
PrintCaughtException(isolate, context, try_catch);
1136+
UNREACHABLE("Cannot toggle Inspector's AsyncHook, please report this.");
1137+
}
1138+
async_hook_enabled_ = enable;
11311139
}
11321140
}
11331141

‎src/inspector_agent.h‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ class Agent {
9090
voidRegisterAsyncHook(v8::Isolate* isolate,
9191
v8::Local<v8::Function> enable_function,
9292
v8::Local<v8::Function> disable_function);
93-
voidEnableAsyncHook();
94-
voidDisableAsyncHook();
93+
voidSetAsyncHookTrackingEnabled(bool enabled);
9594

9695
voidSetParentHandle(std::unique_ptr<ParentInspectorHandle> parent_handle);
9796
std::unique_ptr<ParentInspectorHandle> GetParentHandle(uint64_t thread_id,
@@ -132,7 +131,7 @@ class Agent {
132131
std::shared_ptr<NetworkResourceManager> GetNetworkResourceManager();
133132

134133
private:
135-
voidToggleAsyncHook(v8::Isolate* isolate, v8::Local<v8::Function> fn);
134+
voidSyncAsyncHookState();
136135
voidToggleNetworkTracking(v8::Isolate* isolate, v8::Local<v8::Function> fn);
137136

138137
node::Environment* parent_env_;
@@ -150,8 +149,12 @@ class Agent {
150149
DebugOptions debug_options_;
151150
std::shared_ptr<ExclusiveAccess<HostPort>> host_port_;
152151

153-
bool pending_enable_async_hook_ = false;
154-
bool pending_disable_async_hook_ = false;
152+
// The state of the async hook used for async stack traces that the protocol
153+
// last requested, and the state JS currently has. SyncAsyncHookState()
154+
// reconciles the two when it is possible and safe to call into JS.
155+
bool async_hook_wanted_ = false;
156+
bool async_hook_enabled_ = false;
157+
bool syncing_async_hook_state_ = false;
155158

156159
bool network_tracking_enabled_ = false;
157160
bool pending_enable_network_tracking = false;

‎test/parallel/test-inspector-async-hook-after-done.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ function onAttachToWorker({ params: { sessionId } }) {
3434
session.once('NodeWorker.receivedMessageFromWorker',onMessageReceived);
3535
return;
3636
}
37-
// Force a call to node::inspector::Agent::ToggleAsyncHook by changing the
38-
// async call stack depth
37+
// Force a call to node::inspector::Agent::SyncAsyncHookState by changing
38+
// the async call stack depth
3939
postToWorkerInspector('Debugger.setAsyncCallStackDepth',{maxDepth: 1});
4040
// This is were the original crash happened
4141
session.post('NodeWorker.detach',{ sessionId },()=>{

0 commit comments

Comments
 (0)