Repository: electron/electron
Pull request: fix: bind offscreen paint callback to child `WebContents`

#### Description of Change

Refs https://github.com/electron/electron/pull/47868

Previously, `MaybeOverrideCreateParamsForNewWindow` bound the `OffScreenWebContentsView`'s paint callback to the parent `WebContents` using `base::Unretained(this)`. This was both unsafe (dangling pointer risk if the parent is destroyed before the child) and semantically incorrect — paint events belong to the child window, not the opener.

Replace the callback in `MaybeOverrideCreateParamsForNewWindow` with `base::DoNothing()`, then rebind it to the child `WebContents` in `AddNewContents` via a new `SetCallback` method on `OffScreenWebContentsView`.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [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: shell/browser/api/electron_api_web_contents.cc
patch: @@ -1307,11 +1307,12 @@ void WebContents::MaybeOverrideCreateParamsForNewWindow(
          dict.Get(options::kOffscreen, &is_offscreen) && is_offscreen);
 
     if (is_offscreen) {
+      // Use a no-op callback here. The real OnPaint callback will be bound
+      // to the child WebContents in AddNewContents via SetCallback().
       auto* view = new OffScreenWebContentsView(
           false, offscreen_use_shared_texture_,
           offscreen_shared_texture_pixel_format_,
-          offscreen_device_scale_factor_,
-          base::BindRepeating(&WebContents::OnPaint, base::Unretained(this)));
+          offscreen_device_scale_factor_, base::DoNothing());
       create_params->view = view;
       create_params->delegate_view = view;
     }
@@ -1339,6 +1340,15 @@ content::WebContents* WebContents::AddNewContents(
   v8::HandleScope handle_scope(isolate);
   auto api_web_contents = CreateAndTake(isolate, std::move(new_contents), type);
 
+  // Rebind the paint callback to the child WebContents. The
+  // OffScreenWebContentsView was initially created with the parent's OnPaint
+  // in MaybeOverrideCreateParamsForNewWindow, but the paint data
+  // belongs to the child.
+  if (auto* osr_view = api_web_contents->GetOffScreenWebContentsView()) {
+    osr_view->SetCallback(base::BindRepeating(&WebContents::OnPaint,
+                                              api_web_contents->GetWeakPtr()));
+  }
+
   // We call RenderFrameCreated here as at this point the empty "about:blank"
   // render frame has already been created.  If the window never navigates again
   // RenderFrameCreated won't be called and certain prefs like

filename: shell/browser/osr/osr_web_contents_view.cc
patch: @@ -47,6 +47,10 @@ void OffScreenWebContentsView::SetWebContents(
     view->InstallTransparency();
 }
 
+void OffScreenWebContentsView::SetCallback(const OnPaintCallback& callback) {
+  callback_ = callback;
+}
+
 void OffScreenWebContentsView::SetNativeWindow(NativeWindow* window) {
   if (native_window_)
     native_window_->RemoveObserver(this);

filename: shell/browser/osr/osr_web_contents_view.h
patch: @@ -44,6 +44,7 @@ class OffScreenWebContentsView : public content::WebContentsView,
 
   void SetWebContents(content::WebContents*);
   void SetNativeWindow(NativeWindow* window);
+  void SetCallback(const OnPaintCallback& callback);
 
   // NativeWindowObserver:
   void OnWindowResize() override;