Repository: electron/electron
Pull request: fix: prevent use-after-free in PowerMonitor via dangling OS callbacks

#### Description of Change

PowerMonitor registered OS-level callbacks (HWND UserData and WTS/suspend notifications on Windows, shutdown handler and lock-screen observer on macOS) but never cleaned them up in its destructor. The JS layer also only held the native object in a closure-local variable, allowing GC to reclaim it while those registrations still referenced freed memory.

To fix this we retain the native PowerMonitor at module level in `power-monitor.ts` so it cannot be garbage-collected, and add `DestroyPlatformSpecificMonitors()` to properly tear down OS registrations on destruction. On Windows, unregister WTS and suspend notifications, clear `GWLP_USERDATA`, and destroy the HWND; on macOS, remove the emitter from the global `MacLockMonitor` and reset the Browser shutdown handler.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: none

Changed files:
filename: lib/browser/api/power-monitor.ts
patch: @@ -8,13 +8,19 @@ const {
   isOnBatteryPower
 } = process._linkedBinding('electron_browser_power_monitor');
 
+// Hold the native PowerMonitor at module level so it is never garbage-collected
+// while this module is alive. The C++ side registers OS-level callbacks (HWND
+// user-data on Windows, shutdown handler on macOS, notification observers) that
+// prevent safe collection of the C++ wrapper while those registrations exist.
+let pm: any;
+
 class PowerMonitor extends EventEmitter implements Electron.PowerMonitor {
   constructor () {
     super();
     // Don't start the event source until both a) the app is ready and b)
     // there's a listener registered for a powerMonitor event.
     this.once('newListener', () => {
-      const pm = createPowerMonitor();
+      pm = createPowerMonitor();
       pm.emit = this.emit.bind(this);
 
       if (process.platform === 'linux') {

filename: shell/browser/api/electron_api_power_monitor.cc
patch: @@ -80,6 +80,14 @@ PowerMonitor::PowerMonitor() {
 }
 
 PowerMonitor::~PowerMonitor() {
+#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
+  DestroyPlatformSpecificMonitors();
+#endif
+
+#if BUILDFLAG(IS_MAC)
+  Browser::Get()->SetShutdownHandler(base::RepeatingCallback<bool()>());
+#endif
+
   auto* power_monitor = base::PowerMonitor::GetInstance();
   power_monitor->RemovePowerStateObserver(this);
   power_monitor->RemovePowerSuspendObserver(this);

filename: shell/browser/api/electron_api_power_monitor.h
patch: @@ -54,6 +54,7 @@ class PowerMonitor final : public gin::Wrappable<PowerMonitor>,
 
 #if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
   void InitPlatformSpecificMonitors();
+  void DestroyPlatformSpecificMonitors();
 #endif
 
   // base::PowerStateObserver implementations:

filename: shell/browser/api/electron_api_power_monitor_mac.mm
patch: @@ -15,6 +15,7 @@ @interface MacLockMonitor : NSObject {
 }
 
 - (void)addEmitter:(electron::api::PowerMonitor*)monitor_;
+- (void)removeEmitter:(electron::api::PowerMonitor*)monitor_;
 
 @end
 
@@ -62,6 +63,10 @@ - (void)addEmitter:(electron::api::PowerMonitor*)monitor_ {
   self->emitters.push_back(monitor_);
 }
 
+- (void)removeEmitter:(electron::api::PowerMonitor*)monitor_ {
+  std::erase(self->emitters, monitor_);
+}
+
 - (void)onScreenLocked:(NSNotification*)notification {
   for (auto* emitter : self->emitters) {
     emitter->Emit("lock-screen");
@@ -98,4 +103,9 @@ - (void)onUserDidResignActive:(NSNotification*)notification {
   [g_lock_monitor addEmitter:this];
 }
 
+void PowerMonitor::DestroyPlatformSpecificMonitors() {
+  if (g_lock_monitor)
+    [g_lock_monitor removeEmitter:this];
+}
+
 }  // namespace electron::api

filename: shell/browser/api/electron_api_power_monitor_win.cc
patch: @@ -49,6 +49,20 @@ void PowerMonitor::InitPlatformSpecificMonitors() {
                                     DEVICE_NOTIFY_WINDOW_HANDLE);
 }
 
+void PowerMonitor::DestroyPlatformSpecificMonitors() {
+  if (window_) {
+    WTSUnRegisterSessionNotification(window_);
+    UnregisterSuspendResumeNotification(static_cast<HANDLE>(window_));
+    gfx::SetWindowUserData(window_, nullptr);
+    DestroyWindow(window_);
+    window_ = nullptr;
+  }
+  if (atom_) {
+    UnregisterClass(MAKEINTATOM(atom_), instance_);
+    atom_ = 0;
+  }
+}
+
 LRESULT CALLBACK PowerMonitor::WndProcStatic(HWND hwnd,
                                              UINT message,
                                              WPARAM wparam,
@@ -76,7 +90,7 @@ LRESULT CALLBACK PowerMonitor::WndProc(HWND hwnd,
     }
     if (should_treat_as_current_session) {
       if (wparam == WTS_SESSION_LOCK) {
-        // Unretained is OK because this object is eternally pinned.
+        // SelfKeepAlive prevents GC of this object, so Unretained is safe.
         content::GetUIThreadTaskRunner({})->PostTask(
             FROM_HERE,
             base::BindOnce([](PowerMonitor* pm) { pm->Emit("lock-screen"); },