Compare commits

..

2 Commits

Author SHA1 Message Date
Shelley Vohr
1a76950d89 fix: crash when closing devtools after focus 2026-04-09 10:40:29 +02:00
Shelley Vohr
4dfada86ce fix: menu items not cleaned up after rebuild (#50806)
Menu was holding a SelfKeepAlive to itself from construction, so any
Menu that was never opened (e.g. an application menu replaced before
being shown) stayed pinned in cppgc forever. Repeated calls to
Menu.setApplicationMenu leaked every prior Menu along with its model
and items.

Restore the original Pin/Unpin lifecycle: start keep_alive_ empty and
only assign `this` in OnMenuWillShow. OnMenuWillClose already clears
it.
2026-04-09 11:56:39 +09:00
9 changed files with 116 additions and 74 deletions

View File

@@ -30,19 +30,3 @@ runs:
echo "$HOME/.electron_build_tools/third_party/depot_tools" >> $GITHUB_PATH
echo "$HOME/.electron_build_tools/third_party/depot_tools/python-bin" >> $GITHUB_PATH
fi
- name: Install patched siso (Windows)
# Overrides the CIPD siso with a build from electron/siso that carries a
# retry for transient ERROR_INVALID_PARAMETER during the subninja scan on
# container bind-mounted out/ directories. Remove once the fix has rolled
# into Chromium's siso_version.
if: ${{ runner.os == 'Windows' }}
shell: bash
env:
ELECTRON_SISO_URL: https://github.com/electron/siso/releases/download/v1.5.7-electron.2/siso-windows-amd64.exe
ELECTRON_SISO_SHA256: 0e6b754820be3324d5ea4ca3af3634b4cfcf806d89140d78fec4e2a8ef636c9d
run: |
set -eo pipefail
mkdir -p /c/electron-siso
curl --fail --retry 3 -sSL "$ELECTRON_SISO_URL" -o /c/electron-siso/siso.exe
echo "$ELECTRON_SISO_SHA256 /c/electron-siso/siso.exe" | sha256sum --check --strict -
echo "SISO_PATH=C:\\electron-siso\\siso.exe" >> "$GITHUB_ENV"

View File

@@ -387,7 +387,7 @@ jobs:
needs: checkout-windows
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-32core
build-runs-on: electron-arc-centralus-windows-amd64-16core
test-runs-on: windows-latest
target-platform: win
target-arch: x64
@@ -406,7 +406,7 @@ jobs:
needs: checkout-windows
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-32core
build-runs-on: electron-arc-centralus-windows-amd64-16core
test-runs-on: windows-latest
target-platform: win
target-arch: x86
@@ -425,7 +425,7 @@ jobs:
needs: checkout-windows
if: ${{ needs.setup.outputs.src == 'true' && !inputs.skip-windows }}
with:
build-runs-on: electron-arc-centralus-windows-amd64-32core
build-runs-on: electron-arc-centralus-windows-amd64-16core
test-runs-on: windows-11-arm
target-platform: win
target-arch: arm64

View File

@@ -61,7 +61,7 @@ jobs:
needs: checkout-windows
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-32core
build-runs-on: electron-arc-centralus-windows-amd64-16core
target-platform: win
target-arch: x64
is-release: true
@@ -80,7 +80,7 @@ jobs:
needs: checkout-windows
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-32core
build-runs-on: electron-arc-centralus-windows-amd64-16core
target-platform: win
target-arch: arm64
is-release: true
@@ -99,7 +99,7 @@ jobs:
needs: checkout-windows
with:
environment: production-release
build-runs-on: electron-arc-centralus-windows-amd64-32core
build-runs-on: electron-arc-centralus-windows-amd64-16core
target-platform: win
target-arch: x86
is-release: true

View File

@@ -276,6 +276,7 @@ void Menu::OnMenuWillClose() {
}
void Menu::OnMenuWillShow() {
keep_alive_ = this;
Emit("menu-will-show");
}

View File

@@ -132,7 +132,7 @@ class Menu : public gin::Wrappable<Menu>,
int GetIndexOfCommandId(int command_id) const;
int GetItemCount() const;
gin_helper::SelfKeepAlive<Menu> keep_alive_{this};
gin_helper::SelfKeepAlive<Menu> keep_alive_{nullptr};
};
} // namespace electron::api

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', () => {