Compare commits

...

1 Commits

Author SHA1 Message Date
Shelley Vohr
1a76950d89 fix: crash when closing devtools after focus 2026-04-09 10:40:29 +02:00
4 changed files with 108 additions and 51 deletions

View File

@@ -2315,9 +2315,16 @@ void WebContents::DevToolsOpened() {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
DCHECK(inspectable_web_contents_);
DCHECK(inspectable_web_contents_->GetDevToolsWebContents());
auto handle = FromOrCreate(
isolate, inspectable_web_contents_->GetDevToolsWebContents());
// GetDevToolsWebContents() can be null here when DevTools were closed
// re-entrantly during InspectableWebContents::LoadCompleted() — e.g. when
// a JS handler for `devtools-focused` (fired from the activate path inside
// SetIsDocked) calls closeDevTools() before LoadCompleted finishes.
content::WebContents* const dtwc = GetDevToolsWebContents();
if (!dtwc)
return;
auto handle = FromOrCreate(isolate, dtwc);
devtools_web_contents_.Reset(isolate, handle.ToV8());
// Set inspected tabID.
@@ -2325,12 +2332,11 @@ void WebContents::DevToolsOpened() {
"DevToolsAPI", "setInspectedTabId", base::Value(ID()));
// Inherit owner window in devtools when it doesn't have one.
auto* devtools = inspectable_web_contents_->GetDevToolsWebContents();
bool has_window = devtools->GetUserData(NativeWindowRelay::UserDataKey());
if (owner_window() && !has_window) {
CHECK(!owner_window_.WasInvalidated());
bool has_window = dtwc->GetUserData(NativeWindowRelay::UserDataKey());
if (owner_window_ && !has_window) {
DCHECK(!owner_window_.WasInvalidated());
DCHECK_EQ(handle->owner_window(), nullptr);
handle->SetOwnerWindow(devtools, owner_window());
handle->SetOwnerWindow(dtwc, owner_window());
}
Emit("devtools-opened");
@@ -3016,7 +3022,7 @@ void WebContents::InspectElement(int x, int y) {
if (!enable_devtools_ || !inspectable_web_contents_)
return;
if (!inspectable_web_contents_->GetDevToolsWebContents())
if (!GetDevToolsWebContents())
OpenDevTools(nullptr);
inspectable_web_contents_->InspectElement(x, y);
}

View File

@@ -11,6 +11,7 @@
#include <string_view>
#include <utility>
#include "base/auto_reset.h"
#include "base/base64.h"
#include "base/containers/fixed_flat_set.h"
#include "base/containers/span.h"
@@ -450,13 +451,17 @@ void InspectableWebContents::ShowDevTools(bool activate) {
}
void InspectableWebContents::CloseDevTools() {
if (is_showing_devtools_) {
close_devtools_pending_ = true;
return;
}
if (GetDevToolsWebContents()) {
frontend_loaded_ = false;
embedder_message_dispatcher_.reset();
if (managed_devtools_web_contents_) {
view_->CloseDevTools();
managed_devtools_web_contents_.reset();
}
embedder_message_dispatcher_.reset();
if (!is_guest())
web_contents_->Focus();
}
@@ -551,52 +556,74 @@ void InspectableWebContents::CloseWindow() {
void InspectableWebContents::LoadCompleted() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
frontend_loaded_ = true;
if (managed_devtools_web_contents_)
view_->ShowDevTools(activate_);
if (!GetDevToolsWebContents())
return;
// If the devtools can dock, "SetIsDocked" will be called by devtools itself.
if (!can_dock_) {
SetIsDocked(DispatchCallback(), false);
if (!devtools_title_.empty()) {
view_->SetTitle(devtools_title_);
}
} else {
if (dock_state_.empty()) {
const base::DictValue& prefs =
pref_service_->GetDict(kDevToolsPreferences);
const std::string* current_dock_state =
prefs.FindString("currentDockState");
if (current_dock_state) {
std::string sanitized;
base::RemoveChars(*current_dock_state, "\"", &sanitized);
dock_state_ = IsValidDockState(sanitized) ? sanitized : "right";
} else {
dock_state_ = "right";
frontend_loaded_ = true;
// ShowDevTools and SetIsDocked trigger focus on the DevTools WebContents.
// Focus events fire JS handlers via V8 microtask checkpoints, and those
// handlers can call closeDevTools() re-entrantly. Guard the entire show
// phase so that any re-entrant close is deferred until the stack unwinds.
{
base::AutoReset<bool> guard(&is_showing_devtools_, true);
if (managed_devtools_web_contents_)
view_->ShowDevTools(activate_);
// If the devtools can dock, "SetIsDocked" will be called by devtools
// itself.
if (!can_dock_) {
SetIsDocked(DispatchCallback(), false);
if (!devtools_title_.empty()) {
view_->SetTitle(devtools_title_);
}
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
auto* api_web_contents = api::WebContents::From(GetWebContents());
if (api_web_contents) {
auto* win =
static_cast<NativeWindowViews*>(api_web_contents->owner_window());
// When WCO is enabled, undock the devtools if the current dock
// position overlaps with the position of window controls to avoid
// broken layout.
if (win && win->IsWindowControlsOverlayEnabled()) {
if (IsAppRTL() && dock_state_ == "left") {
dock_state_ = "undocked";
} else if (dock_state_ == "right") {
dock_state_ = "undocked";
} else {
if (dock_state_.empty()) {
const base::DictValue& prefs =
pref_service_->GetDict(kDevToolsPreferences);
const std::string* current_dock_state =
prefs.FindString("currentDockState");
if (current_dock_state) {
std::string sanitized;
base::RemoveChars(*current_dock_state, "\"", &sanitized);
dock_state_ = IsValidDockState(sanitized) ? sanitized : "right";
} else {
dock_state_ = "right";
}
}
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
auto* api_web_contents = api::WebContents::From(GetWebContents());
if (api_web_contents) {
auto* win =
static_cast<NativeWindowViews*>(api_web_contents->owner_window());
// When WCO is enabled, undock the devtools if the current dock
// position overlaps with the position of window controls to avoid
// broken layout.
if (win && win->IsWindowControlsOverlayEnabled()) {
if (IsAppRTL() && dock_state_ == "left") {
dock_state_ = "undocked";
} else if (dock_state_ == "right") {
dock_state_ = "undocked";
}
}
}
}
#endif
std::u16string javascript = base::UTF8ToUTF16(
"EUI.DockController.DockController.instance().setDockSide(\"" +
dock_state_ + "\");");
GetDevToolsWebContents()->GetPrimaryMainFrame()->ExecuteJavaScript(
javascript, base::NullCallback());
std::u16string javascript = base::UTF8ToUTF16(
"EUI.DockController.DockController.instance().setDockSide(\"" +
dock_state_ + "\");");
GetDevToolsWebContents()->GetPrimaryMainFrame()->ExecuteJavaScript(
javascript, base::NullCallback());
}
}
// If CloseDevTools was called re-entrantly during the show phase (e.g. from
// a JS devtools-focused handler), execute the deferred close now that the
// focus notification stack has fully unwound.
if (close_devtools_pending_) {
close_devtools_pending_ = false;
CloseDevTools();
return;
}
#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)

View File

@@ -268,6 +268,14 @@ class InspectableWebContents
std::unique_ptr<InspectableWebContentsView> view_;
bool frontend_loaded_ = false;
// Re-entrancy guard: ShowDevTools triggers focus on the DevTools WebContents,
// which fires JS events whose microtask checkpoint can re-entrantly call
// CloseDevTools(). Destroying the WebContents or its widget while the focus
// notification is still iterating observers is a CHECK/UAF. These flags defer
// the close until the show path has fully unwound.
bool is_showing_devtools_ = false;
bool close_devtools_pending_ = false;
scoped_refptr<content::DevToolsAgentHost> agent_host_;
std::unique_ptr<content::DevToolsFrontendHost> frontend_host_;
std::unique_ptr<DevToolsEmbedderMessageDispatcher>

View File

@@ -1249,6 +1249,22 @@ describe('webContents module', () => {
await devtoolsOpened2;
expect(w.webContents.isDevToolsOpened()).to.be.true();
});
it('does not crash when closing DevTools immediately after opening', async () => {
const w = new BrowserWindow({ show: true });
await w.loadURL('about:blank');
const devToolsFocused = once(w.webContents, 'devtools-focused');
w.webContents.openDevTools({ mode: 'detach' });
w.webContents.inspectElement(100, 100);
await devToolsFocused;
const devtoolsClosed = once(w.webContents, 'devtools-closed');
w.webContents.closeDevTools();
await devtoolsClosed;
expect(w.webContents.isDevToolsOpened()).to.be.false();
});
});
describe('setDevToolsTitle() API', () => {