Compare commits

...

5 Commits

Author SHA1 Message Date
deepak1556
dc24fde4bf fix: crash on shutdown when unregistering power notification
Refs https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregistersuspendresumenotification
the handle should be the return value of registeration api
not the window handle.
2026-04-10 05:33:38 +09:00
deepak1556
2e78fd640f chore: remove redundant selfkeepalive
Followup to 7c0cb61b3c
2026-04-09 17:44:32 +09:00
deepak1556
22739efc7d spec: reduce menu count to remove CI flakiness 2026-04-09 15:49:22 +09:00
deepak1556
676ce61e76 spec: add menu leak regression test 2026-04-09 12:48:59 +09:00
deepak1556
676605bdf3 chore: address blink gc plugin errors
Key fixes:
- Replace `base::WeakPtrFactory` with `gin::WeakCellFactory` in
  MenuMac, MenuViews, and NetLog, since weak pointers to cppgc-managed
  objects must go through weak cells
- Replace `v8::Global<v8::Value>` with `cppgc::Persistent<Menu>` for
  the menu reference in BaseWindow
- Stop using `gin_helper::Handle<T>` with cppgc types; use raw `T*`
  and add a `static_assert` to prevent future misuse
- Add proper `Trace()` overrides for Menu, MenuMac, MenuViews, and
  NetLog to ensure cppgc members are visited during garbage collection
- Replace `SelfKeepAlive` prevent-GC mechanism in Menu with a
  `cppgc::Persistent` prevent-GC captured in `BindSelfToClosure`
- Introduce `GC_PLUGIN_IGNORE` macro to suppress
  known-safe violations: mojo::Remote fields, ObjC bridging pointers,
  and intentional persistent self-references
- Mark `ArgumentHolder` as `CPPGC_STACK_ALLOCATED()` in both Electron's
  and gin's function_template.h to silence raw-pointer-to-GC-type
  warnings
2026-04-09 12:31:25 +09:00
34 changed files with 306 additions and 69 deletions

View File

@@ -147,6 +147,25 @@ config("branding") {
config("electron_lib_config") {
include_dirs = [ "." ]
cflags = []
if (is_clang && clang_use_chrome_plugins) {
# The plugin is built directly into clang, so there's no need to load it
# dynamically.
cflags += [
"-Xclang",
"-add-plugin",
"-Xclang",
"blink-gc-plugin",
"-Xclang",
"-plugin-arg-blink-gc-plugin",
"-Xclang",
"check-directory=electron/shell/",
"-Xclang",
"-plugin-arg-blink-gc-plugin",
"-Xclang",
"check-directory=gin/",
]
}
}
# We generate the definitions twice here, once in //electron/electron.d.ts

View File

@@ -151,3 +151,4 @@ fix_pulseaudio_stream_and_icon_names.patch
fix_fire_menu_popup_start_for_dynamically_created_aria_menus.patch
feat_allow_enabling_extensions_on_custom_protocols.patch
fix_initialize_com_on_desktopmedialistcapturethread_on_windows.patch
gin_mark_argumentholder_as_cppgc_stack_allocated.patch

View File

@@ -0,0 +1,48 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: deepak1556 <hop2deep@gmail.com>
Date: Tue, 7 Apr 2026 02:34:49 +0900
Subject: [gin] Mark ArgumentHolder as CPPGC_STACK_ALLOCATED
ArgumentHolder is always stack-allocated from Invoker.
When ArgType is a pointer to a cppgc-managed type, the blink-gc plugin
reports an error for the raw pointer `value` field:
[blink-gc] Class 'ArgumentHolder<0, T*>' contains invalid fields.
[blink-gc] Raw pointer field 'value' to a GC managed class.
Disallow any heap allocations and make the lifetime explicit.
Backports https://chromium-review.googlesource.com/c/chromium/src/+/7728865
diff --git a/gin/function_template.h b/gin/function_template.h
index 873015289db9709c00c32080e5387d9cdfea1f69..bc84040952b79f02df783833005cc9583f3378a5 100644
--- a/gin/function_template.h
+++ b/gin/function_template.h
@@ -20,6 +20,7 @@
#include "gin/gin_export.h"
#include "gin/per_isolate_data.h"
#include "gin/public/gin_embedders.h"
+#include "v8/include/cppgc/macros.h"
#include "v8/include/v8-external.h"
#include "v8/include/v8-forward.h"
#include "v8/include/v8-persistent-handle.h"
@@ -162,6 +163,9 @@ GIN_EXPORT void ThrowConversionError(Arguments* args,
// at position |index|.
template <size_t index, typename ArgType, typename = void>
struct ArgumentHolder {
+ CPPGC_STACK_ALLOCATED();
+
+ public:
using ArgLocalType = typename CallbackParamTraits<ArgType>::LocalType;
ArgLocalType value;
@@ -184,6 +188,9 @@ template <size_t index, typename ArgType>
std::is_constructible_v<typename CallbackParamTraits<ArgType>::LocalType,
v8::Isolate*>)
struct ArgumentHolder<index, ArgType> {
+ CPPGC_STACK_ALLOCATED();
+
+ public:
using ArgLocalType = typename CallbackParamTraits<ArgType>::LocalType;
ArgLocalType value;

View File

@@ -76,6 +76,7 @@
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/language_util.h"

View File

@@ -732,10 +732,10 @@ bool BaseWindow::IsFocusable() const {
void BaseWindow::SetMenu(v8::Isolate* isolate, v8::Local<v8::Value> value) {
auto context = isolate->GetCurrentContext();
gin_helper::Handle<Menu> menu;
Menu* menu = nullptr;
v8::Local<v8::Object> object;
if (value->IsObject() && value->ToObject(context).ToLocal(&object) &&
gin::ConvertFromV8(isolate, value, &menu) && !menu.IsEmpty()) {
gin::ConvertFromV8(isolate, value, &menu) && menu) {
// We only want to update the menu if the menu has a non-zero item count,
// or we risk crashes.
if (menu->model()->GetItemCount() == 0) {
@@ -744,7 +744,7 @@ void BaseWindow::SetMenu(v8::Isolate* isolate, v8::Local<v8::Value> value) {
window_->SetMenu(menu->model());
}
menu_.Reset(isolate, menu.ToV8());
menu_ = menu;
} else if (value->IsNull()) {
RemoveMenu();
} else {
@@ -754,7 +754,7 @@ void BaseWindow::SetMenu(v8::Isolate* isolate, v8::Local<v8::Value> value) {
}
void BaseWindow::RemoveMenu() {
menu_.Reset();
menu_.Clear();
window_->SetMenu(nullptr);
}

View File

@@ -17,6 +17,7 @@
#include "shell/browser/native_window_observer.h"
#include "shell/common/api/electron_api_native_image.h"
#include "shell/common/gin_helper/trackable_object.h"
#include "v8/include/cppgc/persistent.h"
namespace gin {
class Arguments;
@@ -34,6 +35,7 @@ class NativeWindow;
namespace api {
class Menu;
class View;
class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
@@ -284,7 +286,7 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
#endif
v8::Global<v8::Value> content_view_;
v8::Global<v8::Value> menu_;
cppgc::Persistent<Menu> menu_;
v8::Global<v8::Value> parent_window_;
std::unique_ptr<NativeWindow> window_;

View File

@@ -11,6 +11,7 @@
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/mojom/data_pipe_getter.mojom.h"
#include "shell/common/gc_plugin.h"
namespace electron::api {
@@ -45,6 +46,8 @@ class DataPipeHolder final : public gin::Wrappable<DataPipeHolder> {
private:
std::string id_;
GC_PLUGIN_IGNORE(
"Context tracking of remote is not needed in the browser process.")
mojo::Remote<network::mojom::DataPipeGetter> data_pipe_;
};

View File

@@ -22,6 +22,7 @@
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/node_includes.h"
#include "ui/base/models/image_model.h"
#include "v8/include/cppgc/persistent.h"
#if BUILDFLAG(IS_MAC)
@@ -71,6 +72,11 @@ Menu::~Menu() {
RemoveModelObserver();
}
void Menu::Trace(cppgc::Visitor* visitor) const {
gin::Wrappable<Menu>::Trace(visitor);
visitor->Trace(parent_);
}
void Menu::RemoveModelObserver() {
if (model_) {
model_->RemoveObserver(this);
@@ -189,20 +195,11 @@ void Menu::OnMenuWillShow(ui::SimpleMenuModel* source) {
}
base::OnceClosure Menu::BindSelfToClosure(base::OnceClosure callback) {
// return ((callback, ref) => { callback() }).bind(null, callback, this)
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
v8::Local<v8::Object> self;
if (GetWrapper(isolate).ToLocal(&self)) {
v8::Global<v8::Value> ref(isolate, self);
return base::BindOnce(
[](base::OnceClosure callback, v8::Global<v8::Value> ref) {
std::move(callback).Run();
},
std::move(callback), std::move(ref));
} else {
return base::DoNothing();
}
return base::BindOnce(
[](base::OnceClosure callback, cppgc::Persistent<Menu> prevent_gc) {
std::move(callback).Run();
},
std::move(callback), cppgc::Persistent<Menu>(this));
}
void Menu::InsertItemAt(int index,
@@ -271,12 +268,10 @@ std::u16string Menu::GetAcceleratorTextAtForTesting(int index) const {
}
void Menu::OnMenuWillClose() {
keep_alive_.Clear();
Emit("menu-will-close");
}
void Menu::OnMenuWillShow() {
keep_alive_ = this;
Emit("menu-will-show");
}

View File

@@ -8,13 +8,12 @@
#include <memory>
#include <string>
#include "base/memory/raw_ptr.h"
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/ui/electron_menu_model.h"
#include "shell/common/gin_helper/constructible.h"
#include "shell/common/gin_helper/self_keep_alive.h"
#include "ui/base/mojom/menu_source_type.mojom-forward.h"
#include "v8/include/cppgc/member.h"
namespace gin {
class Arguments;
@@ -45,6 +44,7 @@ class Menu : public gin::Wrappable<Menu>,
static const gin::WrapperInfo kWrapperInfo;
const gin::WrapperInfo* wrapper_info() const override;
const char* GetHumanReadableName() const override;
void Trace(cppgc::Visitor*) const override;
// gin_helper::Constructible
static void FillObjectTemplate(v8::Isolate*, v8::Local<v8::ObjectTemplate>);
@@ -102,9 +102,6 @@ class Menu : public gin::Wrappable<Menu>,
virtual void ClosePopupAt(int32_t window_id) = 0;
virtual std::u16string GetAcceleratorTextAtForTesting(int index) const;
std::unique_ptr<ElectronMenuModel> model_;
raw_ptr<Menu> parent_ = nullptr;
// Observable:
void OnMenuWillClose() override;
void OnMenuWillShow() override;
@@ -132,7 +129,8 @@ class Menu : public gin::Wrappable<Menu>,
int GetIndexOfCommandId(int command_id) const;
int GetItemCount() const;
gin_helper::SelfKeepAlive<Menu> keep_alive_{nullptr};
std::unique_ptr<ElectronMenuModel> model_;
cppgc::Member<Menu> parent_;
};
} // namespace electron::api

View File

@@ -9,6 +9,7 @@
#include <map>
#include "gin/weak_cell.h"
#import "shell/browser/ui/cocoa/electron_menu_controller.h"
namespace electron {
@@ -23,6 +24,8 @@ class MenuMac : public Menu {
explicit MenuMac(gin::Arguments* args);
~MenuMac() override;
void Trace(cppgc::Visitor*) const override;
protected:
// Menu
void PopupAt(BaseWindow* window,
@@ -53,7 +56,7 @@ class MenuMac : public Menu {
// window ID -> open context menu
std::map<int32_t, ElectronMenuController*> popup_controllers_;
base::WeakPtrFactory<MenuMac> weak_factory_{this};
gin::WeakCellFactory<MenuMac> weak_cell_factory_{this};
};
} // namespace api

View File

@@ -15,6 +15,7 @@
#include "content/app_shim_remote_cocoa/render_widget_host_view_cocoa.h" // nogncheck
#include "content/browser/renderer_host/render_widget_host_view_mac.h" // nogncheck
#include "content/public/browser/browser_task_traits.h"
#include "gin/persistent.h"
#include "shell/browser/api/electron_api_base_window.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/browser/native_window.h"
@@ -57,6 +58,11 @@ MenuMac::~MenuMac() {
RemoveModelObserver();
}
void MenuMac::Trace(cppgc::Visitor* visitor) const {
Menu::Trace(visitor);
visitor->Trace(weak_cell_factory_);
}
void MenuMac::PopupAt(BaseWindow* window,
std::optional<WebFrameMain*> frame,
int x,
@@ -77,10 +83,13 @@ void MenuMac::PopupAt(BaseWindow* window,
// has run.
base::OnceClosure callback_with_ref = BindSelfToClosure(std::move(callback));
auto popup = base::BindOnce(&MenuMac::PopupOnUI, weak_factory_.GetWeakPtr(),
native_window->GetWeakPtr(), weak_frame,
window->weak_map_id(), x, y, positioning_item,
std::move(callback_with_ref));
v8::Isolate* isolate = v8::Isolate::GetCurrent();
auto popup = base::BindOnce(
&MenuMac::PopupOnUI,
gin::WrapPersistent(weak_cell_factory_.GetWeakCell(
isolate->GetCppHeap()->GetAllocationHandle())),
native_window->GetWeakPtr(), weak_frame, window->weak_map_id(), x, y,
positioning_item, std::move(callback_with_ref));
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(FROM_HERE,
std::move(popup));
}
@@ -121,9 +130,12 @@ void MenuMac::PopupOnUI(const base::WeakPtr<NativeWindow>& native_window,
return;
NSWindow* nswindow = native_window->GetNativeWindow().GetNativeNSWindow();
v8::Isolate* isolate = v8::Isolate::GetCurrent();
base::OnceClosure close_callback =
base::BindOnce(&MenuMac::OnClosed, weak_factory_.GetWeakPtr(), window_id,
std::move(callback));
base::BindOnce(&MenuMac::OnClosed,
gin::WrapPersistent(weak_cell_factory_.GetWeakCell(
isolate->GetCppHeap()->GetAllocationHandle())),
window_id, std::move(callback));
popup_controllers_[window_id] =
[[ElectronMenuController alloc] initWithModel:model()
useDefaultAccelerator:NO];
@@ -204,8 +216,12 @@ void MenuMac::PopupOnUI(const base::WeakPtr<NativeWindow>& native_window,
}
void MenuMac::ClosePopupAt(int32_t window_id) {
auto close_popup = base::BindOnce(&MenuMac::ClosePopupOnUI,
weak_factory_.GetWeakPtr(), window_id);
v8::Isolate* isolate = v8::Isolate::GetCurrent();
auto close_popup =
base::BindOnce(&MenuMac::ClosePopupOnUI,
gin::WrapPersistent(weak_cell_factory_.GetWeakCell(
isolate->GetCppHeap()->GetAllocationHandle())),
window_id);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, std::move(close_popup));
}

View File

@@ -7,6 +7,7 @@
#include <memory>
#include <utility>
#include "gin/persistent.h"
#include "shell/browser/api/electron_api_base_window.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/browser/native_window_views.h"
@@ -23,6 +24,11 @@ MenuViews::MenuViews(gin::Arguments* args) : Menu(args) {}
MenuViews::~MenuViews() = default;
void MenuViews::Trace(cppgc::Visitor* visitor) const {
Menu::Trace(visitor);
visitor->Trace(weak_cell_factory_);
}
void MenuViews::PopupAt(BaseWindow* window,
std::optional<WebFrameMain*> frame,
int x,
@@ -55,8 +61,11 @@ void MenuViews::PopupAt(BaseWindow* window,
// callback, it is fine passing OnceCallback to it because we reset the
// menu runner immediately when the menu is closed.
int32_t window_id = window->weak_map_id();
v8::Isolate* isolate = v8::Isolate::GetCurrent();
auto close_callback = electron::AdaptCallbackForRepeating(
base::BindOnce(&MenuViews::OnClosed, weak_factory_.GetWeakPtr(),
base::BindOnce(&MenuViews::OnClosed,
gin::WrapPersistent(weak_cell_factory_.GetWeakCell(
isolate->GetCppHeap()->GetAllocationHandle())),
window_id, std::move(callback_with_ref)));
auto& runner = menu_runners_[window_id] =
std::make_unique<MenuRunner>(model(), flags, std::move(close_callback));

View File

@@ -8,7 +8,7 @@
#include <memory>
#include "base/containers/flat_map.h"
#include "base/memory/weak_ptr.h"
#include "gin/weak_cell.h"
#include "shell/browser/api/electron_api_menu.h"
#include "ui/views/controls/menu/menu_runner.h"
@@ -19,6 +19,8 @@ class MenuViews : public Menu {
explicit MenuViews(gin::Arguments* args);
~MenuViews() override;
void Trace(cppgc::Visitor*) const override;
protected:
// Menu
void PopupAt(BaseWindow* window,
@@ -36,7 +38,7 @@ class MenuViews : public Menu {
// window ID -> open context menu
base::flat_map<int32_t, std::unique_ptr<views::MenuRunner>> menu_runners_;
base::WeakPtrFactory<MenuViews> weak_factory_{this};
gin::WeakCellFactory<MenuViews> weak_cell_factory_{this};
};
} // namespace electron::api

View File

@@ -16,6 +16,7 @@
#include "content/public/browser/storage_partition.h"
#include "electron/electron_version.h"
#include "gin/object_template_builder.h"
#include "gin/persistent.h"
#include "net/log/net_log_capture_mode.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/net/system_network_context_manager.h"
@@ -147,8 +148,9 @@ v8::Local<v8::Promise> NetLog::StartLogging(base::FilePath log_path,
file_task_runner_->PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(OpenFileForWriting, log_path),
base::BindOnce(&NetLog::StartNetLogAfterCreateFile,
weak_ptr_factory_.GetWeakPtr(), capture_mode,
max_file_size, std::move(custom_constants)));
gin::WrapPersistent(weak_factory_.GetWeakCell(
args->isolate()->GetCppHeap()->GetAllocationHandle())),
capture_mode, max_file_size, std::move(custom_constants)));
return handle;
}
@@ -236,6 +238,11 @@ const char* NetLog::GetHumanReadableName() const {
return "Electron / NetLog";
}
void NetLog::Trace(cppgc::Visitor* visitor) const {
gin::Wrappable<NetLog>::Trace(visitor);
visitor->Trace(weak_factory_);
}
// static
NetLog* NetLog::Create(v8::Isolate* isolate,
ElectronBrowserContext* browser_context) {

View File

@@ -8,12 +8,13 @@
#include <optional>
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/values.h"
#include "gin/weak_cell.h"
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/log/net_log_capture_mode.h"
#include "services/network/public/mojom/net_log.mojom.h"
#include "shell/common/gc_plugin.h"
#include "shell/common/gin_helper/promise.h"
namespace base {
@@ -56,6 +57,7 @@ class NetLog final : public gin::Wrappable<NetLog> {
v8::Isolate* isolate) override;
const gin::WrapperInfo* wrapper_info() const override;
const char* GetHumanReadableName() const override;
void Trace(cppgc::Visitor*) const override;
v8::Local<v8::Promise> StartLogging(base::FilePath log_path,
gin::Arguments* args);
@@ -74,13 +76,15 @@ class NetLog final : public gin::Wrappable<NetLog> {
private:
raw_ptr<ElectronBrowserContext> browser_context_;
GC_PLUGIN_IGNORE(
"Context tracking of remote is not needed in the browser process.")
mojo::Remote<network::mojom::NetLogExporter> net_log_exporter_;
std::optional<gin_helper::Promise<void>> pending_start_promise_;
scoped_refptr<base::TaskRunner> file_task_runner_;
base::WeakPtrFactory<NetLog> weak_ptr_factory_{this};
gin::WeakCellFactory<NetLog> weak_factory_{this};
};
} // namespace api

View File

@@ -8,7 +8,6 @@
#include "base/power_monitor/power_observer.h"
#include "gin/wrappable.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/common/gin_helper/self_keep_alive.h"
#if BUILDFLAG(IS_LINUX)
#include "shell/browser/lib/power_observer_linux.h"
@@ -89,13 +88,14 @@ class PowerMonitor final : public gin::Wrappable<PowerMonitor>,
// The window used for processing events.
HWND window_;
// Handle returned by RegisterSuspendResumeNotification.
HPOWERNOTIFY power_notify_handle_ = nullptr;
#endif
#if BUILDFLAG(IS_LINUX)
PowerObserverLinux power_observer_linux_{this};
#endif
gin_helper::SelfKeepAlive<PowerMonitor> keep_alive_{this};
};
} // namespace electron::api

View File

@@ -6,11 +6,14 @@
#include <vector>
#include "shell/common/gc_plugin.h"
#import <ApplicationServices/ApplicationServices.h>
#import <Cocoa/Cocoa.h>
@interface MacLockMonitor : NSObject {
@private
GC_PLUGIN_IGNORE("ObjC class cannot participate in cppgc tracing")
std::vector<electron::api::PowerMonitor*> emitters;
}

View File

@@ -45,14 +45,16 @@ void PowerMonitor::InitPlatformSpecificMonitors() {
// For Windows 8 and later, a new "connected standby" mode has been added and
// we must explicitly register for its notifications.
RegisterSuspendResumeNotification(static_cast<HANDLE>(window_),
DEVICE_NOTIFY_WINDOW_HANDLE);
power_notify_handle_ = RegisterSuspendResumeNotification(
static_cast<HANDLE>(window_), DEVICE_NOTIFY_WINDOW_HANDLE);
}
void PowerMonitor::DestroyPlatformSpecificMonitors() {
if (window_) {
WTSUnRegisterSessionNotification(window_);
UnregisterSuspendResumeNotification(static_cast<HANDLE>(window_));
if (power_notify_handle_) {
UnregisterSuspendResumeNotification(power_notify_handle_);
}
gfx::SetWindowUserData(window_, nullptr);
DestroyWindow(window_);
window_ = nullptr;

View File

@@ -14,7 +14,6 @@
#include "services/device/public/mojom/wake_lock_provider.mojom.h"
#include "services/service_manager/public/cpp/connector.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/node_includes.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.h"
@@ -53,11 +52,9 @@ PowerSaveBlocker::PowerSaveBlocker(v8::Isolate* isolate)
PowerSaveBlocker::~PowerSaveBlocker() = default;
// static
gin_helper::Handle<PowerSaveBlocker> PowerSaveBlocker::Create(
v8::Isolate* isolate) {
return gin_helper::CreateHandle(
isolate, cppgc::MakeGarbageCollected<PowerSaveBlocker>(
isolate->GetCppHeap()->GetAllocationHandle(), isolate));
PowerSaveBlocker* PowerSaveBlocker::Create(v8::Isolate* isolate) {
return cppgc::MakeGarbageCollected<PowerSaveBlocker>(
isolate->GetCppHeap()->GetAllocationHandle(), isolate);
}
const gin::WrapperInfo* PowerSaveBlocker::wrapper_info() const {

View File

@@ -9,21 +9,17 @@
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/device/public/mojom/wake_lock.mojom.h"
#include "shell/common/gc_plugin.h"
namespace gin {
class ObjectTemplateBuilder;
} // namespace gin
namespace gin_helper {
template <typename T>
class Handle;
} // namespace gin_helper
namespace electron::api {
class PowerSaveBlocker final : public gin::Wrappable<PowerSaveBlocker> {
public:
static gin_helper::Handle<PowerSaveBlocker> Create(v8::Isolate* isolate);
static PowerSaveBlocker* Create(v8::Isolate* isolate);
// gin::Wrappable
static const gin::WrapperInfo kWrapperInfo;
@@ -57,6 +53,8 @@ class PowerSaveBlocker final : public gin::Wrappable<PowerSaveBlocker> {
// Map from id to the corresponding blocker type for each request.
base::flat_map<int, device::mojom::WakeLockType> wake_lock_types_;
GC_PLUGIN_IGNORE(
"Context tracking of remote is not needed in the browser process.")
mojo::Remote<device::mojom::WakeLock> wake_lock_;
};

View File

@@ -21,7 +21,6 @@
#include "shell/common/gin_converters/image_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/node_includes.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/v8-cppgc.h"
@@ -345,7 +344,7 @@ void Tray::Focus() {
void Tray::PopUpContextMenu(gin::Arguments* args) {
if (!CheckAlive())
return;
gin_helper::Handle<Menu> menu;
Menu* menu = nullptr;
gfx::Point pos;
v8::Local<v8::Value> first_arg;
@@ -363,8 +362,8 @@ void Tray::PopUpContextMenu(gin::Arguments* args) {
}
}
tray_icon_->PopUpContextMenu(
pos, menu.IsEmpty() ? nullptr : menu->model()->GetWeakPtr());
tray_icon_->PopUpContextMenu(pos,
menu ? menu->model()->GetWeakPtr() : nullptr);
}
void Tray::CloseContextMenu() {

View File

@@ -12,8 +12,7 @@
#include "gin/object_template_builder.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_helper/event.h"
#include "shell/common/gin_helper/event_emitter.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
namespace gin_helper {

View File

@@ -11,6 +11,7 @@
#include "extensions/browser/media_capture_util.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
#include "shell/common/gin_helper/handle.h"
#include "v8/include/v8.h"
namespace extensions {

View File

@@ -15,6 +15,7 @@
#include "shell/browser/extensions/api/management/electron_management_api_delegate.h"
#include "shell/browser/extensions/electron_extension_web_contents_observer.h"
#include "shell/browser/extensions/electron_messaging_delegate.h"
#include "shell/common/gin_helper/handle.h"
#include "v8/include/v8.h"
#if BUILDFLAG(ENABLE_PRINTING)

View File

@@ -675,9 +675,9 @@ void ElectronURLLoaderFactory::StartLoadingHttp(
request->method != net::HttpRequestHeaders::kHeadMethod)
dict.Get("uploadData", &upload_data);
gin_helper::Handle<api::Session> session;
api::Session* session = nullptr;
auto* browser_context =
dict.Get("session", &session) && !session.IsEmpty()
dict.Get("session", &session) && session
? session->browser_context()
: ElectronBrowserContext::GetDefaultBrowserContext();

View File

@@ -53,6 +53,7 @@
#include "shell/browser/ui/inspectable_web_contents_view.h"
#include "shell/browser/ui/inspectable_web_contents_view_delegate.h"
#include "shell/common/application_info.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/platform_util.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "third_party/blink/public/common/logging/logging_utils.h"

19
shell/common/gc_plugin.h Normal file
View File

@@ -0,0 +1,19 @@
// Copyright (c) 2025 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_GC_PLUGIN_H_
#define ELECTRON_SHELL_COMMON_GC_PLUGIN_H_
// GC_PLUGIN_IGNORE is used to make the Blink GC plugin ignore a particular
// class or field when checking for proper usage. When using GC_PLUGIN_IGNORE
// a reason must be provided as an argument.
// Based on third_party/blink/renderer/platform/wtf/gc_plugin.h
#if defined(__clang__)
#define GC_PLUGIN_IGNORE(reason) \
__attribute__((annotate("blink_gc_plugin_ignore")))
#else
#define GC_PLUGIN_IGNORE(reason)
#endif
#endif // ELECTRON_SHELL_COMMON_GC_PLUGIN_H_

View File

@@ -20,6 +20,7 @@
#include "shell/common/gin_converters/gfx_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/handle.h"
#include "third_party/blink/public/common/context_menu_data/untrustworthy_context_menu_params.h"
#include "third_party/blink/public/mojom/permissions/permission_status.mojom.h"
#include "ui/base/mojom/menu_source_type.mojom.h"

View File

@@ -8,6 +8,7 @@
#include "content/public/browser/render_process_host.h"
#include "shell/browser/api/electron_api_web_frame_main.h"
#include "shell/common/gin_helper/accessor.h"
#include "shell/common/gin_helper/handle.h"
#include "shell/common/node_util.h"
namespace gin {

View File

@@ -16,6 +16,7 @@
#include "gin/public/gin_embedders.h"
#include "shell/common/gin_helper/destroyable.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "v8/include/cppgc/macros.h"
#include "v8/include/v8-external.h"
#include "v8/include/v8-microtask-queue.h"
#include "v8/include/v8-template.h"
@@ -189,6 +190,9 @@ void ThrowConversionError(gin::Arguments* args,
// at position |index|.
template <size_t index, typename ArgType, typename = void>
struct ArgumentHolder {
CPPGC_STACK_ALLOCATED();
public:
using ArgLocalType = typename CallbackParamTraits<ArgType>::LocalType;
ArgLocalType value;
@@ -222,6 +226,9 @@ struct ArgumentHolder<
std::is_constructible_v<
typename CallbackParamTraits<ArgType>::LocalType,
v8::Isolate*>>> {
CPPGC_STACK_ALLOCATED();
public:
using ArgLocalType = typename CallbackParamTraits<ArgType>::LocalType;
ArgLocalType value;

View File

@@ -7,6 +7,7 @@
#include "base/memory/raw_ptr.h"
#include "gin/converter.h"
#include "v8/include/cppgc/type-traits.h"
namespace gin_helper {
@@ -16,8 +17,15 @@ namespace gin_helper {
// C++ to V8 can cause memory leaks. Copied from
// https://chromium-review.googlesource.com/c/chromium/src/+/6734440 Should be
// removed once https://github.com/electron/electron/issues/47922 is complete.
//
// This class must NOT be used with cppgc-managed types (gin::Wrappable).
// For cppgc types, use T* directly and gin::Converter<T*> for V8 conversion.
template <typename T>
class Handle {
static_assert(!cppgc::IsGarbageCollectedTypeV<T>,
"gin_helper::Handle must not be used with cppgc "
"garbage-collected types. Use T* directly instead.");
public:
Handle() : object_(nullptr) {}

View File

@@ -6,6 +6,7 @@
#define ELECTRON_SHELL_COMMON_GIN_HELPER_SELF_KEEP_ALIVE_H_
#include "gin/weak_cell.h"
#include "shell/common/gc_plugin.h"
namespace gin_helper {
@@ -28,6 +29,7 @@ class SelfKeepAlive final {
explicit operator bool() const { return keep_alive_; }
private:
GC_PLUGIN_IGNORE("Allowed to keep a Persistent to itself.")
cppgc::Persistent<Self> keep_alive_;
};

View File

@@ -8,6 +8,7 @@
#include "base/process/process.h"
#include "base/process/process_metrics.h"
#include "shell/common/api/electron_bindings.h"
#include "shell/common/gc_plugin.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
@@ -204,6 +205,9 @@ class PreloadRealmLifetimeController
std::unique_ptr<base::ProcessMetrics> metrics_;
raw_ptr<ServiceWorkerData> service_worker_data_;
GC_PLUGIN_IGNORE(
"Intentional GC root to keep this object alive until the context is "
"destroyed")
blink::Persistent<PreloadRealmLifetimeController> self_;
};

View File

@@ -290,6 +290,92 @@ describe('cpp heap', () => {
});
});
describe('menu module', () => {
// Regression test for https://github.com/electron/electron/issues/50791
it('should not leak when rebuilding application menu', async () => {
const { remotely } = await startRemoteControlApp(['--js-flags=--expose-gc']);
const result = await remotely(async () => {
const { Menu } = require('electron');
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
const { getCppHeapStatistics } = require('node:v8');
function buildLargeMenu () {
return Menu.buildFromTemplate(
Array.from({ length: 10 }, (_, i) => ({
label: `Menu ${i}`,
submenu: Array.from({ length: 20 }, (_, j) => ({
label: `Item ${i}-${j}`,
click: () => {}
}))
}))
);
}
async function rebuildAndMeasure (n: number) {
for (let i = 0; i < n; i++) {
Menu.setApplicationMenu(buildLargeMenu());
}
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 0));
v8Util.requestGarbageCollectionForTesting();
}
return getCppHeapStatistics('brief').used_size_bytes;
}
await rebuildAndMeasure(50);
const after1 = await rebuildAndMeasure(50);
const after2 = await rebuildAndMeasure(50);
return { after1, after2 };
});
const growth = result.after2 - result.after1;
expect(growth).to.be.at.most(
result.after1 * 0.1,
`C++ heap grew by ${growth} bytes between two identical rounds of 100 menu rebuilds — likely a leak`
);
});
});
describe('powerMonitor module', () => {
it('should retain native PowerMonitor via JS module reference', async () => {
const rc = await startRemoteControlApp(['--expose-internals', '--js-flags=--expose-gc']);
const result = await rc.remotely(async (heap: string) => {
const { powerMonitor, app } = require('electron');
const { recordState } = require(heap);
const v8Util = (process as any)._linkedBinding('electron_common_v8_util');
// Register a listener to trigger native PowerMonitor creation.
const listener = () => {};
powerMonitor.on('suspend', listener);
// Add and remove several listeners to exercise the path,
// then GC to ensure no duplicate native objects are created.
for (let i = 0; i < 5; i++) {
const tmp = () => {};
powerMonitor.on('resume', tmp);
powerMonitor.removeListener('resume', tmp);
}
v8Util.requestGarbageCollectionForTesting();
const state = recordState();
const nodes = state.snapshot.filter(
(node: any) => node.name === 'Electron / PowerMonitor'
);
const found = nodes.length > 0;
const noDuplicates = nodes.length === 1;
setTimeout(() => app.quit());
return { found, noDuplicates };
}, path.join(__dirname, '../../third_party/electron_node/test/common/heap'));
const [code] = await once(rc.process, 'exit');
expect(code).to.equal(0);
expect(result.found).to.equal(true, 'PowerMonitor should be in snapshot (held by JS module)');
expect(result.noDuplicates).to.equal(true, 'should have exactly one PowerMonitor instance');
});
});
describe('internal event', () => {
it('should record as node in heap snapshot', async () => {
const { remotely } = await startRemoteControlApp(['--expose-internals']);